Por Dennes Torres dennes@bufaloinfo.com.brDennes Torres possui as certificações MCAD, MCSD,MCSE, MCDBA e MCT. Atualmente atua como diretor da Búfalo Informática, líder do grupo de usuários DevASPNet, co-lider dos grupos devSQL e getWindows no Rio de Janeiro, podendo sempre ser encontrado na lista de discussão do grupo DevASPNet (devaspnet-subscribe@yahoogrupos.com.br) bem como nas reuniões do grupo. Possui também um blog em http://cidadaocarioca.blogspot.com |
|
|
|
|
| Utilizando POO na criação de jogos com XNA | |
|
|
|
No artigo anterior, que encontra-se em http://www.bufaloinfo.com.br/artigos/coluna39.asp mostrei o passo-a-passo de criação de jogos com o XNA Game Studio Express. Porém destaquei que naquele artigo estava utilizando um estilo estruturado, que traz desvantagens ao resultado.
Neste artigo vamos implementar novamente o mesmo jogo, só que utilizando uma metodologia orientada a objeto, de forma a resultar em um código mais organizado e de mais fácil manutenção.
1) Criar o novo projeto de nosso jogo
2) Alterar as propriedades do objeto Graphics para definir o tamanho da tela
3) Definir as variáveis que serão necessárias para montar a tela do jogo
(no momento, apenas o spriteBatch)
SpriteBatch sprite;
4) Inserir as 2 imagens no projeto
5) Criar uma classe base de objeto
Primeiramente, identificamos os objetos do nosso jogo : Bola e Jogador. Vamos precisar de uma classe para o objeto Bola e outra classe para o objeto Jogador, sem dúvida. Mas a questão é que alguns dos comportamentos destas classes são identicos e, portanto, podemos generaliza-los em uma classe base.
Veja os comportamentos comuns :
- Ambos possuem uma posição
- Ambos se movem (ambos horizontalmente, a bola também se move verticalmente)
- Ambos possuem uma imagem que deve ser impressa no jogo
- Ambos não podem sair horizontalmente da tela
- Então vamos criar uma classe chamada BaseObjeto.
6) Implementar o controle da imagem no BaseObjeto
private Texture2D _imagem;
public Texture2D imagem
{
get
{
return(_imagem);
}
}
A imagem dos objetos do jogo, para ser carregada, precisa de um objeto GraphicsDevice, que controla a área gráfica do jogo. Se o objeto GraphicsDevice mudar, a imagem precisa ser recarregada.
Assim sendo a classe BaseObjeto precisará estar pronta para receber um objeto GraphicsDevice e sempre que receber um novo GraphicsDevice deverá refazer a carga da imagem.
private GraphicsDevice _graph;
public GraphicsDevice graphics
{
get
{
return(_graph);
}
set
{
_graph=value;
_imagem=Texture2D.FromFile(graphics,nomeImagem());
}
}
Para carregar a imagem utilizamos o método nomeImagem, que irá nos informar qual imagem será carregada. Foi criado desta forma porque cada classe base (jogador, bola) tem uma imagem diferente. Então na classe pai utilizamos um método abstrato nomeImagem de forma que cada classe filha poderá determinar o nome e caminho da própria imagem.
Em nossa classe BaseObjeto fazemos apenas uma declaração :
protected abstract string nomeImagem();
7) Implementar o controle de velocidade no BaseObjeto
A velocidade de movimentação de um objeto será contada em pixels. Na classe BaseObjeto iremos implementar apenas a velocidade horizontal, que ambos os objetos possuem.
private int velocidadeH;
public int velocidade
{
get
{
return(velocidadeH);
}
set
{
velocidadeH=value;
}
}
8) Implementar o controle de posição
protected Point _posicao;
Precisaremos interceptar quando houver uma tentativa, de fora da classe, de mudar a posição do objeto. Porém criar uma propriedade para manipular um objeto Point do XNA é um pouco complicado, pois o tipo Point é armazenado como value type. Então contornamos o problema :
public virtual void mudarPosicao(Point p)
{
_posicao=p;
}
public Point posicao
{
get
{
return(_posicao);
}
}
A propriedade é readOnly e utilizamos um método para mudar a posição. Por sua vez a variável _posicao é protected, podendo ser manipulada por qualquer classe filha diretamente.
Observe que o mudarPosicao foi definido como virtual, podendo ser sobrescrito em classes filhas.
9) Implementar o desenho do objeto
Para isso precisaremos de apenas um método, veja :
public void desenhar(SpriteBatch sprite)
{
sprite.Draw(imagem,new Rectangle(posicao.X,posicao.Y,
imagem.Width,imagem.Height),Color.White);
}
O objeto SpriteBatch é utilizado para fazer o desenho, precisa ser recebido como parâmetro. Utilizamos então o método Draw.
10) Criar a movimentação básica
Basta um método Mover e fazer o teste do resultado, verificando se saiu dos limites laterais da tela. O comportamento da bola e do jogador caso tenham saido do limite será diferente, portanto o que é feito é apenas sinalizar a saida :
protected bool passouLimite;
public virtual bool Mover()
{
_posicao.X+=velocidadeH;
if ((posicao.X<=0)||(posicao.X>=(800 - imagem.Width)))
passouLimite=true;
return(true);
}
11) Criar a classe Jogador, herdando de BaseObjeto
class Jogador : BaseObjeto
{
public Jogador(int x, int y)
: base(x, y)
{
velocidade = 3;
}
}
Observe que chamamos o construtor da classe base e definimos um valor default para a velocidade.
12) Definir o método nomeImagem
protected override string nomeImagem()
{
return ("../../paddle.png");
}
Desta forma especificamos a imagem que será utilizada para o jogador
13) Controlar a saida do limite da tela
No caso do jogador, ele não pode sair da tela, então revertemos a movimentação
public override bool Mover()
{
base.Mover();
if (passouLimite)
{
_posicao.X -= velocidade;
passouLimite = false;
}
return (true);
}
Sobrescrevemos o método mover para gerar esta nova lógica, mas continuamos a utilizar o mover original no local adequado.
14) Criar a movimentação para a esquerda
A movimentação padrão é feita apenas para a direita, então precisamos de um método para mover para a esquerda :
public void moverEsquerda()
{
_posicao.X -= velocidade;
if (posicao.X < 5)
_posicao.X = 5;
}
15) Criar a classe Bola
16) A Bola terá não só a movimentação horizontal, mas também uma movimentação vertical, precisaremos defini-la.
class Bola : BaseObjeto
{
private int velocidadeV;
public Bola(int x, int y)
: base(x, y)
{
velocidade = 3;
velocidadeV = 3;
}
public int velocidadeVert
{
get
{
return (velocidadeV);
}
set
{
velocidadeV = value;
}
}
17) Definir o método nomeImagem
protected override string nomeImagem()
{
return ("../../ball.png");
}
18) Implementar as movimentações personalizadas
A bola não se move apenas na horizontal, mas também na vertical. Se colidir com um dos limites da tela, a direção deve ser invertida. Se a bola passar da parte inferior, sai do jogo, então o método mover retorna true indicando que o objeto saiu do jogo.
private bool _BolaFora = false;
public override bool Mover()
{
_posicao.Y += velocidadeV;
base.Mover();
if (passouLimite)
{
velocidade *= -1;
passouLimite = false;
}
if ((posicao.Y <= 0))
velocidadeV *= -1;
if (posicao.Y >= (600 - imagem.Height))
BolaFora = true;
return (BolaFora);
}
public bool BolaFora
{
get
{
return _BolaFora;
}
private set
{
_BolaFora = value;
}
}
Observe que utilizamos o método Mover original e logo em seguida testamos a variável passouLimite para verificar se a bola passou dos limites da tela e inverter a velocidade horizontal. O calculo do limite vertical, porém, é feito todo neste método.
A variável _BolaFora sinaliza se a bola saiu de jogo (parte inferior da tela).
19) Implementar o teste de colisão
Como isso depende de uma comparação com valores da classe jogador, precisaremos implementar na forma de um método :
int Acertos;
public bool colidiu(Jogador objJogador)
{
if (posicao.Y >= (objJogador.posicao.Y - imagem.Height))
if ((posicao.X >= (objJogador.posicao.X - imagem.Width)) && (posicao.X <= (objJogador.posicao.X + objJogador.imagem.Width)))
{
velocidadeVert *= -1;
Acertos += 1;
if (Acertos == 6)
{
if (velocidade > 0)
velocidade += 1;
else
velocidade -= 1;
if (velocidadeVert > 0)
velocidadeVert += 1;
else
velocidadeVert -= 1;
Acertos = 0;
}
return (true);
}
return (false);
}
Observe o cuidado do cálculo com relação as dimensões da bola e do jogador
Neste método implementamos também uma lógica de jogo adicional : Depois de 6 acertos a bola aumenta de velocidade.
20) Recolocar a bola em Jogo
Vamos sobrescrever o método mudarposicao para reconhecer quando a bola estiver sendo recolocada em jogo, alterando o flag BolaFora.
public override void mudarPosicao(Point p)
{
base.mudarPosicao(p);
this.BolaFora = false;
Acertos = 0;
}
A variável Acertos (da lógica de aumento de velocidade) é também zerada.
21) Na classe Game, definir as variáveis de objetos do jogo
Jogador objJogador;
Bola objBola;
22) Na classe Game, criar a inicialização das classes
A inicialização pode estar acontecendo no inicio do jogo ou quando a bola sair de jogo. Isso deve ser testado, veja :
void IniciarClasses()
{
if (objJogador==null) {
objJogador=new Jogador(400,550);
objBola=new Bola(200,50);
}
else
{
objBola.mudarPosicao(new Point(200,50));
}
}
23) Disparar o IniciarClasses no constructor da classe Game
public Game1()
{
graphics = new GraphicsDeviceManager(this);
content = new ContentManager(Services);
IniciarClasses();
}
24) Criar a inicialização das imagens
void Inicializar()
{
objJogador.graphics=graphics.GraphicsDevice;
objBola.graphics =graphics.GraphicsDevice;
sprite=new SpriteBatch(graphics.GraphicsDevice);
}
As classes foram programadas para inicializar suas próprias imagens ao receberem o GraphicsDevice. Por fim, inicializamos o SpriteBatch
25) Programar o onStarting da classe Game
protected override void Initialize()
{
// TODO: Add your initialization logic here
base.Initialize();
graphics.GraphicsDevice.DeviceReset +=GraphicsDevice_DeviceReset;
Inicializar();
}
void GraphicsDevice_DeviceReset(object sender,EventArgs e)
{
Inicializar();
}
Inicializamos as imagens no OnStarting e interceptamos o evento deviceReset do GraphicsDevice para refazermos a inicialização das imagens.
26) Programar a entrada pelo teclado
void EntradaTeclado()
{
KeyboardState currentState;
currentState = Keyboard.GetState();
Keys[] currentKeys = currentState.GetPressedKeys();
//check for up and down arrow keys
foreach (Keys key in currentKeys)
{
if (key == Keys.Left)
objJogador.moverEsquerda();
if (key == Keys.Right)
objJogador.Mover();
if (key == Keys.Escape)
this.Exit();
}
}
O XNA Framework nos ajuda a lidar com o teclado através do objeto KeyBoard. Disparamos então o método adequado na classe Jogador e deixamos o ESC como tecla de saida do jogo.
27) Implementar o método Update da classe Game
protected override void Update(GameTime gameTime)
{
// Allows the default game to exit on Xbox 360 and Windows
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// TODO: Add your update logic here
EntradaTeclado();
//Se voltou false, a bola saiu de jogo
if (objBola.Mover())
IniciarClasses();
objBola.colidiu(objJogador);
base.Update(gameTime);
}
O método Update da classe do jogo será chamado em intervalos de tempo para executar as ações do jogo. Neste método então fazemos o seguinte :
A) Verificamos a entrada de teclado, consequentemente gerando a movimentação do jogador
B) Movemos a bola e, se saiu de jogo, reiniciamos
C) Verificamos se a bola colidiu com o jogador
28) Implementar o desenho do jogo
O método Draw já é criado parcialmente codificado na classe Game. Precisaremos apenas completar a codificação em um ponto claramente marcado com um TODO
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
sprite.Begin( SpriteBlendMode.AlphaBlend );
objJogador.desenhar(sprite);
objBola.desenhar(sprite);
sprite.End();
base.Draw(gameTime);
}
O spriteBatch controla o processo de desenho, de forma levemente semelhante a uma transação, com um begin/end. Como já implementamos o desenho dos objetos em suas classes, basta chamar os métodos desenhar passando o objeto spriteBatch como parâmetro.