This article is currently in the process of being translated into Portuguese (~99% done).
In this article series, we're building a complete Snake game from scratch. It makes sense to start with the Introduction and then work your way through the articles one by one, to get the full understanding.
If you want to get the complete source code for the game at once, to get started modifying and learning from it right now, consider downloading all our samples!
Creating & moving the Snake
No último artigo, criamos uma boa área na nossa implementação da SnakeWPF para a serpente se movimentar. Com isso em voga, agora está na hora de criar a serpente em si e depois fazê-la se mover pela área. Mais uma vez, vamos usar a classe WPF Rectangle para formar uma serpente com um determinado comprimento, em que cada elemento terá a mesma largura e altura que os quadrados do fundo, ou como a denominamos: A constante SnakeSquareSize!
Criando a Serpente
Iremos desenhar a serpente com um método denominado de DrawSnake() - o método é na verdade bastante simples, mas requer um pouco de material extra, incluindo uma nova classe denominada de SnakePart, bem como alguns campos extra na classe Window. Vamos começar com a classe SnakePart, que normalmente definiriamos num novo arquivo (SnakePart.cs, por exemplo):
using System.Windows;
namespace WpfTutorialSamples.Games
{
public class SnakePart
{
public UIElement UiElement { get; set; }
public Point Position { get; set; }
public bool IsHead { get; set; }
}
}
Esta classe simples conterá informações sobre cada parte da serpente: Onde está posicionado o elemento na nossa área do jogo, que UIElement (um Rectângulo, no nosso caso) representa a parte, e se é a parte da cabeça da serpente ou não? Usaremos tudo mais tarde, mas primeiro, dentro da nossa classe Window, precisamos de definir alguns campos para serem usados no nosso método DrawSnake() (e mais tarde também em outros métodos):
public partial class SnakeWPFSample : Window
{
const int SnakeSquareSize = 20;
private SolidColorBrush snakeBodyBrush = Brushes.Green;
private SolidColorBrush snakeHeadBrush = Brushes.YellowGreen;
private List<SnakePart> snakeParts = new List<SnakePart>();
......
Nós definimos dois SolidColorBrushs, um para o corpo e outro para a cabeça. Definimos também uma List<SnakePart> que guardará referências de todas as partes da serpente. Com isso, podemos agora implementar o nosso método DrawSnake():
private void DrawSnake()
{
foreach(SnakePart snakePart in snakeParts)
{
if(snakePart.UiElement == null)
{
snakePart.UiElement = new Rectangle()
{
Width = SnakeSquareSize,
Height = SnakeSquareSize,
Fill = (snakePart.IsHead ? snakeHeadBrush : snakeBodyBrush)
};
GameArea.Children.Add(snakePart.UiElement);
Canvas.SetTop(snakePart.UiElement, snakePart.Position.Y);
Canvas.SetLeft(snakePart.UiElement, snakePart.Position.X);
}
}
}
Como pode ver, este método não é particularmente complicado: iteramos a lista snakeParts e, para cada parte, verificamos se um UIElement foi especificado para esta parte - se não foi, nós o criamos (representado por um retângulo), o atribuímos à propriedade UiElement da instância SnakePart, e ao mesmo tempo o adicionamos à área de jogo. Observe como usamos a propriedade Position da instância SnakePart para posicionarmos o elemento dentro da Canvas denominada GameArea.
O truque aqui é, claro, que as partes da serpente serão definidas num outro lugar, permitindo-nos adicionar uma ou várias partes à serpente, atribuir-lhes a posição desejada, e depois fazer com que o método DrawSnake() execute todo o trabalho para nós. Nós faremos isso como parte do mesmo processo usado para mover a serpente.
Movendo a Serpente
Para alimentar o método DrawSnake() com algo, precisamos de popular a lista snakeParts. Esta lista serve constantemente como base para onde desenhar cada elemento da serpente, por isso também vamos usá-la para criar o movimento da serpente. O processo de mover a serpente consiste basicamente em adicionar um novo elemento a ela, na direção em que a serpente se encontra a mover, e depois apagar a última parte da serpente. Isto dará a sensação de estarmos realmente a mover cada elemento, mas, na verdade, estaremos apenas a adicionar novos elementos enquanto apagamos os antigos.
Portanto, vamos precisar de um método MoveSnake(), que eu vou mostrar dentro de instantes, mas primeiro, precisamos de adicionar mais alguns no topo da definição da nossa classe Window:
public partial class SnakeWPFSample : Window
{
const int SnakeSquareSize = 20;
private SolidColorBrush snakeBodyBrush = Brushes.Green;
private SolidColorBrush snakeHeadBrush = Brushes.YellowGreen;
private List<SnakePart> snakeParts = new List<SnakePart>();
public enum SnakeDirection { Left, Right, Up, Down };
private SnakeDirection snakeDirection = SnakeDirection.Right;
private int snakeLength;
......
Adicionamos uma nova enumeração com a denominação SnakeDirection, que à partida é autoexplicativa. Para o efeito, temos um campo privado para gravar a direção actual, (snakeDirection), e temos outra variável inteira para gravar o comprimento desejado da serpente (snakeLength). Com isso em voga, estamos prontos para implementar o método MoveSnake(). É um pouco longo, então eu adicionei comentários de linha a cada uma das partes importantes:
private void MoveSnake()
{
// Remove the last part of the snake, in preparation of the new part added below
while(snakeParts.Count >= snakeLength)
{
GameArea.Children.Remove(snakeParts[0].UiElement);
snakeParts.RemoveAt(0);
}
// Next up, we'll add a new element to the snake, which will be the (new) head
// Therefore, we mark all existing parts as non-head (body) elements and then
// we make sure that they use the body brush
foreach(SnakePart snakePart in snakeParts)
{
(snakePart.UiElement as Rectangle).Fill = snakeBodyBrush;
snakePart.IsHead = false;
}
// Determine in which direction to expand the snake, based on the current direction
SnakePart snakeHead = snakeParts[snakeParts.Count - 1];
double nextX = snakeHead.Position.X;
double nextY = snakeHead.Position.Y;
switch(snakeDirection)
{
case SnakeDirection.Left:
nextX -= SnakeSquareSize;
break;
case SnakeDirection.Right:
nextX += SnakeSquareSize;
break;
case SnakeDirection.Up:
nextY -= SnakeSquareSize;
break;
case SnakeDirection.Down:
nextY += SnakeSquareSize;
break;
}
// Now add the new head part to our list of snake parts...
snakeParts.Add(new SnakePart()
{
Position = new Point(nextX, nextY),
IsHead = true
});
//... and then have it drawn!
DrawSnake();
// We'll get to this later...
//DoCollisionCheck();
}
Com isso, agora temos toda a lógica necessária para se criar o movimento da serpente. Observe como usamos frequentemente a constante SnakeSquareSize em todos os aspectos do jogo, desde a renderização do padrão quadriculado do fundo à criação e atribuição à serpente.
Resumo
Desde o primeiro artigo, agora temos um fundo e, a partir deste artigo, temos o código para desenhar e mover a serpente. Mas, mesmo com esta lógica, ainda não há movimento real ou até mesmo uma serpente em si na área do jogo, porque nós ainda não chamamos nenhum destes métodos.
O call-to-action (apelo à ação) para o movimento da serpente deve vir de uma fonte repetida, porque a serpente deve estar constantemente em movimento enquanto o jogo estiver em execução - no WPF, temos a classe DispatcherTimer, que nos vai ajudar com isso. O movimento contínuo da serpente, usando um cronômetro, será o tema do próximo artigo.