| netlib.narod.ru | < Назад | Оглавление | Далее > |
Во всех современных играх есть много экранов: начальный экран, экран для инструкций, экран для самой игры и т.д. Поскольку на каждом экране показывается гораздо больше, чем простое изображение, в игровой индустрии эти экраны обычно называют сценами.
Сцена состоит (обычно) из некоего фонового изображения, фоновой музыки и группы «актеров», которые «играют» на сцене, показывая пользователю некую информацию об игре.
Для примера взгляните на начальный экран Rock Rain Enhanced, показанный на рис. 4.1.

Рис. 4.1. Начальный экран
В этой сцене у вас есть красивый фон экрана, два слова, выходящие из-за краев экрана и образующе фразу «Rock Rain», а также меню с командами для игры и фоновой музыкой.
Обратите внимание, что в этой сцене есть несколько «актеров». Помимо спрайтов, размещенных в форме названия игры, у вас есть анимированное меню, по которому можно перемещаться с помощью игрового пульта Xbox 360 или клавиатуры. Эта группа изображений, звуков и актеров формирует сцену. Пользователь может перейти к другой сцене, согласно командам меню. В этой версии Rock Rain у вас три сцены: начальная сцена, сцена помощи и сцена игры. Рис. 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. Изображения, являющиеся частями сцены помощи
Также заметьте усовершенствования, внесенные в обработку ввода. Вы всегда сравниваете предыдущее состояние устройства с его текущим состоянием, чтобы определять факт нажатия пользователем кнопки или клавиши в текущей сцене.
Таким образом, в вашей сцене помощи есть всего два 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. Сцена помощи в нормальном и широкоэкранном формате
Начальный экран игры всегда передает «вкус» самой игры. Обычно он показывает что-то впечатляющее, что должно представлять некоторые возможности игры, и меню, дающее пользователю возможность перемещаться между самой игрой, настройками, помощью и т.д.
Для Rock Rain вы создадите сцену с названием игры, отображаемым большими буквами, появляющимися из-за края экрана, и меню прямо под ним (в стиле аркадных игр 1980-х годов), с фоном с некоей астероидной темой. Для этого вы используете текстуры, показанные на рис. 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 из предыдущей главы, и по существу делает те же самые вещи. Вы используете этот класс позже для составления сцены игры.
Вам надо создать еще один элемент сцены игры: табло со счетом. Это табло со счетом показывает количество набранных очков и энергию космического корабля игрока. Этот класс простой: он только рисует две строки текста на экране. Добавьте к проекту класс с именем 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 | < Назад | Оглавление | Далее > |