Olá pessoal, tudo certo?
Depois de alguns dias tratando de assuntos variados, volto a tratar de XNA. Se você não sabe do que estou falando, talvez queira ler o primeiro post dessa série.
O tema de hoje são “Animated Sprites”. Para maior comodidade, considere fazer o download do código fonte.
Vamos aos fatos…
Um pouco mais sobre o método Draw
Como explicado no post anterior, o método Draw é o responsável por “colocar as coisas na tela”. Segue o código gerado pelo template padrão do XNA Game Studio:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); }
Esse método recebe sempre um parâmetro: gameTime. Este parâmetro representa o tempo decorrido desde o início da execução do jogo. Ele é usado para que possamos fazer com que nossas animações tenham a mesma velocidade, mesmo em computadores diferentes (com velocidades diferentes).
No final do método ocorre uma chamada para a implementação da classe base. Por hora, saiba apenas que isso é realmente necessário e essa chamada deve sempre ocorrer (não remova essa chamada em hipótese alguma).
Frame e Framerate
Conforme apresentado no post anterior e na descrição do método Draw, podemos notar que, por padrão, XNA vai limpar e redesenhar a tela toda em cada iteração do Game Loop. Toda cena (imagem na tela) resultante de uma execução do método Draw pode ser designada como “frame”.
Em uma analogia simples, um jogo 2D em XNA é como um daqueles “cartoon flipbooks” onde cada página possui o mesmo desenho da página anterior, porém com uma pequena alteração. Quando você vê uma página após a outra, em sequência, terá a “ilusão” de alguma animação.
O número de frames desenhados em um segundo é chamado de framerate e geralmente é designado como FPS (Frames-per-second). Por exemplo, dizer que um jogo está “rodando” em 60 fps significa dizer que 60 frames estão sendo desenhados a cada segundo. Trazendo para XNA, significa dizer que o Update/Draw está sendo executado 60 vezes em cada segundo.
Content Pipeline
Todos os recursos – como gráficos, sons e efeitos – são carregados no XNA através de um mecanismo chamado Content Pipeline.
Essencialmente, o Content Pipeline pega os arquivos desses recursos e os converte, durante a compilação, para um formato interno do XNA. Na prática, isso permite que nossos códigos não precise conter lógica de carga para recursos de diversos formatos. Além disso, se algo passa pela “importação” do Content Pipeline certamente será “corretamente” tratado durante a execução.
Adicionando recursos ao Content Pipeline
Adicionar recursos ao Content Pipeline é muito simples. Observe o Solution Explorer:
Para adicionar recursos ao Content Pipeline, basta adicionar os arquivos relacionados a recursos no projeto direcionado para isso.
Quando você cria um novo projeto XNA, o template do Visual Studio sempre criará dois arquivos: um para seu código, outro para seus recursos. O projeto dos recursos tem o mesmo nome do projeto para códigos e o sufixo “Content”.
Lembre-se que você pode adicionar imagens, vídeos, áudio. Por uma questão de organização, recomendo que você crie uma “pastinha” para cada tipo de conteúdo. Observe:
Para conferir se seu arquivo será “entendido” pelo Content Pipeline, verifique as propriedades do arquivo adicionado.
A seção identificada como XNA Framework Content Pipeline terá suas propriedades preenchidas apenas se o recurso foi “reconhecido”.
Recuperando um “recurso” da Content Pipeline durante a execução do jogo
Assumindo que você tenha uma imagem carregada e reconhecida no Content Pipeline, estamos prontos para a desenhar na tela. Para isso, precisamos escrever um código para acessar esse recurso.
O tipo utilizado para “armazenar” uma imagem, em XNA, é Texture2D. Para avançar, criamos uma variável desse tipo na classe e carregamos o recurso em LoadContent. Observe:
Texture2D texture; protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); texture = Content.Load(@"Images\smurf_sprite"); }
Para pintar, modificamos o método Draw. Observe:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); spriteBatch.Draw(texture, Vector2.Zero, Color.White); spriteBatch.End(); base.Draw(gameTime); }
Conforme indicado no post anterior, todo desenho ocorre pelo objeto spriteBatch. Observe o resultado:
Como você pode ver, desenhar alguma coisa com XNA começa por uma chamada ao método Begin e termina com o método End do objeto spriteBatch. Para desenhar a textura, passamos como parâmetro:
- uma textura (Texture2D) que deverá ser desenhada (carregada em LoadContent);
- uma posição (Vector2) em coordenadas 2D. Em XNA, a coordenada (0, 0) corresponde ao canto superior esquerdo da janela;
- uma cor (Color) que será “aplicada” na textura. Especificando branco, a imagem não será alterada.
Observe o mesmo exemplo, com vermelho sendo passado no terceiro argumento:
Centralizando a figura no centro da tela de jogo, observe o código:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); Vector2 pos = new Vector2( (Window.ClientBounds.Width - texture.Width) / 2, (Window.ClientBounds.Height - texture.Height) / 2 ); spriteBatch.Begin(); spriteBatch.Draw(texture, pos, Color.White); spriteBatch.End(); base.Draw(gameTime); }
Resultado …
Há muitas outros parâmetros que permitem “personalizar” o comportamento do Draw.
Animando (Animated Sprites)
Até aqui, todos nossos códigos mostraram como “pintar” uma textura simples na tela. Considere agora esse desenho:
Esse desenho é um “Animated Sprite Sheet”. Temos diversos quadros de animação. O segredo para “animar” é desenhar sequencialmente cada quadro. Há classes “especiais” no XNA que nos ajudam a fazer isso, mas hoje vamos fazer “na unha”.
Para começar, definimos algumas propriedades do nosso “Sprite Sheet”. Observe:
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Point frameSize = new Point(128, 128); Point currentFrame = new Point(0, 0); Point sheetSize = new Point(4, 4); // ... }
Foram apenas três propriedades. Observe:
- frameSize – tamanho de cada cédula de desenho. Em nosso “sheet” cada cédula tem 128×128 (a imagem total tem 512x 512);
- currentFrame – indica a célula atual. Utilizo um point para fazer com que X seja a coluna e Y seja a linha;
- sheetSize – indicação da quantidade de colunas e linhas.
Eis nosso método Draw (lembre-se, só desenho):
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); Vector2 pos = new Vector2( (Window.ClientBounds.Width - frameSize.X) / 2, (Window.ClientBounds.Height - frameSize.Y) / 2 ); spriteBatch.Begin(); spriteBatch.Draw(texture, pos, new Rectangle(currentFrame.X * frameSize.X, currentFrame.Y * frameSize.Y, frameSize.X, frameSize.Y), Color.White, 0, Vector2.Zero, 1, SpriteEffects.None, 0); spriteBatch.End(); base.Draw(gameTime); }
O retângulo corresponde a célula da sheet que desejo desenhar.
Agora, o método Update (lembre-se, todo “cálculo” aqui):
protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); currentFrame.X++; if (currentFrame.X >= sheetSize.X) { currentFrame.X = 0; currentFrame.Y++; if (currentFrame.Y >= sheetSize.Y) currentFrame.Y = 0; } base.Update(gameTime); }
A lógica é muito simples. Em cada frame, seleciono a próxima célula da sheet. Quando todas as células foram percorridas, retorno para a primeira célula.
Uma tela de nosso jogo:
Você não está vendo a animação, mas ela acontece (baixe o código fonte para ver a coisa acontecendo)
Ajustando o Framerate global
Como já mencionado, framerate é o termo utilizado para descrever quantos frames são desenhados em um segundo. No XNA, o valor default é 60 FPS para PCs e XBox 360 e 30 FPS no Windows Phone 7.
Podemos mudar o framerate padrão do XNA no construtor do jogo (é o lugar para isso). Observe:
public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; this.TargetElapsedTime = new TimeSpan(0, 0, 0, 0, 50); }
Isso diz ao XNA para chamar a cada 50 milisegundos, o que significa 20 fps. Essa simples alteração faz nossa animação rodar bem mais “devagar”.
Vale observar que você não deve mexer nessa propriedade. Você deveria deixar o jogo rodar em 60fps.
Ajustando a velocidade de animação de um sprite na unha
No código que segue, a forma “correta” de ajustar “na unha” a velocidade de animação. Observe:
int timeSizeLastFrame = 0; protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); timeSizeLastFrame += gameTime.ElapsedGameTime.Milliseconds; if (timeSizeLastFrame > 50) { timeSizeLastFrame -= 50; currentFrame.X++; if (currentFrame.X >= sheetSize.X) { currentFrame.X = 0; currentFrame.Y++; if (currentFrame.Y >= sheetSize.Y) currentFrame.Y = 0; } } base.Update(gameTime); }
Cansei.
Por hoje, era isso.
Ari C. Raimundo
03/03/2011
Elemar,
Excelente post. Muito bem explicado e com um ótimo exemplo de código.
Parabéns.
Danilo
15/10/2011
Muito legal o tutorial… bem explicado. Parabéns.
Você tbm postou como animar um sprite sem ser “na unha”? Pois foi dito que tem classes do XNA para lidar com isso, não é?
Abraços.
marcelocesards
23/11/2011
Cara, muito bom!!! estou curioso para saber onde foi que o Elemar aprendeu a programar no XNA.
parabéns pelo tópico. vou recomendar.
abs.
elemarjr
23/11/2011
Simples. Trabalho em uma empresa que desenvolve sistemas em 3D. Acabei aprendendo XNA por conta.
Há muitos outros posts sobre o tema.