Olá pessoal, tudo certo?
Hoje, volto a falar sobre desenvolvimento com XNA.
Já apresentei os fundamentos dessa tecnologia em um primeiro post. Também mostrei uma técnica simples de animação 2D usando Sprite Sheets em um segundo post. Hoje, apresento algumas sugestões de design e alguns fundamentos para “gerenciar” interação com o usuário.
Este será mais um post com pouco texto e muito código. Lembre-se que todos os exemplos demonstrados aqui está disponível no github.
Os sprites que utilizo aqui foram extraídos do projeto Platformer.
Isolando a lógica de um Game desenvolvido em XNA
Como apresentei no primeiro post, a lógica do XNA pode ser resumida como indicado no seguinte diagrama. Observe:
A execução desses métodos é orquestrada pela infraestrutura do XNA. Todos esses métodos estão na classe principal do Game.
Minha lógica para “organização” do método está em “tirar código” da classe Game e colocar em “objetos” auxiliares que reproduzem a mesma organização de métodos. Por isso, crio uma interface de referência.
interface IGameElement { Game Game { get; } void LoadContent(); void Update(GameTime gameTime); void Draw(SpriteBatch spbatch, GameTime gameTime); void UnloadContent(); }
Essa interface identica objetos que contem elementos que “formam” a lógica do game.
Isolando código para “Sprites Animation” – Don’t Repeat Yourself
A técnica que apresentei para animação de sprites, no post passado, consiste na adição de uma boa quantidade quantidade de código na classe Game. O problema daquela abordagem está na necessidade de duplicar lógica caso desejasse animar mais sprites. O código que segue “isola” a lógica de animação em uma classe externa. Observe:
class Animation : IGameElement { Texture2D texture; Rectangle[] frames; public Animation(Game game, string resource, int columns = 1, int rows = 1, int frameInterval = 60) { this.Game = game; this.Resource = resource; this.Columns = columns; this.Rows = rows; this.Position = Vector2.Zero; this.FrameInterval = frameInterval; this.AutoRepeat = true; } public string Resource { get; private set; } public int Columns { get; private set; } public int Rows { get; private set; } public int FrameInterval { get; private set; } public bool LeftToRight { get; set; } public bool AutoRepeat { get; set; } public int Width { get; private set; } public int Height { get; private set; } public Game Game {get; private set; } public Vector2 Position { get; set; } public void Reset() { this.currentFrame = 0; } public void LoadContent() { texture = Game.Content.Load(this.Resource); var cellCount = Columns * Rows; double incx = texture.Width / (double)Columns; double incy = texture.Height / (double)Rows; this.Width = (int)incx; this.Height = (int)incy; frames = new Rectangle[cellCount]; for (int i = 0; i < Rows; i++) for (int j = 0; j < Columns; j++) { var index = (i * Columns) + j; frames[index] = new Rectangle( (int) (incx * j), (int) (incy * i), this.Width, this.Height ); } } int currentFrame; int timeSizeLastFrame; public void Update(GameTime gameTime) { timeSizeLastFrame += gameTime.ElapsedGameTime.Milliseconds; if (timeSizeLastFrame > FrameInterval) { timeSizeLastFrame -= FrameInterval; currentFrame++; if (currentFrame >= frames.Length) currentFrame = (AutoRepeat ? 0 : frames.Length - 1); } } public void Draw(SpriteBatch spbatch, GameTime gameTime) { if (LeftToRight) spbatch.Draw(texture, this.Position, frames[currentFrame], Color.White, 0, Vector2.Zero, Vector2.One, SpriteEffects.FlipHorizontally, 0); else spbatch.Draw(texture, this.Position, frames[currentFrame], Color.White); } public void UnloadContent() { } }
Perceba que, basicamente, repeti o código do post anterior. Como estou usando uma interface pública semelhante a utilizada na classe Game, tive que fazer bem poucos ajustes na lógica. Aproveitei para adicionar alguns comportamentos sofisticados como: configuração para AutoRepeat e a possibilidade de dar Reset.
Utilizando a classe Animation para colocar “animar” diversos Sprite Sheets
Para demonstrar o funcionamento da classe Animation, que acabamos de construir, faço um “Game” simples que “anima” cinco sprites diferentes. Esses são os sprites:
Celebrate:
Die:
Run:
Jump:
Idle:
Abaixo, o código que escrevi para “animar” esses sprites:
class AnimationGame : Game { Animation run; Animation jump; Animation celebrate; Animation die; Animation idle; GraphicsDeviceManager graphics; SpriteBatch spriteBatch; public AnimationGame() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; run = new Animation(this, @"Images\Sprites\Player\Run", 10); jump = new Animation(this, @"Images\Sprites\Player\Jump", 11); celebrate = new Animation(this, @"Images\Sprites\Player\Celebrate", 11); die = new Animation(this, @"Images\Sprites\Player\Die", 12); idle = new Animation(this, @"Images\Sprites\Player\Idle"); } protected override void LoadContent() { run.LoadContent(); jump.LoadContent(); celebrate.LoadContent(); die.LoadContent(); idle.LoadContent(); int x, y; y = (Window.ClientBounds.Height - 64) / 2; x = (Window.ClientBounds.Width - (64 * 5)) / 2; run.Position = new Vector2(x, y); jump.Position = new Vector2(x += 64, y); celebrate.Position = new Vector2(x += 64, y); die.Position = new Vector2(x += 64, y); idle.Position = new Vector2(x += 64, y); spriteBatch = new SpriteBatch(GraphicsDevice); } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); run.Update(gameTime); jump.Update(gameTime); celebrate.Update(gameTime); die.Update(gameTime); idle.Update(gameTime); } protected override void Draw(GameTime gameTime) { spriteBatch.Begin(); run.Draw(spriteBatch, gameTime); jump.Draw(spriteBatch, gameTime); celebrate.Draw(spriteBatch, gameTime); die.Draw(spriteBatch, gameTime); idle.Draw(spriteBatch, gameTime); spriteBatch.End(); base.Draw(gameTime); } }
Perceba como fazer com que a classe “Animation” tenha uma interface parecida com “Game” simplifica a lógica do jogo. Basicamente, em cada “método” de game, chamo o correspondente de Animation.
static class Program { ////// The main entry point for the application. /// static void Main(string[] args) { using (var game = new AnimationGame()) //using (var game = new UserInputGame1()) { game.Run(); } } }
Abaixo o resultado desse código (infelizmente estático aqui na Web, baixe o código e execute o “game” para ver o resultado)
Bonito!
Criando um nível adicional de abstração – criando um “personagem”
Os diversos sprites que animei no exemplo acima revelam diversos comportamentos para um personagem. Em um jogo 2D, um “personagem” se expressa através de animações que revelam seu estado.
Observe que, mais uma vez, reproduzi a “abstração” de Game.
enum PlayerActions { Idle = 0, Run = 1, Jump = 2, Celebrate = 3, Die = 4 } class Player : IGameElement { readonly Animation[] Animations = new Animation[5]; public Player(Game game) { this.Game = game; Animations[(int) PlayerActions.Idle] = new Animation(game, @"Images\Sprites\Player\Idle"); Animations[(int)PlayerActions.Run] = new Animation(game, @"Images\Sprites\Player\Run", 10); Animations[(int)PlayerActions.Jump] = new Animation(game, @"Images\Sprites\Player\Jump", 11); Animations[(int)PlayerActions.Jump].AutoRepeat = false; Animations[(int)PlayerActions.Celebrate] = new Animation(game, @"Images\Sprites\Player\Celebrate", 11); Animations[(int)PlayerActions.Die] = new Animation(game, @"Images\Sprites\Player\Die", 12); Animations[(int)PlayerActions.Die].AutoRepeat = false; } public void Reset() { CurrentAction = PlayerActions.Idle; Position = new Vector2( Game.Window.ClientBounds.Width / 2, Position.Y); } public Game Game { get; private set; } public Vector2 Position { get; set; } public PlayerActions CurrentAction { get; private set; } public bool LeftToRight { get; private set; } public void LoadContent() { for (int i = 0; i < 5; i++) Animations[i].LoadContent(); } int speed = 4; public void Update(GameTime gameTime) { if (this.CurrentAction != PlayerActions.Die) { float x = this.Position.X; float y = this.Position.Y; if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Left)) { x -= speed; this.CurrentAction = PlayerActions.Run; LeftToRight = false; } else if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Right)) { x += speed; this.CurrentAction = PlayerActions.Run; LeftToRight = true; } else if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.C)) { this.CurrentAction = PlayerActions.Celebrate; } else { this.CurrentAction = PlayerActions.Idle; } this.Position = new Vector2(x, y); if (x < 32) this.CurrentAction = PlayerActions.Die; if (x > Game.Window.ClientBounds.Width - 32) this.CurrentAction = PlayerActions.Die; Animations[(int)CurrentAction].LeftToRight = LeftToRight; } Animations[(int)CurrentAction].Position = new Vector2( this.Position.X - Animations[(int)CurrentAction].Width / 2, this.Position.Y - Animations[(int)CurrentAction].Height ); Animations[(int)CurrentAction].Update(gameTime); } public void Draw(SpriteBatch spbatch, GameTime gameTime) { Animations[(int)CurrentAction].Draw(spbatch, gameTime); } public void UnloadContent() { } }
E aqui você pode verificar como adiciono “interação” ao game. Meu método Update:
- desloca o personagem para esquerda e para a direita, se as setas estiverem pressionadas;
- coloca o personagem em estado de “comemoração”, se a tecla “C” estiver pressionada;
- mata o personagem caso ele tente “sair da janela”
Utilizo a mesma lógica para carregar um “chão”. Observe:
class Floor : IGameElement { public Floor(Game game) { this.Game = game; } public Game Game { get; private set; } readonly Texture2D[] Textures = new Texture2D[7]; public void LoadContent() { for (int i = 0; i < 7; i++) Textures[i] = Game.Content.Load( string.Format(@"Images\Tiles\BlockA{0}", i) ); } public void UnloadContent() {} public void Update(GameTime gameTime) {} public void Draw(SpriteBatch spbatch, GameTime gameTime) { Vector2 p = new Vector2(0, Game.Window.ClientBounds.Height - 32); int index = 0; while (p.X < Game.Window.ClientBounds.Width) { index = (index + 1) % 7; var texture = Textures[index]; spbatch.Draw(texture, p, Color.White); p.X += 40; } } }
Por fim, observe como a “lógica” do game fica simples. Em alto nível, tudo que o game faz é iniciar o personagem e o piso.
public class UserInputGame1 : Game { Floor floor; Player player; GraphicsDeviceManager graphics; SpriteBatch spriteBatch; public UserInputGame1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; floor = new Floor(this); player = new Player(this); } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); floor.LoadContent(); player.LoadContent(); player.Position = new Vector2( Window.ClientBounds.Width / 2, Window.ClientBounds.Height - 32 ); } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); if (player.CurrentAction == PlayerActions.Die) if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Space)) player.Reset(); floor.Update(gameTime); player.Update(gameTime); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); floor.Draw(spriteBatch, gameTime); player.Draw(spriteBatch, gameTime); spriteBatch.End(); base.Draw(gameTime); } }
Eis o resultado:
Por hoje, era isso!
Alberto Monteiro
março 11, 2011
E ai grande Elemar.
So tem um errinho, quando reseta o personagem, a ação de morrer não reseta, para corrigir isso la no método Reset do player fiz assim:
public void Reset()
{
CurrentAction = PlayerActions.Idle;
Position = new Vector2(Game.Window.ClientBounds.Width/2, Position.Y);
Animations[(int) PlayerActions.Die].Reset();
}
elemarjr
março 12, 2011
Bacana, realmente havia um “errinho” ali
Alberto Monteiro
março 11, 2011
Aproveitando fiz mais uma modificação, adicionei a funcionalidade de pular, usando a tecla alt:
else if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.LeftAlt) || StillJumping > 0)
{
CurrentAction = PlayerActions.Jump;
StillJumping = StillJumping > 44 ? 0 : StillJumping + 1;
}
Esse variavel StillJumping é uma variavel inteira que criei para que você não precise segurar o Alt para completar o movimento. Dessa maneira é so apertar o ALT e ele efetua a movimentação do pulo por completo!
Obs: 44? Por que? Bom o speed do player é 4, e a quantidade de frames do pulo são de 11, então por isso que o movimento acontece até que StillJumping seja menor que 44 e maior que 0.
elemarjr
março 12, 2011
Perfeito!