Изображения в играх могут быть как анимированными, так и неанимированными. Неанимированное изображение – это некий рисунок заданного размера, состоящий из одного кадра, или фрейма. Примером неанимированного изображения служит статический или двигающийся объект, состоящий из одного фрейма, рассмотренный нами в предыдущей главе. Анимированное изображение – это изображение, состоящее уже из определенного набора фреймов или ряда последовательных взаимосвязанных зображений (анимационная последовательность). Переход по всем имеющимся фреймам анимационной последовательности создает иллюзию анимации в игровом процессе. Анимированное изображение может состоять из любого количества фреймов, задающих анимационную последовательность для персонажей игры. Отсчет фреймов изображения происходит от нуля, точно так же, как это делается в простом массиве данных. Количество фреймов анимационной последовательности не ограничено, но весь набор последующих фреймов анимационной последовательности должен совпадать с размером самого первого фрейма как по ширине, так и по высоте. То есть все фреймы должны быть одинакового размера. Располагать фреймы изображения можно горизонтально, ертикально или компоновать любым удобным для вас способом. Отсчет по фреймам изображения всегда происходит слева направо и сверху вниз. Посмотрите на рис. 7.1, где изображен шагающий робот. Рис. 7.1. Шагающий робот Изображение шагающего робота на рис. 7.1 состоит из четырех фреймов анимационной последовательности, где каждый фрейм определяет одну из фаз движения робота. Циклическое перемещение по фреймам этого изображения или постоянная перерисовка реймов на экране телевизора создаст эффект ходьбы робота в игре. Этот механизм работы с анимацией используется в консольных, компьютерных и мобильных играх, а также в мультипликационных фильмах. В этой главе вашему вниманию будут представлены два проекта – Animation и Background. В первом проекте Animation вы научитесь загружать в программу анимационные последовательности и создавать анимацию на экране. Во втором проекте, или во второй части этой главы, мы добавим в игру статическое фоновое изображение (как принято говорить, Background, или задний фон), заметно улучшив тем самым графический интерфейс всей игры. Итак, переходим к работе над проектами. 7.1. Проект Animation Задача первого проекта заключается в загрузке в программу анимационной последовательности, а также реализации механизма перебора всех имеющихся фреймов. В текущем проекте сам спрайт рисуется в центре экрана без возможности его перемещения по экрану. 7.1.1. Анимационная последовательность В нашей игре анимационная последовательность персонажей будет состоять из набора нескольких фреймов, имитирующих движение объекта в момент его падения сверху вниз. В предыдущей главе в проект загружалось изображение девушки, состоящее из одного фрейма. Чтобы сделать анимационную последовательность необходимо к имеющемуся фрейму дорисовать еще несколько дополнительных фреймов. Количество фреймов и содержание самого рисунка могут быть любыми, главное, чтобы циклический переход по анимационной последовательности создавал эффект какого-то движения, которое было близко к естественным движениям. Наша задача в этой связи понятна и не очень проста. Необходимо нарисовать падающую сверху вниз девушку, которая должна «трепыхать» руками, ногами, телом и т. д. Заметьте, что чем больше фреймов вы нарисуете, тем плавнее будет анимация, но и, с другой стороны, уж очень большое количество фреймов может несколько ритормаживать игру. Конечно, в консольных играх количество фреймов одной анимационной оследовательности не критично и может составлять 10–20 и более фреймов, тогда как в мобильных играх разработчикам приходится себя сильно ограничивать в этом плане. На рис. 7.2 представлен набор фреймов для падающей с небес девушки. 7.1.2. Класс Sprite проекта Animation В исходный код класса Sprite нового проекта Animation необходимо добавить несколько элементов. Как вы помните, все наши последующие разработки модифицируют редыдущие. В первую очередь в классе Sprite нужно создать четыре новые еременные, объявление которых происходит в глобальной области исходного кода файла Sprite.cs. Рис. 7.2. Анимационная последовательность девушки private int frameСount; private double timeFrame; private int frame; private double totalElapsed; Первая переменная frameСount впоследствии будет содержать в себе количество фреймов нашей анимационной последовательности и примет прямое участие в организации циклического показа фреймов на экране. Вторая переменная timeFrame получит значение времени, измеряемое в миллисекундах, отведенное для показа одного из фреймов на экране. Именно с помощью этой переменной мы будем регулировать время задержки показа одного фрейма на экране. Если не делать задержки для показа одного из фреймов анимации, то переход по всей анимационной последовательности пройдет с такой скоростью, что вы не успеете даже заметить, что мы там пытались анимировать и зачем. Следующая переменная frame – это своего рода счетчик, представляющий в текущий момент один из фреймов. Этот счетчик по прошествии заданного времени задержки (переменная timeFrame) увеличивается на единицу для перехо- да к показу следующего фрейма. Последняя переменная totalElapsed будет содержать значение времени, необходимое для расчета времени задержки показа одного фрейма, а точнее расчета прошедшего времени, выделенного на показ одного из фреймов. После объявления в исходном коде всех переменных их необходимо инициализировать заданными значениями. Для этих целей в классе Sprite формируется второй конструктор, необходимый для создания анимированных объектов этого класса. public Sprite(int frameCount, int framesPerSec) { frameСount = frameCount; timeFrame = (float)1 / framesPerSec; frame = 0; totalElapsed = 0; } Теперь в классе Sprite имеются два конструктора этого класса, позволяющие создавать как статические, так и анимированные спрайты, или объекты этого класса. Второй конструктор класса Sprite содержит два целочисленных параметра – frameCount и framesPerSec, которые передаются в создаваемый объект класса. Первый параметр frameCount задает общее количество фреймов для загружаемого при создании этого объекта изображения. Количество фреймов загружаемой анимационной последовательности необходимо точно знать, оно определяется на этапе создания рисунка. В нашем случае для девушки мы имеем 12 фреймов. На основе полученного значения, или количества фреймов изображения, происходит расчет времени задержки для показа одного фрейма на экране. Второй параметр framesPerSec конструктора класса Sprite в момент создания объекта этого класса получает количество кадров, которые необходимо показать за одну секунду. Иначе говоря, в этом параметре задается определенное количество кадров, показываемых на экране за одну секунду. Это значение позволяет рассчитать время задержки, необходимое для показа одного фрейма на экране. Параметр framesPerSec в какой-то мере позволяет задавать скорость перемещения по фреймам или даже задает скорость перебора фреймов анимационной последовательности изображения. Такой подход позволяет реализовывать в игре разную степень скорости анимации объектов, а также возможность создавать различное количество объектов класса Sprite, каждый из которых может содержать любое число фреймов анимационной последовательности. Далее в исходном коде класса Sprite добавляется новый метод UpdateFrame(), реализующий механику перебора фреймов изображения на основании полученного системного времени и его сравнения со временем задержки, отведенным на показ одного фрейма. public void UpdateFrame(double elapsed) { totalElapsed += elapsed; if (totalElapsed > timeFrame) { frame++; frame = frame % (frameСount - 1); totalElapsed -= timeFrame; } } В метод UpdateFrame() передается значение времени, полученное с момента предыдущего вызова этого метода, и его увеличение на единицу (получение и передача времени реализуются в классе Game1). Затем в конструкции кода if/else проверяется условие, в котором время задержки для показа одного фрейма (переменная timeFrame) не должно быть больше полученного значения времени. Как только это время больше или время задержки для показа одного фрейма истекает, то происходит увеличение счетчика фреймов на единицу и соответственно переход к показу следующего фрейма. Затем в конце исходного кода класса Sprite добавляется новый метод DrawAnimationSprite(), предназначенный для рисования анимированных спрайтов, созданных посредством анимированного конструктора класса Sprite. public void DrawAnimationSprite(SpriteBatch spriteBatch) { int frameWidth = spriteTexture.Width / frameÑount; Rectangle rectangle = new Rectangle(frameWidth * frame, 0, frameWidth, spriteTexture.Height); spriteBatch.Draw(spriteTexture, spritePosition, rectangle, Color.White); } В первой строке кода этого метода переменная frameWidth инициализируется значением ширины одного фрейма анимационной последовательности, измеряемой в пикселях. Строка spriteTexture.Width позволяет получить текущую ширину графического изображения (длину всех 12 фреймов изображения sprite.png), и далее происходит деление этого значения на общее количество фреймов анимационной последовательности (например: 200 пикселей / 5 фреймов = 40 пикселей), что в итоге дает нам искомую ширину одного фрейма. Вот поэтому все фреймы анимационной последовательности должны быть одинаковыми! В предложенном блоке кода можно использовать и числовые значения для каждого конкретного изображения, но тогда для каждого загружаемого в игру спрайта вам придется писать отдельный метод DrawAnimationSprite(). Во второй строке кода метода DrawAnimationSprite() создается прямоугольник или видимая прямоугольная область, которая по своему размеру равна ширине и высоте одного фрейма анимационной последовательности. То есть создается механизм отсечения всех фреймов анимационной последовательности, кроме одного-единственного фрейма, который рисуется в текущий момент на экране. Если не создавать такого механизма, то на экране будет нарисована вся анимационная последовательность разом, состоящая из 12 фреймов. Фактически в коде создается этакая дыра в стене заданного размера, за которой мы подставляем необходимые фреймы рисунка, чередуя их с заданной скоростью, что в итоге и создает эффект анимации. В последней строке кода метода DrawAnimationSprite() происходит прорисовка одного из фреймов на экране телевизора: spriteBatch.Draw(spriteTexture, spritePosition, rectangle, Color.White); где параметры: * spriteTexture – это загруженное в игру изображение; * spritePosition – позиция на экране телевизора; * rectangle – ограничивающий или отсекающий прямоугольник, равный ширине и высоте одного фрейма; * Color.White – задает цвет для рисуемого изображения. Полный исходный код класса Sprite проекта Animation представлен в листинге 7.1. //========================================================================= /// /// Листинг 7.1 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Автор книги: Горнаков С. Г. /// Глава 7 /// Проект: Animation /// Класс: Sprite /// Создаем анимацию /// //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; #endregion namespace Animation { public class Sprite { public Texture2D spriteTexture; public Vector2 spritePosition; private int frameСount; private double timeFrame; private int frame; private double totalElapsed; /// /// Конструктор /// public Sprite() { } /// /// Конструктор для анимированного спрайта /// public Sprite(int frameCount, int framesPerSec) { frameСount = frameCount; timeFrame = (float)1 / framesPerSec; frame = 0; totalElapsed = 0; } /// /// Цикличный переход по фреймам - анимация /// public void UpdateFrame(double elapsed) { totalElapsed += elapsed; if (totalElapsed > timeFrame) Проект Animation { frame++; frame = frame % (frameСount - 1); totalElapsed -= timeFrame; } } } /// /// Загрузка спрайта в игру /// public void Load(ContentManager content, String stringTexture) { spriteTexture = content.Load(stringTexture); } /// /// Рисуем простой спрайт /// public void DrawSprite(SpriteBatch spriteBatch) { spriteBatch.Draw(spriteTexture, spritePosition, Color.White); } /// /// Рисуем анимированный спрайт /// public void DrawAnimationSprite(SpriteBatch spriteBatch) { int frameWidth = spriteTexture.Width / frameСount; Rectangle rectangle = new Rectangle(frameWidth * frame, 0, frameWidth, spriteTexture.Height); spriteBatch.Draw(spriteTexture, spritePosition, rectangle, Color.White); } } } 7.1.3. Класс Game1 проекта Animation Теперь давайте перейдем к рассмотрению класса Game1 проекта Animation. Ниже в листинге 7.2 представлен исходный код этого класса. Сначала посмотрим на весь код этого класса, а затем приступим к его изучению. //========================================================================= /// /// Листинг 7.2 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Автор книги: Горнаков С. Г. /// Глава 7 /// Проект: Animation /// Класс: Game1 /// Создаем анимацию /// //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace Animation { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; SpriteBatch spriteBatch; Sprite sprite; /// /// Конструктор /// public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1280; graphics.PreferredBackBufferHeight = 720; sprite = new Sprite(12, 10); } /// /// Инициализациия /// protected override void Initialize() { sprite.spritePosition = new Vector2(300, 200); base.Initialize(); } /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); sprite.Load(content, «Content\\Textures\\sprite»); } Проект Animation } /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); double elapsed = gameTime.ElapsedGameTime.TotalSeconds; sprite.UpdateFrame(elapsed); base.Update(gameTime); } /// /// Рисуем на экране /// protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); sprite.DrawAnimationSprite(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } } } В исходном коде класса Game1, в области глобальных переменных происходит объявление объекта sprite класса Sprite. Sprite sprite; Затем в конструкторе класса Game1 создается полноценный объект sprite. sprite = new Sprite(12, 10); Для формирования этого объекта используется конструктор класса Sprite, подготовленный для создания анимированных объектов. В качестве параметров в конструктор класса Sprite передаются два целочисленных значения. Первое значение – это количество фреймов анимационной последовательности для загружаемого в игру изображения. Второй целочисленный параметр конструктора класса Sprite задает определенное значение, на базе которого происходит расчет времени задержки для показа одного фрейма анимационной последовательности. Чем больше это значение, тем быстрее будет анимация на экране. Далее в исходном коде класса Game1 в методе Initialize() назначается позиция для спрайта на экране телевизора, а в методе LoadGraphicsContent() происходит загрузка изображения sprite.png. Исходное изображение располагается в рабочем каталоге проекта в папках Content\Textures. Затем в методе Update() мы обновляем состояние игры и вызываем метод UpdateFrame() класса Sprite. protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); double elapsed = gameTime.ElapsedGameTime.TotalSeconds; sprite.UpdateFrame(elapsed); base.Update(gameTime); } В качестве параметра в методе UpdateFrame() класса Sprite передается значение переменной elapsed. Эта переменная при каждой итерации игрового цикла, то есть при каждом новом проходе по методу Update(GameTime gameTime), получает текущее значение времени, прошедшее за один игровой цикл или за один проход по коду метода Update(GameTime gameTime). А уже в методе UpdateFrame() класса Sprite происходит сравнение времени задержки на показ одного фрейма с прошедшим временем и соответственно решается, показывать по-прежнему этот фрейм анимационной последовательности или переходить к следующему фрейму. В самом конце исходного кода класса Game1 происходит вызов метода Draw(), где на экран выводится один из фреймов всей анимационной последовательности изображения. protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); sprite.DrawAnimationSprite(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } Проект Animation 7.2. Проект Background Фоновые изображения в играх могут быть различного плана, здесь все зависит от конкретной задачи в реализации самой игры. В нашем случае фон представлен статическим изображением размером в 1280 пикселей по ширине и 720 пикселей по высоте. Поэтому начальной точкой вывода изображения служит начало отсчета системы координат (рис. 7.3). Рис. 7.3. Расположение фона на экране телевизора В качестве фоновой картинки мы используем изображение гор, песка и неба. Графический файл фона background.png располагается в рабочем каталоге проекта в папке Content\Textures. Добавление изображения в проект необходимо произвести явно через выполнение команд Add -> Exiting Item (глава 6, раздел 6.2), как это мы делали для изображения sprite.png. Загружать фоновое изображение мы будем напрямую прямо в классе Game1, поскольку для фона не определено никаких дополнительных функций, то и создание отдельного класса для фона или использование класса Sprite не обязательно. Если же в других ваших играх фоновое изображение будет иметь гораздо больше функциональных возможностей, например механизм скроллинга, то стоит создать для этих целей отдельный класс. Пример реализации скроллинга в играх показан в документации к XNA Game Studio Express (раздел Programming Guide -> Graphics -> 2D Graphics -> Make a Scrolling Background). В листинге 7.3 представлен код класса Game1 проекта Background. //========================================================================= /// /// Листинг 7.3 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Автор книги: Горнаков С. Г. /// Глава 7 /// Проект: Background /// Класс: Game1 /// Добавляем фон /// //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace Background { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; SpriteBatch spriteBatch; Sprite sprite; private Texture2D background; /// /// Конструктор /// public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1280; graphics.PreferredBackBufferHeight = 720; sprite = new Sprite(12, 10); } /// /// Инициализация /// protected override void Initialize() { sprite.spritePosition = new Vector2(300, 200); base.Initialize(); } /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); sprite.Load(content, «Content\\Textures\\sprite»); background = content.Load(«Content\\Textures\\background»); } } /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { … } /// /// Рисуем на экране /// protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background, new Vector2(0, 0), Color.White); sprite.DrawAnimationSprite(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } } } В исходном коде класса Game1 в области глобальных переменных происходит объявление объекта background. Как мы уже решили, этот объект представляет в игре фоновое изображение. private Texture2D background; Затем в методе LoadGraphicsContent() происходит загрузка фона в игру из файла background.png. В данном случае механизм загрузки фона идентичен механизму загрузки простого спрайта без создания дополнительного класса Sprite. Этот механизм мы изучали в начале главы 6. Background = content.Load(«Content\\Textures\\background»); В свою очередь, в методе Draw() фон, а также анимированный спрайт рисуются на экране. protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background, new Vector2(0, 0), Color.White); sprite.DrawAnimationSprite(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } Главное в исходном коде метода Draw() – это последовательность вывода рисунков на экран телевизора. Каждое последующее рисуемое изображение накладывается поверх предыдущего. Запомнить это очень легко, просто помните, что первая строка кода метода Draw(), которая рисует изображение, – это первый слой, следующая строка кода метода Draw() – соответственно следующий слой, который накладывается поверх предыдущего слоя, и т. д. (рис. 7.4). Этот механизм позволяет формировать многослойные фоновые и игровые сцены, где, например, главный герой может заходить за дом или дерево, формируя тем самым потенциальную ось Z, которая удалена от глаз пользователя как бы внутрь телевизора или монитора (трехмерный мир). Если в нашем коде взять и поменять местами строки с выводом на экран фона и спрайта, то на дисплее, кроме фона, вы ничего не увидите, потому что фоновый рисунок размером во весь экран просто-напросто закроет собой нашу анимацию. Это очень частая ошибка со стороны начинающих программистов, поэтому не забывайте, что каждый последующий слой накладывается поверх предыдущего! На этом все, переходим к следующей главе и поговорим о движении объектов в пространстве, а также о некоторых особенностях реализации простого искусственного интеллекта. Рис. 7.4. Наложение слоев в игровом мире |