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 | < Назад | Оглавление | Далее > |