Нередко для отрисовки спрайтов используются целые спрайт-листы, которые представляют набор изображений в рамках одного спрайта. Плавное перемещение подобных изображений рождает иллюзию движения. Например, у нас есть следующий спрайт:
Данный спрайт представляет набор связанных изображений или фреймов. Если мы быстро прокрутим эти фреймы, то у нас возникнет ощущение бегущего человечка. Подобные спрайт-листы позволяют зафиксировать состояние игрового персонажа и затем выводить его в зависимости от условий. Что гораздо удобнее применения статичных спрайтов с одним изображением.
Загрузим этот спрайт в проект и попробуем его анимировать. Приложение обновляет свое состояние 60 раз в секунду, то есть чтобы анимировать спрайт и перемещаться по его изображениям, нам надо изменять текущие координаты отрисовки части спрайта.
Определим следующий код игры:
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; namespace Game1 { public class Game1 : Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Texture2D texture; Vector2 position = Vector2.Zero; int frameWidth = 108; int frameHeight = 140; Point currentFrame = new Point(0, 0); Point spriteSize = new Point(8, 2); public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; TargetElapsedTime = new System.TimeSpan(0, 0, 0, 0, 400); } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); texture = Content.Load<Texture2D>("scottpilgrim_multiple"); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) Exit(); ++currentFrame.X; if (currentFrame.X >= spriteSize.X) { currentFrame.X = 0; ++currentFrame.Y; if (currentFrame.Y >= spriteSize.Y) currentFrame.Y = 0; } base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend); spriteBatch.Draw(texture, position, new Rectangle(currentFrame.X * frameWidth, currentFrame.Y * frameHeight, frameWidth, frameHeight), Color.White, 0, Vector2.Zero, 1, SpriteEffects.None, 0); spriteBatch.End(); base.Draw(gameTime); } } }
Ключевыми для отрисовки нужного фрейма из спрайта являются следующие переменные:
int frameWidth = 108; int frameHeight = 140; Point currentFrame = new Point(0, 0); Point spriteSize = new Point(8, 2);
Переменные frameWidth
и frameHeight
указывают соответственно на ширину и высоту фрейма, то есть отдельного
изображения на спрайте. Переменная currentFrame, представляющая структуру Point, задает положение текущего фрейма. По умолчанию мы устанавливаем значение
new Point(0, 0)
, то есть самый первый фрейм на спрайте. Размер спрайта задается через переменную spriteSize
.
Ее значение new Point(8, 2)
указывает, что на спрайте 8 фреймов по ширине и 2 фрейма по высоте.
В методе Update с каждым новым оборотом игрового цикла переменная, указывающая на текущий фрейм, будет увеличивать свое значение, пока не достигнет предельных значений:
++currentFrame.X; if (currentFrame.X >= spriteSize.X) { currentFrame.X = 0; ++currentFrame.Y; if (currentFrame.Y >= spriteSize.Y) currentFrame.Y = 0; }
После этого происходит отрисовка спрайта с помощью перегрузки метода spriteBatch.Draw()
:
spriteBatch.Draw(texture, position, new Rectangle(currentFrame.X * frameWidth, currentFrame.Y * frameHeight, frameWidth, frameHeight), Color.White, 0, Vector2.Zero, 1, SpriteEffects.None, 0);
При запуске приложения начнется анимация фреймов в спрайт-листов. Однако мы можем столкнуться, что анимация работает слишком быстро. В этом случае мы можем установить явным образом частоту смены кадров. Для этого добавим в конструктор класса Game1 следующую строку:
TargetElapsedTime = new System.TimeSpan(0, 0, 0, 0, 50);
Свойство TargetElapsedTime задает частоту смены кадров. В данном случае в качестве времени игрового цикла устанавливается 50 миллисекунд. То есть кадры будут меняться с частотой в 1000 / 50 = 20 раз в секунду, что меньше стандартного значения (60 раз в секунду).
При задании частоты кадров в игре нам надо учитывать, что если частота будет слишком высокой, то процессор может не успевать обновлять состояние игры и производить перерисовку. Если же, наоборот, сделать небольшую частоту, то игра будет нединамичной, так как обновления будут производиться очень медленно.
Однако данный метод установки скорости анимации имеет один недостаток: если у нас в приложении используются несколько спрайтов, которые должны обновляться с разной частотой, то установка одной и той же частоты глобально будет не лучшим решением.
Поэтому удалим из конструктора строку:
TargetElapsedTime = new System.TimeSpan(0, 0, 0, 0, 50);
И добавим в класс Game1 две глобальные переменные:
int currentTime = 0; // сколько времени прошло int period = 50; // период обновления в миллисекундах
И после этого изменим метод Update:
protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) Exit(); // добавляем к текущему времени прошедшее время currentTime += gameTime.ElapsedGameTime.Milliseconds; // если текущее время превышает период обновления спрайта if (currentTime > period) { currentTime -= period; // вычитаем из текущего времени период обновления ++currentFrame.X; // переходим к следующему фрейму в спрайте if (currentFrame.X >= spriteSize.X) { currentFrame.X = 0; ++currentFrame.Y; if (currentFrame.Y >= spriteSize.Y) currentFrame.Y = 0; } } base.Update(gameTime); }
С помощью свойства gameTime.ElapsedGameTime.Milliseconds мы можем получить время игрового цикла, по умолчанию это примерно 1000/60 = 16 миллисекунд. И затем это время добавляем переменной currentTime, которое содержит время, прошедшее после последнего обновления фреймов. Если это время окажется больше интервала обновления фреймов, то переходим к новому фрейму.
Но имитация человечка, бегущего на месте, не очень выразительна. Объединим анимацию фрейма с перемещением спрайта по экрану. Полный код программы:
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; namespace Game1 { public class Game1 : Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Texture2D texture; Vector2 position = Vector2.Zero; float speed = 5f; int currentTime = 0; // сколько времени прошло int period = 50; // частота обновления в миллисекундах int frameWidth = 108; int frameHeight = 140; Point currentFrame = new Point(0, 0); Point spriteSize = new Point(8, 2); public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); texture = Content.Load<Texture2D>("scottpilgrim_multiple"); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) Exit(); currentTime += gameTime.ElapsedGameTime.Milliseconds; if (currentTime > period) { currentTime -= period; position.X += speed; if (position.X > Window.ClientBounds.Width - frameWidth || position.X < 0) { speed *= -1; ++currentFrame.Y; if (currentFrame.Y >= spriteSize.Y) currentFrame.Y = 0; } ++currentFrame.X; if (currentFrame.X >= spriteSize.X) { currentFrame.X = 0; } } base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend); spriteBatch.Draw(texture, position, new Rectangle(currentFrame.X * frameWidth, currentFrame.Y * frameHeight, frameWidth, frameHeight), Color.White, 0, Vector2.Zero, 1, SpriteEffects.None, 0); spriteBatch.End(); base.Draw(gameTime); } } }