netlib.narod.ru< Назад | Оглавление | Далее >

Создание игровых экранов

Во всех современных играх есть много экранов: начальный экран, экран для инструкций, экран для самой игры и т.д. Поскольку на каждом экране показывается гораздо больше, чем простое изображение, в игровой индустрии эти экраны обычно называют сценами.

Сцена состоит (обычно) из некоего фонового изображения, фоновой музыки и группы «актеров», которые «играют» на сцене, показывая пользователю некую информацию об игре.

Для примера взгляните на начальный экран Rock Rain Enhanced, показанный на рис. 4.1.


Рис. 4.1. Начальный экран

Рис. 4.1. Начальный экран


В этой сцене у вас есть красивый фон экрана, два слова, выходящие из-за краев экрана и образующе фразу «Rock Rain», а также меню с командами для игры и фоновой музыкой.

Обратите внимание, что в этой сцене есть несколько «актеров». Помимо спрайтов, размещенных в форме названия игры, у вас есть анимированное меню, по которому можно перемещаться с помощью игрового пульта Xbox 360 или клавиатуры. Эта группа изображений, звуков и актеров формирует сцену. Пользователь может перейти к другой сцене, согласно командам меню. В этой версии Rock Rain у вас три сцены: начальная сцена, сцена помощи и сцена игры. Рис. 4.2 показывает взаимосвязь между этими игровыми сценами.


Рис. 4.2. Взаимосвязь игровых сцен

Рис. 4.2. Взаимосвязь игровых сцен


Теперь, используя термины XNA, каждая сцена это GameComponent, имеющий другие GameComponent, представляющие актеров сцены. У каждой сцены есть собственные уникальные качества, но некоторые вещи являются общими. Например, каждая сцена содержит собственную коллекцию GameComponent, представляющую актеров в этой сцене. Также в каждой сцене есть метод, показывающий или закрывающий ее, согласно выбору пользователя в потоке сцен (например, когда вы открываете сцену игры, вы также закрываете начальную сцену).

Также у вас должна быть возможность включить паузу в каждой сцене. Это полезно, когда вы, например, хотите прервать игру, чтобы быстро сбегать в ванную. Для этого достаточно просто не выполнять метод Update, принадлежащий GameComponent сцены. Вспомните, что XNA вызывает метод Update чтобы обновить состояние GameComponent. Если вызова нет, GameComponent не будет обновлен и игровая сцена окажется «остановленной».

В этой архитектуре единственные GameComponent, которые будут добавлены к списку компонентов игры, это сцены, поскольку другие GameComponent, которые собственно образуют сцены, будут добавлены к списку компонентов соответствующей сцены.

Изначально вы создаете класс, реализующий общую функциональность сцен, добавив новый GameComponent, названный GameScene. В целях организации проекта поместите его в папку Core.

Начнем с кода. Во-первых, поскольку ваша сцена это визуальный компонент, она наследуется от DrawableGameComponent, а не от GameComponent. Затем, как упоминалось, каждая сцена содержит свой собственный список актеров, что для вас означает собственный список GameComponent. Начнем класс со следующих объявлений:

/// <summary>
/// Список дочерних GameComponent
/// </summary>
private readonly List<GameComponent> components;

Также добавим свойство, возвращающее список components, чтобы у нас была возможность добавлять к сцене новых актеров из наследуемых классов:

/// <summary>
/// Компоненты игровой сцены
/// </summary>
public List<GameComponent> Components
{
    get { return components; }
}

В конструкторе класса вы инициализируете данный список и указываете, что изначально компонент не будет видим и его состояние не будет обновляться, используя атрибуты Visible и Enable класса DrawableGameComponent:

/// <summary>
/// Конструктор по умолчанию
/// </summary>
public GameScene(Game game) : base(game)
{
    components = new List<GameComponent>();

    Visible = false;
    Enabled = false;
}

Теперь, чтобы показать или скрыть сцену, надо изменить значения этих атрибутов. Создадим для этого два метода:

/// <summary>
/// Показать сцену
/// </summary>
public virtual void Show()
{
    Visible = true;
    Enabled = true;
}

/// <summary>
/// Скрыть сцену
/// </summary>
public virtual void Hide()
{
    Visible = false;
    Enabled = false;
}

Теперь вы должны правильно обращаться с актерами сцены. Для каждого вызова метода Update сцены вы должны вызвать соответствующий метод каждого актера сцены, чтобы изменить их состояние. Если объект сцены отключен (Enabled = false), то XNA не будет вызывать метод Update, и также не будет обновлен ни один из актеров сцены, поскольку их соответствующие методы Update не будут выполнены:

/// <summary>
/// Позволяет GameComponent обновить себя
/// </summary>
/// <param name="gameTime">Предоставляет снимок значения таймера.</param>
public override void Update(GameTime gameTime)
{
    // Обновление дочерних GameComponents (если включены)
    for (int i = 0; i < components.Count; i++)
    {
        if (components[i].Enabled)
        {
            components[i].Update(gameTime);
        }
    }

    base.Update(gameTime);
}

Код рисования для актеров похож. Для каждого исполнения метода Draw сцены вызываются методы Draw каждого DrawableGameComponent, включенного в список компонентов сцены:

/// <summary>
/// Позволяет GameComponent рисовать свое содержимое на игровом экране
/// </summary>
public override void Draw(GameTime gameTime)
{
    // Рисование дочерних GameComponents (если видимы)
    for (int i = 0; i < components.Count; i++)
    {
        GameComponent gc = components[i];

        if ((gc is DrawableGameComponent) &&
            ((DrawableGameComponent) gc).Visible)
        {
            ((DrawableGameComponent) gc).Draw(gameTime);
        }
    }

    base.Draw(gameTime);
}

Если коротко, все, что делает этот GameComponent — корректное управление вызовами методов Draw и Update класса игры, и рисование и обновление всех других GameComponent, образующих сцену. Также заметьте, что методы Show и Hide показывают и скрывают игровую сцену, предотвращая исполнение методов Draw и Update с использованием свойств Visible и Enabled. Просто, не так ли?

Давайте создадим три GameComponent, унаследованных от этого класса: один для начальной сцены игры, другой для сцены помощи и еще один собственно для сцены игры. Класс игры будет показывать правильную сцену, согласно состоянию игры. Таким образом, вы начинаете с начальной сцены, затем игрок может перейти к сцене игры и вернуться назад к начальной сцене, когда потеряет все свои жизни. Кроме того, игрок может выбрать переход от начальной сцены к сцене помощи и так далее, пока он не выберет вариант, чтобы покинуть начальную сцену.

Итак, добавим три GameComponent с именами StartScene, HelpScene и ActionScene, соответственно. Начнем со сцены помощи, объявленной в классе Game1 вашей игры так:

// Сцены игры
protected HelpScene helpScene;

// Активная сцена игры
protected GameScene activeScene;

Обратите внимание, что эти три GameComponent будут наследоваться от показанного ранее класса GameScene. Однако вам не надо менять их сейчас — мы вскоре вернемся к каждому из них. Атрибут activeScene содержит активную в данный момент сцену игры.

Создание экрана помощи

Давайте начнем с самой простой сцены в нашей игре. В этой сцене вы показываете инструкцию к игре, и пользователь может нажать кнопку A на игровом пульте Xbox 360 или клавишу Enter на клавиатуре, чтобы вернуться к начальной сцене.

Эта сцена содержит только инструкции о том, как играть в игру, и вы можете создать ее, просто показывая простое изображение с инструкцией к игре. Однако, поскольку сцена состоит из GameComponent, сперва вам нужен один GameComponent для рисования изображений.

Добавим новый GameComponent к папке Core и назовем его ImageComponent.cs. Снова этот компонент является визуальным компонентом, поэтому он наследуется от DrawableGameComponent, а не от GameComponent.

Этот GameComponent способен рисовать текстуру на экране, центрируя или растягивая ее для соответствия размеров изображения и экрана. Чтобы сделать это, добавьте следующее перечисление, которое будет использовать конструктор, чтобы информировать компонент о том, как должно рисоваться изображение:

public enum DrawMode
{
    Center = 1,
    Stretch,
};

Вы уже знаете, что вам, помимо соответствующего атрибута, описывающего, как изображение будет рисоваться в данном случае, нужен объект Texture2D, объект Rectangle и объект SpriteBatch для рисования изображения. Объявите эти объекты в классе:

// Рисуемая текстура
protected readonly Texture2D texture;

// Режим рисования
protected readonly DrawMode drawMode;

// SpriteBatch
protected SpriteBatch spriteBatch = null;

// Прямоугольник изображения
protected Rectangle imageRect;

В конструкторе класса вычисляем прямоугольник, определяющий местоположение изображения на экране, в зависимости от режима рисования, определяемого значением перечисления DrawMode:

/// <summary>
/// Конструктор по умолчанию
/// </summary>
/// <param name="game">Объект игры</param>
/// <param name="texture">Рисуемая текстура</param>
/// <param name="drawMode">Режим рисования</param>
public ImageComponent(Game game, Texture2D texture, DrawMode drawMode)
       : base(game)
{
    this.texture  = texture;
    this.drawMode = drawMode;

    // Получаем текущий пакет спрайтов
    spriteBatch = (SpriteBatch)
             Game.Services.GetService(typeof (SpriteBatch));

    // Создаем прямоугольник с размерами и местоположением изображения
    switch (drawMode)
    {
        case DrawMode.Center:
            imageRect = new Rectangle((Game.Window.ClientBounds.Width -
                    texture.Width)/2,(Game.Window.ClientBounds.Height -
                    texture.Height)/2,texture.Width, texture.Height);
            break;
        case DrawMode.Stretch:
            imageRect = new Rectangle(0, 0, Game.Window.ClientBounds.Width,
                    Game.Window.ClientBounds.Height);
            break;
    }
}

В методе Draw вы просто используете объект SpriteBatch для рисования изображения:

/// <summary>
/// Позволяет GameComponent рисовать себя
/// </summary>
/// <param name="gameTime">Предоставляет снимок значения таймера</param>
public override void Draw(GameTime gameTime)
{
    spriteBatch.Draw(texture, imageRect, Color.White);
    base.Draw(gameTime);
}

Обратите внимание, что помещение изображения на сцену может оказаться не таким простым, как думалось раньше. Если изображение показывается на телевизоре или на обычном мониторе с соотношением сторон 4:3, оно отображается правильно. Однако если оно рисуется на широкоэкранном мониторе или телевизоре, изображение может искажаться и выглядеть на экране неестественно.

Итак, вы можете создать два изображения: одно для мониторов и телевизоров с экраном 4:3, и другое для широкоэкранных. Вы можете выбирать рисуемое изображение в зависимости от типа экрана, хотя в этом случае вам всегда придется создавать две версии каждого изображения, которое вы хотите показать. Другой часто используемый подход заключается в рисовании двух перекрывающихся изображений. Одно изображение является фоном, искажающимся, чтобы занимать весь экран (широкоэкранный или нет), а другое рисуется поверх него в центре, так что выглядит нормально, независимо от того широкоэкранный у вас монитор или нет. В вашей игре вы будете использовать текстуры, показанные на рис. 4.3.


Рис. 4.3. Изображения, являющиеся частями сцены помощи

Рис. 4.3. Изображения, являющиеся частями сцены помощи


Также заметьте усовершенствования, внесенные в обработку ввода. Вы всегда сравниваете предыдущее состояние устройства с его текущим состоянием, чтобы определять факт нажатия пользователем кнопки или клавиши в текущей сцене.

Таким образом, в вашей сцене помощи есть всего два GameComponent, которые рисуют изображения: один для рисования фонового изображения и другой для рисования изображения с инструкциями на переднем плане. Добавьте новый класс с именем HelpScene и поместите в него код из листинга 4.1.


Листинг 4.1. Игровой компонент HelpScene


#region Инструкции Using 
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using RockRainEnhanced.Core;
#endregion

namespace RockRainEnhanced
{
    /// <summary>
    /// Это GameComponent представляющий сцену помощи
    /// </summary>
    public class HelpScene : GameScene
    {
        public HelpScene(Game game, Texture2D textureBack, Texture2D textureFront)
             : base(game)
        {
            Components.Add(new ImageComponent(game, textureBack,
                           ImageComponent.DrawMode.Stretch));
            Components.Add(new ImageComponent(game, textureFront,
                           ImageComponent.DrawMode.Center));
        }
    }
}

Также добавьте следующий код в класс Game1 и измените метод LoadContent, чтобы увидеть этот компонент в действии. Вы просто загружаете связанное содержимое, создаете экземпляр HelpScene, и выполняете метод Show объекта HelpScene:

// Текстуры
protected Texture2D helpBackgroundTexture, helpForegroundTexture;

/// <summary>
/// LoadContent вызывается один раз в игре
/// и это место для загрузки всего вашего содержимого
/// </summary>
protected override void LoadContent()
{
    // Создание нового SpriteBatch,
    // который будет использован для рисования текстур
    spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
    Services.AddService(typeof (SpriteBatch), spriteBatch);

    // Создание сцены с инструкциями
    helpBackgroundTexture = Content.Load<Texture2D>("helpbackground");
    helpForegroundTexture = Content.Load<Texture2D>("helpForeground");
    helpScene = new HelpScene(this, helpBackgroundTexture,
                              helpForegroundTexture);
    Components.Add(helpScene);
    helpScene.Show();
    activeScene = helpScene;
}

Выполните код. Результат представлен на рис. 4.4. Посмотрите, как сцена соразмерно отображается в обычном формате (4:3) и в широкоэкранном (16:9).


Рис. 4.4. Сцена помощи в нормальном и широкоэкранном формате

Рис. 4.4. Сцена помощи в нормальном и широкоэкранном формате


Создание начального экрана

Начальный экран игры всегда передает «вкус» самой игры. Обычно он показывает что-то впечатляющее, что должно представлять некоторые возможности игры, и меню, дающее пользователю возможность перемещаться между самой игрой, настройками, помощью и т.д.

Для Rock Rain вы создадите сцену с названием игры, отображаемым большими буквами, появляющимися из-за края экрана, и меню прямо под ним (в стиле аркадных игр 1980-х годов), с фоном с некоей астероидной темой. Для этого вы используете текстуры, показанные на рис. 4.5.


Рис. 4.5. Текстуры для начального экрана

Рис. 4.5. Текстуры для начального экрана


Итак, у вас в начальной сцене есть четыре актера. Первый, с именем Rock, появляется на сцене слева и перемещается в центр. Второй, с именем Rain, появляется справа и также перемещается в центр экрана. Третий, с именем Enchanced, мерцает чуть ниже слова Rain.

Четвертый актер отображается после трех предыдущих, и это меню с игровыми вариантами. Поскольку это несколько сложнее, чем простая анимация спрайтов, вы сперва создадите GameComponent для поддержки меню.

Создание компонента для меню

Ваше меню для игры должно быть простым и функциональным одновременно. Оно будет рисоваться с использованием двух шрифтов, причем больший шрифт применяется для подсветки выбранного пункта.

Начнем с добавления в папку Core нового GameComponent с именем TextMenuComponent. И снова это визуальный компонент,так что наследуется он от DrawableGameComponent вместо GameComponent.

В этом компоненте вам необходимо два шрифта для рисования текста в обычном и выделенном состоянии, список строк с рисукмыми элементами, цвет для обычного и выделенного элементов, размер и местоположение меню и, как всегда, объект SpriteBatch для рисования текста на экране. Итак, добавим к классу следующий код для объявления всех этих объектов:

// SpriteBatch
protected SpriteBatch spriteBatch = null;

// Шрифты
protected readonly SpriteFont regularFont, selectedFont;

// Цвета
protected Color regularColor  = Color.White,
                selectedColor = Color.Red;

// Местоположение меню
protected Vector2 position = new Vector2();

// Элементы
protected int selectedIndex = 0;
private readonly StringCollection menuItems;

// Размер меню в пикселях
protected int width, height;

Также добавим набор свойств для управления этими атрибутами:

/// <summary>
/// Установка пунктов меню
/// </summary>
/// <param name="items">Элементы меню</param>
public void SetMenuItems(string[] items)
{
    menuItems.Clear();
    menuItems.AddRange(items);
    CalculateBounds();
}

/// <summary>
/// Ширина меню в пикселях
/// </summary>
public int Width
{
    get { return width; }
}

/// <summary>
/// Высота меню в пикселях
/// </summary>
public int Height
{
    get { return height; }
}

/// <summary>
/// Индекс выбранного пункта меню
/// </summary>
public int SelectedIndex
{
    get { return selectedIndex; }
    set { selectedIndex = value; }
}

/// <summary>
/// Обычный цвет меню
/// </summary>
public Color RegularColor
{
    get { return regularColor; }
    set { regularColor = value; }
}

/// <summary>
/// Цвет выбранного пункта
/// </summary>
public Color SelectedColor
{
    get { return selectedColor; }
    set { selectedColor = value; }
}

/// <summary>
/// Местоположение компонента на экране
/// </summary>
public Vector2 Position
{
    get { return position; }
    set { position = value; }
}

Обратите внимание на CalculateBounds() в методе SetMenuItems. Элементы меню рисуются отцентрированными по горизонтали. Для этого вам надо вычислить ширину и высоту меню — значения, которые могут меняться в зависимости от добавленных к компоненту элементов и размера шрифта. Метод CalculateBounds выполняет требуемые вычисления, используя метод MeasureString класса SpriteFont, возвращающий размер в пикселях использующей данный шрифт строки:

/// <summary>
/// Получение границ меню
/// </summary>
protected void CalculateBounds()
{
    width  = 0;
    height = 0;

    foreach (string item in menuItems)
    {
        Vector2 size = selectedFont.MeasureString(item);

        if (size.X > width)
        {
            width = (int) size.X;
        }

        height += selectedFont.LineSpacing;
    }
}

Метод Draw, который рисует эти элементы, прост, поскольку вам надо только в цикле рисовать каждый элемент, один под другим, используя правильный шрифт для выбранных и обычных пунктов. Каждый элемент рисуется со слегка перекрывающейся тенью, создаваемой двойным рисованием одного и того же текста, что обеспечивает лучший вид текста. Код этого метода такой:

/// <summary>
/// Позволяет GameComponent рисовать себя
/// </summary>
/// <param name="gameTime">Предоставляет снимок значения таймера</param>
public override void Draw(GameTime gameTime)
{
    float y = position.Y;

    for (int i = 0; i < menuItems.Count; i++)
    {
        SpriteFont font;
        Color theColor;

        if (i == SelectedIndex)
        {
            font = selectedFont;
            theColor = selectedColor;
        }
        else
        {
            font = regularFont;
            theColor = regularColor;
        }

        // Рисуем тень текста
        spriteBatch.DrawString(font, menuItems[i],
              new Vector2(position.X + 1, y + 1), Color.Black);

        // Рисуем текстовый элемент
        spriteBatch.DrawString(font, menuItems[i],
              new Vector2(position.X, y), theColor);
        y += font.LineSpacing;
    }
    base.Draw(gameTime);
}

Фактически, отвечающая за рисование часть класса является самой простой. Компонент также должен обрабатывать пользовательский ввод, используя клавиатуру (клавиши перемещения курсора вверх и вниз) или игровой пульт Xbox 360. Возможно, вы хотите, чтобы некие звуковые эффекты уведомляли пользователей, когда они меняют или выбирают пункт меню. В этом случае добавьте к классу несколько новых атрибутов для поддержки звука и пользовательского ввода:

// Используется для поддержки ввода
protected KeyboardState oldKeyboardState;
protected GamePadState  oldGamePadState;

// Для звуковых эффектов
protected AudioComponent audioComponent;

Обработку пользовательского ввода следует размещать в методе Update, как вы делали раньше. Вы просто проверяете состояние клавиатуры и игрового пульта, как видели в предыдущей главе, для изменения значения атрибута selectedIndex:

/// <summary>
/// Позволяет GameComponent обновлять себя
/// </summary>
/// <param name="gameTime">Предоставляет снимок значения таймера</param>
public override void Update(GameTime gameTime)
{
    GamePadState gamepadState   = GamePad.GetState(PlayerIndex.One);
    KeyboardState keyboardState = Keyboard.GetState();
    bool down, up;

    // Поддержка клавиатуры
    down = (oldKeyboardState.IsKeyDown(Keys.Down) &&
           (keyboardState.IsKeyUp(Keys.Down)));
    up = (oldKeyboardState.IsKeyDown(Keys.Up) &&
         (keyboardState.IsKeyUp(Keys.Up)));

    // Поддержка крестовины пульта
    down |= (oldGamePadState.DPad.Down == ButtonState.Pressed) &&
            (gamepadState.DPad.Down == ButtonState.Released);
    up |= (oldGamePadState.DPad.Up == ButtonState.Pressed) &&
          (gamepadState.DPad.Up == ButtonState.Released);
    if (down || up)
    {
        audioComponent.PlayCue("menu_scroll");
    }
    if (down)
    {
        selectedIndex++;
        if (selectedIndex == menuItems.Count)
        {
            selectedIndex = 0;
        }
    }
    if (up)
    {
        selectedIndex--;
        if (selectedIndex == -1)
        {
            selectedIndex = menuItems.Count - 1;
        }
    }
    oldKeyboardState = keyboardState;
    oldGamePadState  = gamepadState;

    base.Update(gameTime);
}

И, наконец, в конструкторе класса вы должны инициализировать все эти вещи:

/// <summary>
/// Конструктор по умолчанию
/// </summary>
/// <param name="game">Главный объект игры</param>
/// <param name="normalFont">Шрифт для обычных элементов</param>
/// <param name="selectedFont">Шрифт для выбранных элементов</param>
public TextMenuComponent(Game game, SpriteFont normalFont,
                         SpriteFont selectedFont) : base(game)
{
    regularFont = normalFont;
    this.selectedFont = selectedFont;
    menuItems = new StringCollection();

    // Получаем текущий spritebatch
    spriteBatch = (SpriteBatch)
                   Game.Services.GetService(typeof (SpriteBatch));

    // Получаем текущий audiocomponent и воспроизводим фоновую музыку
    audioComponent = (AudioComponent)
    Game.Services.GetService(typeof (AudioComponent));

    // Используется для поддержки ввода
    oldKeyboardState = Keyboard.GetState();
    oldGamePadState = GamePad.GetState(PlayerIndex.One);
}

Еще о начальном экране

Добавим новый класс с именем StartScene, наследуемый от GameScene, подобно тому, что мы делали с HelpScene. В этой сцене у вас есть начальная анимация с двумя спрайтами (со словами Rock и Rain), меню, фоновая музыка и отдельный мерцающий на экране спрайт со словом enhanced. Начнем с добавления к классу StartScene следующих атрибутов:

// Разное
protected TextMenuComponent menu;
protected readonly Texture2D elements;

// Звук
protected AudioComponent audioComponent;
protected Cue backMusic;

// SpriteBatch
protected SpriteBatch spriteBatch = null;

// Графический интерфейс
protected Rectangle rockRect = new Rectangle(0, 0, 536, 131);
protected Vector2 rockPosition;
protected Rectangle rainRect = new Rectangle(120, 165, 517, 130);
protected Vector2 rainPosition;
protected Rectangle enhancedRect = new Rectangle(8, 304, 375, 144);
protected Vector2 enhancedPosition;
protected bool showEnhanced;
protected TimeSpan elapsedTime = TimeSpan.Zero;

Атрибуты rockRect, rainRect и enhancedRect ссылаются на прямоугольники исходной текстуры, содержащие слова Rock, Rain и enhanced. Атрибуты rockPosition, rainPosition и enhancedPosition содержат местоположения этих элементов на экране. Рисуйте эти изображения в выбранных позициях, но изменяйте позиции спрайтов со словами Rock и Rain, чтобы получить красивую начальную анимацию. Когда слова Rock и Rain займут предназначенные позиции, вы помещаете на экран мерцающее слово enhanced и показываете стартовое меню.

Все это делается в показанном ниже методе Update. Обратите внимание на вычисления для версии Xbox 360, предназначенные для поддержки экранов с соотношением сторон 16:9.

/// <summary>
/// Позволяет GameComponent обновлять себя
/// </summary>
/// <param name="gameTime">Предоставляет снимок значения таймера</param>
public override void Update(GameTime gameTime)
{
    if (!menu.Visible)
    {
        if (rainPosition.X >= (Game.Window.ClientBounds.Width - 595)/2)
        {
            rainPosition.X -= 15;
        }

        if (rockPosition.X <= (Game.Window.ClientBounds.Width - 715)/2)
        {
            rockPosition.X += 15;
        }
        else
        {
            menu.Visible = true;
            menu.Enabled = true;
            backMusic.Play();

#if XBOX360
            enhancedPosition = new Vector2((rainPosition.X +
                rainRect.Width - enhancedRect.Width / 2), rainPosition.Y);
#else
            enhancedPosition =
                new Vector2((rainPosition.X + rainRect.Width -
                enhancedRect.Width/2) - 80, rainPosition.Y);
#endif

            showEnhanced = true;
        }
    }
    else
    {
        elapsedTime += gameTime.ElapsedGameTime;
        if (elapsedTime > TimeSpan.FromSeconds(1))
        {
            elapsedTime -= TimeSpan.FromSeconds(1);
            showEnhanced = !showEnhanced;
        }
    }

    base.Update(gameTime);
}

Метод Draw рисует спрайты в их текущих позициях и рисует спрайт со словом enhanced, если спрайты со словами Rock и Rain уже заняли свои итоговые позиции (это контролируется атрибутом showEnhanced):

/// <summary>
/// Позволяет GameComponent рисовать себя
/// </summary>
/// <param name="gameTime">Предоставляет снимок значения таймера</param>
public override void Draw(GameTime gameTime)
{
    base.Draw(gameTime);

    spriteBatch.Draw(elements, rockPosition, rockRect, Color.White);
    spriteBatch.Draw(elements, rainPosition, rainRect, Color.White);

    if (showEnhanced)
    {
        spriteBatch.Draw(elements, enhancedPosition, enhancedRect,
                         Color.White);
    }
}

Теперь вам надо проделать дополнительную работу. Метод Show должен помещать эти спрайты в их начальные позиции и запускать звуковые эффекты. Метод Hide должен останавливать фоновую музыку, иначе эта музыка будет играть в другой сцене, не так ли? Код для этих методов такой:

/// <summary>
/// Показывает начальную сцену
/// </summary>
public override void Show()
{
    audioComponent.PlayCue("newmeteor");
    backMusic = audioComponent.GetCue("startmusic");

    rockPosition.X = -1*rockRect.Width;
    rockPosition.Y = 40;
    rainPosition.X = Game.Window.ClientBounds.Width;
    rainPosition.Y = 180;

    // Помещает меню в центр экрана
    menu.Position = new Vector2((Game.Window.ClientBounds.Width -
                    menu.Width)/2, 330);

    // Эти элементы видимы, когда сформирован заголовок Rock Rain
    menu.Visible = false;
    menu.Enabled = false;
    showEnhanced = false;

    base.Show();
}

/// <summary>
/// Скрываем начальную сцену
/// </summary>
public override void Hide()
{
    backMusic.Stop(AudioStopOptions.Immediate);

    base.Hide();
}

И в конструкторе вы должны все инициализировать, включая компонент Menu с соответствующими вариантами:

/// <summary>
/// Конструктор по умолчанию
/// </summary>
/// <param name="game">Главный объект игры</param>
/// <param name="smallFont">Шрифт для элементов меню</param>
/// <param name="largeFont">Шрифт для выбранных элементов меню</param>
/// <param name="background">Текстура фонового изображения</param>
/// <param name="elements">Текстура с элементами переднего плана</param>

public StartScene(Game game, SpriteFont smallFont, SpriteFont largeFont,
                  Texture2D background,Texture2D elements)
    : base(game)
{
    this.elements = elements;
    Components.Add(new ImageComponent(game, background,
                   ImageComponent.DrawMode.Center));

    // Создаем меню
    string[] items = {"One Player", "Two Players", "Help", "Quit"};
    menu = new TextMenuComponent(game, smallFont, largeFont);
    menu.SetMenuItems(items);
    Components.Add(menu);

    // Получаем текущий spritebatch
    spriteBatch = (SpriteBatch) Game.Services.GetService(
                                               typeof (SpriteBatch));

    // Получаем текущий audiocomponent и воспроизводим фоновую музыку
    audioComponent = (AudioComponent)
                    Game.Services.GetService(typeof(AudioComponent));
}

Теперь модифицируем код метода LoadContent в классе Game1 для загрузки содержимого, необходимого в этой сцене:

/// <summary>
/// LoadContent вызывается один раз за игру и
/// является местом для загрузки всего вашего содержимого
/// </summary>
protected override void LoadContent()
{
    // Создаем новый SpriteBatch,
    // который будет использоваться для рисования текстур
    spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
    Services.AddService(typeof (SpriteBatch), spriteBatch);

    // Создаем сцену помощи
    helpBackgroundTexture = Content.Load<Texture2D>("helpbackground");
    helpForegroundTexture = Content.Load<Texture2D>("helpForeground");
    helpScene = new HelpScene(this, helpBackgroundTexture,
                              helpForegroundTexture);
    Components.Add(helpScene);

    // Создаем начальную сцену
    smallFont = Content.Load<SpriteFont>("menuSmall");
    largeFont = Content.Load<SpriteFont>("menuLarge");
    startBackgroundTexture = Content.Load<Texture2D>("startbackground");
    startElementsTexture = Content.Load<Texture2D>("startSceneElements");
    startScene = new StartScene(this, smallFont$, largeFont,
                        startBackgroundTexture, startElementsTexture);
    Components.Add(startScene);
    startScene.Show();
    activeScene = startScene;
  }
}

Объявите следующие объекты в классе Game1, чтобы увидеть сцену в действии:

protected StartScene startScene;
protected Texture2D startBackgroundTexture, startElementsTexture;

// Шрифты
private SpriteFont smallFont, largeFont

Выполните программу, и вы должны увидеть что-то похожее на рис. 4.1.

Создание сцены игры

К этому моменту вы создали только начальную сцену и сцену помощи. Самая важная сцена отсутствует: сама сцена игры! Эта сцена выглядит почти так же, как первая версия Rock Rain, с некоторыми изменениями игровых правил и поддержкой двух игроков.

К тому же есть интересное изменение: использование анимированных спрайтов, в которых у вас есть анимация, составленная из множества кадров, отображаемых на экране в заданном порядке в течение определенного времени, что дает иллюзию движения. Компонент для анимированных спрайтов — это общий компонент в любой игре, так что начнем сцену с создания этого компонента.

Создание игрового компонента для анимированных спрайтов

Как было показано в главе 2, анимированные спрайты являются базовым ресурсом любой двухмерной игры. Они позволяют иметь в сцене актеров, которые будут больше, чем просто движущиеся изображения, и позволят вам получить иллюзию анимации, таким же образом, как это делается в мультфильмах. В случае Rock Rain вы используете анимированные спрайты для анимации ваших астероидов, чтобы теперь они вращались, перемещаясь по экрану. Итак, создайте класс с именем Sprite и используйте для этого GameComponent код из листинга 4.2. Этот код является просто усовершенствованной версией кода, показанного в главе 2. Поместите его в папку Core проекта.


Листинг 4.2. Игровой компонент Sprite


#region Инструкции Using
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
#endregion

namespace RockRainEnhanced.Core
{
    /// <summary>
    /// Это GameComponent, реализующий анимированные спрайты
    /// </summary>
    public class Sprite : DrawableGameComponent
    {
        private int activeFrame;
        private readonly Texture2D texture;
        private List<Rectangle> frames;

        protected Vector2 position;
        protected TimeSpan elapsedTime = TimeSpan.Zero;
        protected Rectangle currentFrame;
        protected long frameDelay;
        protected SpriteBatch sbBatch;

        /// <summary>
        /// Конструктор по умолчанию
        /// </summary>
        /// <param name="game">Объект игры</param>
        /// <param name="theTexture">Текстура, содержащая кадры спрайта</param>
        public Sprite(Game game, ref Texture2D theTexture)
            : base(game)
        {
            texture = theTexture;
            activeFrame = 0;
        }

        /// <summary>
        /// Список с кадрами анимации
        /// </summary>
        public List<Rectangle> Frames
        {
            get { return frames; }
            set { frames = value; }
        }

        /// <summary>
        /// Позволяет GameComponent выполнять любую инициализацию,
        /// необходимую перед запуском. Здесь выполняется запрос любых
        /// необходимых сервисов и загрузка содержимого
        /// </summary>
        public override void Initialize()
        {
            // Получаем текущий spritebatch
            sbBatch =
                (SpriteBatch) Game.Services.GetService(typeof (SpriteBatch));
            base.Initialize();
        }

        /// <summary>
        /// Позволяет GameComponent обновлять себя
        /// </summary>
        /// <param name="gameTime">Предоставляет снимок значения таймера</param>
        public override void Update(GameTime gameTime)
        {
            elapsedTime += gameTime.ElapsedGameTime;

            // Пришло время для следующего кадра?
            if (elapsedTime > TimeSpan.FromMilliseconds(frameDelay))
            {
                elapsedTime -= TimeSpan.FromMilliseconds(frameDelay);
                activeFrame++;
                if (activeFrame == frames.Count)
                {
                    activeFrame = 0;
                }

                // Получаем текущий кадр
                currentFrame = frames[activeFrame];
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// Рисование спрайта
        /// </summary>
        /// <param name="gameTime">Предоставляет снимок значения таймера</param>
        public override void Draw(GameTime gameTime)
        {
            sbBatch.Draw(texture, position, currentFrame, Color.White);

            base.Draw(gameTime);
        }
    }
}

Метод Update меняет текущий кадр каждые n миллисекунд, чтобы создать иллюзию анимации, а метод Draw рисует текущий кадр в текущем местоположении на экране. Теперь используем этот класс для создания анимированных спрайтов астероидов. Создайте класс с именем Meteor и используйте код из листинга 4.3.


Листинг 4.3. Игровой компонент Meteor


#region Инструкции Using
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using RockRainEnhanced.Core;
#endregion

namespace RockRainEnhanced
{
    /// <summary>
    /// Это класс анимированного спрайта для астероида
    /// </summary>
    public class Meteor : Sprite
    {
        // Вертикальная скорость
        protected int Yspeed;

        // Горизонтальная скорость
        protected int Xspeed;

        protected Random random;

        // Уникальный идентификатор астероида
        private int index;

        public Meteor(Game game, ref Texture2D theTexture) :
            base(game, ref theTexture)
        {
            Frames = new List<Rectangle>();
            Rectangle frame = new Rectangle();

            frame.X = 468;
            frame.Y = 0;
            frame.Width = 49;
            frame.Height = 44;
            Frames.Add(frame);

            frame.Y = 50;
            Frames.Add(frame);

            frame.Y = 98;
            frame.Height = 45;
            Frames.Add(frame);

            frame.Y = 146;
            frame.Height = 49;
            Frames.Add(frame);

            frame.Y = 200;
            frame.Height = 44;
            Frames.Add(frame);

            frame.Y = 250;
            Frames.Add(frame);

            frame.Y = 299;
            Frames.Add(frame);

            frame.Y = 350;
            frame.Height = 49;
            Frames.Add(frame);

            // Инициализация генератора случайных чисел
            // и помещение астероида в исходную позицию
            random = new Random(GetHashCode());
            PutinStartPosition();
        }

        /// <summary>
        /// Инициализация местоположения и скорости астероида
        /// </summary>
        public void PutinStartPosition()
        {
            position.X = random.Next(Game.Window.ClientBounds.Width -
                                     currentFrame.Width);
            position.Y = 0;
            YSpeed = 1 + random.Next(9);
            XSpeed = random.Next(3) - 1;
        }

        /// <summary>
        /// Обновление местоположения астероида
        /// </summary>
        public override void Update(GameTime gameTime)
        {
            // Проверяем, видим ли астероид
            if ((position.Y >= Game.Window.ClientBounds.Height) ||
                (position.X >= Game.Window.ClientBounds.Width) ||
                (position.X <= 0))
            {
                PutinStartPosition();
            }

            // Перемещение астероида
            position.Y += Yspeed;
            position.X += Xspeed;

            base.Update(gameTime);
        }

        /// <summary>
        /// Вертикальная скорость
        /// </summary>
        public int YSpeed
        {
            get { return Yspeed; }
            set
            {
                Yspeed = value;
                frameDelay = 200 - (Yspeed * 5);
            }
        }

        /// <summary>
        /// Горизонтальная скорость
        /// </summary>
        public int XSpeed
        {
            get { return Xspeed; }
            set { Xspeed = value; }
        }

        /// <summary>
        /// Идентификатор астероида
        /// </summary>
        public int Index
        {
            get { return index; }
            set { index = value; }
        }

        /// <summary>
        /// Проверка, пересекается ли астероид с указанным
        /// прямоугольником 
        /// </summary>
        /// <param name="rect">Проверяемый прямоугольник</param>
        /// <returns>true, если произошло столкновение</returns>
        public bool CheckCollision(Rectangle rect)
        {
            Rectangle spriterect =new Rectangle(
                                        (int) position.X, (int) position.Y,
                                        currentFrame.Width, currentFrame.Height);
            return spriterect.Intersects(rect);
        }
    }
}

Класс похож на первую версию из предыдущей главы: только в конструкторе находится код, который добавляет кадры анимации. Все остальное следует той же логике, что и раньше. Верно, астероиды продолжают «падать», но теперь они не похожи на статические изображения, вместо этого у нас есть анимация, благодаря которой они выглядят вращающимися. Круто, не так ли?

Вы также добавили свойство Index для получения уникального идентификатора каждого астероида в игре, чтобы иметь возможность получить когда потребуется конкретный астероид (вы используете эту возможность в следующей версии Rock Rain).

Давайте создадим еще один GameComponent, на этот раз только с целью улучшения проекта, который централизует всю обработку астероидов. Этот класс будет ответственным за рисование и обновление всех астероидов в игре, а также выполнять проверку столкновений и добавлять по прошествии времени новые астероиды. Преимущество наличия объекта, управляющего другими объектами, заключается в том, что проект игры становится проще и в то же время эффективнее. Например, вам не надо перебирать все GameComponent, чтобы проверить столкновения, как это делалось в версии из предыдущей главы, а только пройтись по GameComponent переданным под управление этого диспетчера, который управляет астероидами. Этот способ позволяет немного увеличить производительность.

Добавьте класс с именем MeteorsManager и поместите в него код из листинга 4.4.


Листинг 4.4. Игровой компонент MeteorsManager


#region Инструкции Using
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using RockRainEnhanced.Core;
#endregion

namespace RockRainEnhanced
{
    /// <summary>
    /// GameComponent, реализующий диспетчер всех астероидов в игре
    /// </summary>
    public class MeteorsManager : DrawableGameComponent
    {
        // Список активных астероидов
        protected List<Meteor> meteors;

        // Константа начального количества астероидов
        private const int STARTMETEORCOUNT = 10;

        // Время до появления нового астероида
        private const int ADDMETEORTIME = 5000;
        protected Texture2D meteorTexture;
        protected TimeSpan elapsedTime = TimeSpan.Zero;
        protected AudioComponent audioComponent;

        public MeteorsManager(Game game, ref Texture2D theTexture)
            : base(game)
        {
            meteorTexture = theTexture;
            meteors = new List<Meteor>();
        }

        /// <summary>
        /// Позволяет GameComponent выполнять любую инициализацию,
        /// необходимую перед запуском. Здесь выполняется запрос любых
        /// необходимых сервисов и загрузка содержимого
        /// </summary>
        public override void Initialize()
        {
            audioComponent = (AudioComponent)
                   Game.Services.GetService(typeof (AudioComponent));

            meteors.Clear();
            Start();

            for (int i = 0; i < meteors.Count; i++)
            {
                meteors[i].Initialize();
            }

            base.Initialize();
        }

        /// <summary>
        /// Запуск метеоритного дождя
        /// </summary>
        public void Start()
        {
            // Инициализация счетчика
            elapsedTime = TimeSpan.Zero;

            // Добавление астероида
            for (int i = 0; i < STARTMETEORCOUNT; i++)
            {
                AddNewMeteor();
            }
        }

        /// <summary>
        /// Все астероиды игры
        /// </summary>
        public List<Meteor> AllMeteors
        {
            get { return meteors; }
        }

        /// <summary>
        /// Проверяем, не пора ли добавлять новый астероид
        /// </summary>
        private void CheckforNewMeteor(GameTime gameTime)
        {
            // Добавляем камень каждые ADDMETEORTIME
            elapsedTime += gameTime.ElapsedGameTime;
            if (elapsedTime > TimeSpan.FromMilliseconds(ADDMETEORTIME))
            {
                elapsedTime -= TimeSpan.FromMilliseconds(ADDMETEORTIME);
                AddNewMeteor();

                // Воспроизводим звук для нового астероида
                audioComponent.PlayCue("newmeteor");
            }
        }

        /// <summary>
        /// Добавляем новый астероид к сцене
        /// </summary>
        private void AddNewMeteor()
        {
            Meteor newMeteor = new Meteor(Game, ref meteorTexture);
            newMeteor.Initialize();
            meteors.Add(newMeteor);

            // Устанавливаем идентификатор астероида
            newMeteor.Index = meteors.Count - 1;
        }

        /// <summary>
        /// Позволяет GameComponent обновлять себя
        /// </summary>
        /// <param name="gameTime">Предоставляет снимок значения таймера</param>
        public override void Update(GameTime gameTime)
        {
            CheckforNewMeteor(gameTime);

            // Обновление астероидов
            for (int i = 0; i < meteors.Count; i++)
            {
                meteors[i].Update(gameTime);
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// Проверяем, столкнулся ли корабль с астероидом
        /// </summary>
        /// <returns>true, если произошло столкновение</returns>
        public bool CheckForCollisions(Rectangle rect)
        {
            for (int i = 0; i < meteors.Count; i++)
            {
                if (meteors[i].CheckCollision(rect))
                {
                    // БУМ !!
                    audioComponent.PlayCue("explosion");

                    // Возвращаем астероид в исходную позицию
                    meteors[i].PutinStartPosition();
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Позволяет GameComponent рисовать свое содержимое на экране
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            // Рисуем астероиды
            for (int i = 0; i < meteors.Count; i++)
            {
                meteors[i].Draw(gameTime);
            }

            base.Draw(gameTime);
        }
    }
}

Обратите внимание, что этот класс содержит значительную часть кода, который раньше находился внутри класса Game1 из предыдущей главы, и по существу делает те же самые вещи. Вы используете этот класс позже для составления сцены игры.

ПРИМЕЧАНИЕ
Обычно хорошей идеей будет создание управляющего класса для каждой группы GameComponent в игре. Нормально видеть такие классы, как EnemyManager, WizardManager и т.д., поскольку они помещают все сложности, связанные с конкретным типом игровых элементов, в единственный класс. Это упрощает код и увеличивает возможности повторного использования этих компонентов в других играх.

Создание табло со счетом

Вам надо создать еще один элемент сцены игры: табло со счетом. Это табло со счетом показывает количество набранных очков и энергию космического корабля игрока. Этот класс простой: он только рисует две строки текста на экране. Добавьте к проекту класс с именем Score и поместите в него код из листинга 4.5.


Листинг 4.5. Игровой компонент Score


#region Инструкции Using
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
#endregion

namespace RockRainEnhanced
{
    /// <summary>
    /// GameComponent, реализующий счет игры
    /// </summary>
    public class Score : DrawableGameComponent
    {
        // Spritebatch
        protected SpriteBatch spriteBatch = null;

        // Местоположение счета
        protected Vector2 position = new Vector2();

        // Значения
        protected int value;
        protected int power;
        protected readonly SpriteFont font;
        protected readonly Color fontColor;

        public Score(Game game, SpriteFont font, Color fontColor)
            : base(game)
        {
            this.font = font;
            this.fontColor = fontColor;

            // Получаем текущий spritebatch
            spriteBatch = (SpriteBatch)
                    Game.Services.GetService(typeof (SpriteBatch));
        }

        /// <summary>
        /// Очки
        /// </summary>
        public int Value
        {
            get { return value; }
            set { this.value = value; }
        }

        /// <summary>
        /// Энергия
        /// </summary>
        public int Power
        {
            get { return power; }
            set { power = value; }
        }

        /// <summary>
        /// Местоположение компонента на экране
        /// </summary>
        public Vector2 Position
        {
            get { return position; }
            set { position = value; }
        }

        /// <summary>
        /// Позволяет GameComponent рисовать себя
        /// </summary>
        /// <param name="gameTime">Предоставляет снимок значения таймера</param>
        public override void Draw(GameTime gameTime)
        {
            string TextToDraw = string.Format("Score: {0}", value);

            // Рисуем тень текста
            spriteBatch.DrawString(font, TextToDraw,
                                   new Vector2(position.X + 1, position.Y + 1),
                                   Color.Black);

            // Рисуем текст
            spriteBatch.DrawString(font, TextToDraw,
                                   new Vector2(position.X, position.Y),
                                   fontColor);

            float height = font.MeasureString(TextToDraw).Y;
            TextToDraw = string.Format("Power: {0}", power);

            // Рисуем тень текста
            spriteBatch.DrawString(font, TextToDraw,
                                   new Vector2(position.X + 1,
                                               position.Y + 1 + height),
                                   Color.Black);
            // Рисуем текст
            spriteBatch.DrawString(font, TextToDraw,
                                   new Vector2(position.X,
                                               position.Y + 1 + height),
                                   fontColor);

            base.Draw(gameTime);
        }
    }
}

И снова все это выглядит похоже на код из предыдущей версии, только в этот раз все заключено в класс и текст рисуется с небольшой тенью под ним, для улучшения четкости и придания стиля, как вы поступали с компонентом Menu.

Создание источников энергии

Изменения процесса игры Rock Rain принесли необходимость интересных дополнительных компонентов. Корабль игрока теперь содержит конечный источник энергии, уменьшающийся со временем и резко сокращающийся после столкновения с астероидом. Вы должны предоставить игрокам возможность дозаправлять их корабли, чтобы они могли дольше оставаться в игре, накапливая больше очков.

Вы создадите новый GameComponent, который выглядит как небольшая канистра топлива, появляющаяся через регулярные интервалы и «падающая» вместе с астероидами. Если игрок касается ее, его корабль дозаправляется энергией. Идея заключается в том, что игрок внимательно следит за этим новым элементом и пытается получить его, не столкнувшись с каким-нибудь астероидом.

Добавьте новый класс с именем PowerSource и поместите в него код из листинга 4.6.


Листинг 4.6. Игровой компонент PowerSource


#region Инструкции Using
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using RockRainEnhanced.Core;
#endregion

namespace RockRainEnhanced
{
    /// <summary>
    /// GameComponent, реализующий источник энергии
    /// </summary>
    public class PowerSource : Sprite
    {
        protected Texture2D texture;
        protected Random random;

        public PowerSource(Game game, ref Texture2D theTexture)
            : base(game, ref theTexture)
        {
            texture = theTexture;
            Frames = new List<Rectangle>();
            Rectangle frame = new Rectangle();

            frame.X = 291;
            frame.Y = 17;
            frame.Width = 14;
            frame.Height = 12;
            Frames.Add(frame);

            frame.Y = 30;
            Frames.Add(frame);

            frame.Y = 43;
            Frames.Add(frame);

            frame.Y = 57;
            Frames.Add(frame);

            frame.Y = 70;
            Frames.Add(frame);

            frame.Y = 82;
            Frames.Add(frame);

            frameDelay = 200;

            // Инициализация генератора случайных чисел
            // и помещение источника энергии в исходную позицию
            random = new Random(GetHashCode());
            PutinStartPosition();
        }

        /// <summary>
        /// Инициализация местоположения и скорости
        /// </summary>
        public void PutinStartPosition()
        {
            position.X = random.Next(Game.Window.ClientBounds.Width -
                                     currentFrame.Width);
            position.Y = -10;
            Enabled = false;
        }

        public override void Update(GameTime gameTime)
        {
            // Проверяем, видим ли источник энергии
            if (position.Y >= Game.Window.ClientBounds.Height)
            {
                position.Y = 0;
                Enabled = false;
            }

            // Перемещение
            position.Y += 1;

            base.Update(gameTime);
        }

        /// <summary>
        /// Проверяем, пересекается ли объект с указанным прямоугольником
        /// </summary>
        /// <param name="rect">Проверяемый прямоугольник</param>
        /// <returns>true, если произошло столкновение</returns>
        public bool CheckCollision(Rectangle rect)
        {
            Rectangle spriterect =
                     new Rectangle((int) position.X, (int) position.Y,
                                   currentFrame.Width, currentFrame.Height);
            return spriterect.Intersects(rect);
        }
    }
}

Вы делаете то же самое, что и в классе Meteor, создавая анимацию со списком кадров и обновляя местоположение по вертикали по мере прохождения времени, чтобы создать эффект «падения».

Создание игрового компонента игрока

Мы почти закончили, но главный актер сцены игры все еще отсутствует: игрок! В новой версии код для GameComponent игрока практически тот же, что и в предыдущей главе, но только с добавлением поддержки многопользовательской игры. Эта поддержка отличается от предыдущей версии в основном обработкой энергии, клавиатуры, очков и способом рисования игрока. Код класса Player приведен в листинге 4.7.


Листинг 4.6. Игровой компонент Player


#region Инструкции Using
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
#endregion

namespace RockRainEnhanced
{
    /// <summary>
    /// Это GameComponent реализующий корабль игрока
    /// </summary>
    public class Player : DrawableGameComponent
    {
        protected Texture2D texture;
        protected Rectangle spriteRectangle;
        protected Vector2 position;
        protected TimeSpan elapsedTime = TimeSpan.Zero;
        protected PlayerIndex playerIndex;

        // Область экрана
        protected Rectangle screenBounds;

        // Игровые ресурсы
        protected int score;
        protected int power;
        private const int INITIALPOWER = 100;

        public Player(Game game, ref Texture2D theTexture, PlayerIndex playerID,
                      Rectangle rectangle) : base(game)
        {
            texture = theTexture;
            position = new Vector2();
            playerIndex = playerID;

            // Создаем прямоугольник источника. Он представляет
            // местоположение картинки спрайта на поверхности 
            spriteRectangle = rectangle;
#if XBOX360
            // На Xbox 360 нам надо беспокоиться о
            // "безопасной" области телевизионного экрана
            screenBounds = new Rectangle((int)(Game.Window.ClientBounds.Width *
                         0.03f),(int)(Game.Window.ClientBounds.Height * 0.03f),
                         Game.Window.ClientBounds.Width -
                         (int)(Game.Window.ClientBounds.Width * 0.03f),
                         Game.Window.ClientBounds.Height -
                         (int)(Game.Window.ClientBounds.Height * 0.03f));
#else
            screenBounds = new Rectangle(0, 0, Game.Window.ClientBounds.Width,
                                         Game.Window.ClientBounds.Height);
#endif
        }

        /// <summary>
        /// Помещаем корабль в исходную позицию на экране
        /// </summary>
        public void Reset()
        {
            if (playerIndex == PlayerIndex.One)
            {
                position.X = screenBounds.Width/3;
            }
            else
            {
                position.X = (int) (screenBounds.Width/1.5);
            }
            position.Y = screenBounds.Height - spriteRectangle.Height;
            score = 0;
            power = INITIALPOWER;
        }

        /// <summary>
        /// Общий счет игрока
        /// </summary>
        public int Score
        {
            get { return score; }
            set
            {
                if (value < 0)
                {
                    score = 0;
                }
                else
                {
                    score = value;
                }
            }
        }

        /// <summary>
        /// Оставшаяся энергия
        /// </summary>
        public int Power
        {
            get { return power; }
            set { power = value; }
        }

        /// <summary>
        /// Обновляем местоположение игрока, счет и энергию
        /// </summary>
        public override void Update(GameTime gameTime)
        {
            // Перемещаем корабль с пульта Xbox
            GamePadState gamepadstatus = GamePad.GetState(playerIndex);
            position.Y += (int) ((gamepadstatus.ThumbSticks.Left.Y*3)*-2);
            position.X += (int) ((gamepadstatus.ThumbSticks.Left.X*3)*2);

            // Перемещаем корабль с клавиатуры
            if (playerIndex == PlayerIndex.One)
            {
                HandlePlayer1KeyBoard();
            }
            else
            {
                HandlePlayer2KeyBoard();
            }

            // Сохраняем игрока в пределах экрана
            KeepInBound();

            // Обновляем счет
            elapsedTime += gameTime.ElapsedGameTime;
            if (elapsedTime > TimeSpan.FromSeconds(1))
            {
                elapsedTime -= TimeSpan.FromSeconds(1);
                score++;
                power--;
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// Сохраняем корабль внутри экрана
        /// </summary>
        private void KeepInBound()
        {
            if (position.X < screenBounds.Left)
            {
                position.X = screenBounds.Left;
            }
            if (position.X > screenBounds.Width - spriteRectangle.Width)
            {
                position.X = screenBounds.Width - spriteRectangle.Width;
            }
            if (position.Y < screenBounds.Top)
            {
                position.Y = screenBounds.Top;
            }
            if (position.Y > screenBounds.Height - spriteRectangle.Height)
            {
                position.Y = screenBounds.Height - spriteRectangle.Height;
            }
        }

        /// <summary>
        /// Обрабатываем клавиши для игрока 1 (клавиши курсора)
        /// </summary>
        private void HandlePlayer1KeyBoard()
        {
            KeyboardState keyboard = Keyboard.GetState();

            if (keyboard.IsKeyDown(Keys.Up))
            {
                position.Y -= 3;
            }
            if (keyboard.IsKeyDown(Keys.Down))
            {
                position.Y += 3;
            }
            if (keyboard.IsKeyDown(Keys.Left))
            {
                position.X -= 3;
            }
            if (keyboard.IsKeyDown(Keys.Right))
            {
                position.X += 3;
            }
        }

        /// <summary>
        /// Обрабатываем клавиши для игрока 2 (ASDW)
        /// </summary>
        private void HandlePlayer2KeyBoard()
        {
            KeyboardState keyboard = Keyboard.GetState();

            if (keyboard.IsKeyDown(Keys.W))
            {
                position.Y -= 3;
            }
            if (keyboard.IsKeyDown(Keys.S))
            {
                position.Y += 3;
            }
            if (keyboard.IsKeyDown(Keys.A))
            {
                position.X -= 3;
            }
            if (keyboard.IsKeyDown(Keys.D))
            {
                position.X += 3;
            }
        }

        /// <summary>
        /// Рисуем спрайт корабля
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            // Получаем текущий spritebatch
            SpriteBatch sBatch = (SpriteBatch)
            Game.Services.GetService(typeof (SpriteBatch));

            // Рисуем корабль
            sBatch.Draw(texture, position, spriteRectangle, Color.White);

            base.Draw(gameTime);
        }

        /// <summary>
        /// Получаем ограничивающий прямоугольник
        /// для позиции корабля на экране
        /// </summary>
        public Rectangle GetBounds()
        {
            return new Rectangle((int) position.X, (int) position.Y,
                                 spriteRectangle.Width, spriteRectangle.Height);
        }
    }
}

Как видите, это практически тот же класс, что и в предыдущей главе, но в методе Update вы слегка по-другому обрабатываете пользовательский ввод, проверяя значение PlayerIndex, чтобы обратиться к правильному игровому пульту или клавишам клавиатуры. В многопользовательской игре вы создаете два экземпляра объектов данного класса с различными значениями PlayerIndex и различными прямоугольниками в текстуре для разных спрайтов космических кораблей.

Объединяем все вместе

Теперь у вас есть все компоненты сцены игры. Астероиды, счет и игрок (или игроки) готовы приступить к работе. Сейчас мы добавим класс с именем ActionScene. Эта сцена является самой сложной сценой в игре. Она координирует действия всех компонентов, а также управляет состояниями игры, такими как пауза и завершение игры.

Начнем со следующего объявления всех элементов сцены:

// Основы
protected Texture2D actionTexture;
protected Cue backMusic;
protected SpriteBatch spriteBatch = null;

// Игровые элементы
protected Player player1;
protected Player player2;
protected MeteorsManager meteors;
protected PowerSource powerSource;
protected SimpleRumblePad rumblePad;
protected ImageComponent background;
protected Score scorePlayer1;
protected Score scorePlayer2;

// Ресурсы GUI
protected Vector2 pausePosition;
protected Vector2 gameoverPosition;
protected Rectangle pauseRect = new Rectangle(1, 120, 200, 44);
protected Rectangle gameoverRect = new Rectangle(1, 170, 350, 48);

// Элементы GameState
protected bool paused;
protected bool gameOver;
protected TimeSpan elapsedTime = TimeSpan.Zero;
protected bool twoPlayers;

Это выглядит похоже на атрибуты игры из предыдущей главы, но теперь у нас есть два экземпляра Player (для многопользовательской игры), два атрибута для управления состоянием игры (paused и gameOver), компоненты для Score, PowerSource, Meteors и т.д.

Конструктор инициализирует все эти объекты следующим образом:

/// <summary>
/// Конструктор по умолчанию
/// </summary>
/// <param name="game">Главный объект игры</param>
/// <param name="theTexture">Текстура со спрайтами</param>
/// <param name="backgroundTexture">Текстура для фона</param>
/// <param name="font">Шрифт, используемый для счета</param>
public ActionScene(Game game, Texture2D theTexture,
                   Texture2D backgroundTexture, SpriteFont font) : base(game)
{
    // Получаем текущий звуковой компонент и воспроизводим фоновую музыку
    audioComponent = (AudioComponent)
    Game.Services.GetService(typeof (AudioComponent));

    background = new ImageComponent(game, backgroundTexture,
                                    ImageComponent.DrawMode.Stretch);
    Components.Add(background);

    actionTexture = theTexture;
    spriteBatch = (SpriteBatch)
             Game.Services.GetService(typeof (SpriteBatch));

    meteors = new MeteorsManager(Game, ref actionTexture);
    Components.Add(meteors);

    player1 = new Player(Game, ref actionTexture, PlayerIndex.One,
                         new Rectangle(323, 15, 30, 30));
    player1.Initialize();
    Components.Add(player1);

    player2 = new Player(Game, ref actionTexture, PlayerIndex.Two,
                         new Rectangle(360, 17, 30, 30));
    player2.Initialize();
    Components.Add(player2);

    scorePlayer1 = new Score(game, font, Color.Blue);
    scorePlayer1.Position = new Vector2(10, 10);
    Components.Add(scorePlayer1);

    scorePlayer2 = new Score(game, font, Color.Red);
    scorePlayer2.Position = new Vector2(
                  Game.Window.ClientBounds.Width - 200, 10);
    Components.Add(scorePlayer2);

    rumblePad = new SimpleRumblePad(game);
    Components.Add(rumblePad);

    powerSource = new PowerSource(game, ref actionTexture);
    powerSource.Initialize();
    Components.Add(powerSource);
}

Посмотрите, как вы создаете два экземпляра класса Player. Для каждого игрока просто меняется PlayerIndex и Rectangle, задающий изображение корабля в текстуре.

Вам также необходимо управлять состояниями игры и определять для одного или для двух игроков запущена игра, а так же проверять не погиб ли какой-нибудь из игроков. Добавьте к классу следующие свойства:

/// <summary>
/// Режим игры для 2 игроков
/// </summary>
public bool TwoPlayers
{
    get { return twoPlayers; }
    set { twoPlayers = value; }
}

/// <summary>
/// True, если игра в состоянии gameOver
/// </summary>
public bool GameOver
{
    get { return gameOver; }
}

/// <summary>
/// Режим паузы
/// </summary>
public bool Paused
{
    get { return paused; }
    set
    {
        paused = value;
        if (paused)
        {
            backMusic.Pause();
        }
        else
        {
            backMusic.Resume();
        }
    }
}

Подобно всем другим сценам, вы можете использовать методы Show и Hide для инициализации и освобождения компонентов сцены. В методе Show вы запускаете воспроизведение фоновой музыки и устанавливаете состояние player2, если у вас игра для двух игроков:

/// <summary>
/// Показываем сцену игры
/// </summary>
public override void Show()
{
    backMusic = audioComponent.GetCue("backmusic");
    backMusic.Play();

    meteors.Initialize();
    powerSource.PutinStartPosition();

    player1.Reset();
    player2.Reset();

    paused = false;
    pausePosition.X = (Game.Window.ClientBounds.Width -
                       pauseRect.Width)/2;
    pausePosition.Y = (Game.Window.ClientBounds.Height -
                       pauseRect.Height)/2;

    gameOver = false;
    gameoverPosition.X = (Game.Window.ClientBounds.Width -
                          gameoverRect.Width)/2;
    gameoverPosition.Y = (Game.Window.ClientBounds.Height -
                          gameoverRect.Height)/2;

    // Игра для двух игроков?
    player2.Visible = twoPlayers;
    player2.Enabled = twoPlayers;
    scorePlayer2.Visible = twoPlayers;
    scorePlayer2.Enabled = twoPlayers;

    base.Show();
}

/// <summary>
/// Скрываем сцену
/// </summary>
public override void Hide()
{
    // Останавливаем фоновую музыку
    backMusic.Stop(AudioStopOptions.Immediate);

    // Останавливаем вибрацию
    rumblePad.Stop(PlayerIndex.One);
    rumblePad.Stop(PlayerIndex.Two);

    base.Hide();
}

И, как всегда, метод Update синхронизирует все эти объекты, проверяя столкновения и меняя состояние игры на завершение, когда игроки погибают.

/// <summary>
/// Позволяет GameComponent обновлять себя
/// </summary>
/// <param name="gameTime">Предоставляет снимок значения таймера</param>
public override void Update(GameTime gameTime)
{
    if ((!paused) && (!gameOver))
    {
        // Проверяем столкновения с астероидами
        HandleDamages();

        // Проверяем подбор дополнительной энергии
        HandlePowerSourceSprite(gameTime);

        // Обновление счета
        scorePlayer1.Value = player1.Score;
        scorePlayer1.Power = player1.Power;
        if (twoPlayers)
        {
            scorePlayer2.Value = player2.Score;
            scorePlayer2.Power = player2.Power;
        }

        // Проверяем гибель игрока
        gameOver = ((player1.Power <= 0) || (player2.Power <= 0));
        if (gameOver)
        {
            player1.Visible = (player1.Power > 0);
            player2.Visible = (player2.Power > 0) && twoPlayers;

            // Останавливаем музыку
            backMusic.Stop(AudioStopOptions.Immediate);

            // Останавливаем вибрацию
            rumblePad.Stop(PlayerIndex.One);
            rumblePad.Stop(PlayerIndex.Two);
        }

        // Обновляем все другие GameComponents
        base.Update(gameTime);
    }

    // В состоянии gameOver сохраняем анимацию астероидов
    if (gameOver)
    {
        meteors.Update(gameTime);
    }
}

Методы HandleDamages и HandlePowerSourceSprite проверяют столкновения с астероидами (и уменьшают энергию игрока), проверяют столкновения с источником энергии (и добавляют игроку энергию), и проверяют, что энергия игрока меньше или равна нулю для завершения игры и перевода его в состояние окончания игры.

Метод HandleDamages также похож на метод проверки столкновений из предыдущей главы. Снова этот метод проверяет столкновения игрока с астероидами и одного игрока с другим. При каждом столкновении игрок теряет десять очков и десять единиц энергии:

/// <summary>
/// Обработка столкновений с астероидами
/// </summary>
private void HandleDamages()
{
    // Проверка столкновений для игрока 1
    if (meteors.CheckForCollisions(player1.GetBounds()))
    {
        // Взболтать!
        rumblePad.RumblePad(PlayerIndex.One, 500, 1.0f, 1.0f);

        // Штраф игрока
        player1.Power -= 10;
        player1.Score -= 10;
    }

    // Проверка столкновений для игрока 2
    if (twoPlayers)
    {
        if (meteors.CheckForCollisions(player2.GetBounds()))
        {
            // Взболтать!
            rumblePad.RumblePad(PlayerIndex.Two, 500, 1.0f, 1.0f);

            // Штраф игрока
            player2.Power -= 10;
            player2.Score -= 10;
        }

        // Проверка столкновений между игроками
        if (player1.GetBounds().Intersects(player2.GetBounds()))
        {
            rumblePad.RumblePad(PlayerIndex.One, 500, 1.0f, 1.0f);
            player1.Power -= 10;
            player1.Score -= 10;
            rumblePad.RumblePad(PlayerIndex.Two, 500, 1.0f, 1.0f);
            player2.Power -= 10;
            player2.Score -= 10;
        }
    }
}

Метод HandlePowerSourceSprite делает ту же самую работу, но для спрайта источника энергии. Если какой-нибудь игрок сталкивается с этим спрайтом, он получает 50 единиц энергии. Метод также проверяет, пришло ли время появиться в игре новому источнику энергии, используя интервал в 15 секунд.

/// <summary>
/// Обработка источника энергии
/// </summary>
private void HandlePowerSourceSprite(GameTime gameTime)
{
    if (powerSource.CheckCollision(player1.GetBounds()))
    {
        // Игрок 1 получил источник энергии
        audioComponent.PlayCue("powerget");
        elapsedTime = TimeSpan.Zero;
        powerSource.PutinStartPosition();
        player1.Power += 50;
    }
    if (twoPlayers)
    {
        // Игрок 2 получил источник энергии
        if (powerSource.CheckCollision(player2.GetBounds()))
        {
            audioComponent.PlayCue("powerget");
            elapsedTime = TimeSpan.Zero;
            powerSource.PutinStartPosition();
            player2.Power += 50;
        }
    }

    // Проверка для отправки нового источника энергии
    elapsedTime += gameTime.ElapsedGameTime;
    if (elapsedTime > TimeSpan.FromSeconds(15))
    {
        elapsedTime -= TimeSpan.FromSeconds(15);
        powerSource.Enabled = true;
    }
}

И, наконец, метод Draw просто рисует объекты для заданного состояния игры:

/// <summary>
/// Позволяет GameComponent рисовать себя
/// </summary>
/// <param name="gameTime">Предоставляет снимок значения таймера</param>
public override void Draw(GameTime gameTime)
{
    // Рисуем все GameComponent
    base.Draw(gameTime);

    if (paused)
    {
        // Рисуем текст "pause"
        spriteBatch.Draw(actionTexture, pausePosition, pauseRect,
                         Color.White);
    }
    if (gameOver)
    {
        // Рисуем текст "game over"
        spriteBatch.Draw(actionTexture, gameoverPosition, gameoverRect,
                         Color.White);
}

Заметьте, что здесь снова сохранилась большая часть игровой логики, созданной вами в предыдущей главе. Вы только добавили поддержку для двух игроков и два дополнительных состояния игры: одно, когда игрок приостанавливает игру (нажимая во время игры клавишу Enter или кнопку A на игровом пульте Xbox 360), и другое, когда у игроков заканчивается энергия. Когда это происходит, игра выводит на экран сообщение и ждет, пока игрок не нажмет клавишу Enter или кнопку A на игровом пульте Xbox 360.


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz