This article has been localized into Vietnamese by the community.
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!
Tạo và di chuyển con rắn
Trong bài viết trước, chúng tôi đã tạo ra một khu vực đẹp để con rắn trong quá trình triển khai SnakeWPF của chúng tôi di chuyển xung quanh. Với vị trí đó, giờ là lúc tạo ra con rắn thực sự và sau đó khiến nó di chuyển xung quanh khu vực. Một lần nữa, chúng ta sẽ sử dụng lớp WPF Rectangle để tạo thành một con rắn có độ dài nhất định, với mỗi phần tử có cùng chiều rộng và chiều cao với hình vuông nền, hoặc như chúng ta gọi nó là: Hằng số SnakeSapesSize!
Tạo con rắn
Chúng ta sẽ vẽ con rắn bằng một phương thức gọi là DrawSnake() - phương thức này thực sự khá đơn giản, nhưng nó đòi hỏi khá nhiều thứ bổ sung, bao gồm một lớp mới gọi là SnakePart, cũng như một số trường bổ sung trên lớp Window. Hãy bắt đầu với lớp SnakePart, mà bạn thường định nghĩa trong một tệp mới (ví dụ SnakePart.cs):
using System.Windows;
namespace WpfTutorialSamples.Games
{
public class SnakePart
{
public UIElement UiElement { get; set; }
public Point Position { get; set; }
public bool IsHead { get; set; }
}
}
Lớp đơn giản này sẽ chứa thông tin về từng bộ phận của con rắn: Phần tử được đặt ở đâu trong khu vực trò chơi của chúng ta, mà UIElement bổ sung (hình chữ nhật(Rectangle), trong trường hợp của chúng ta) đại diện cho phần đó, và đây có phải là phần đầu của con rắn hay không? Chúng ta sẽ sử dụng tất cả sau này, nhưng trước tiên, bên trong lớp Window của chúng ta, chúng ta cần xác định một vài trường sẽ được sử dụng trong phương thức DrawSnake() (và sau đó trong các phương thức khác):
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>();
......
Chúng tôi xác định hai SolidColorBrush, một cho cơ thể và một cho đầu. Chúng tôi cũng xác định List<SnakePart>, sẽ giữ một tham chiếu đến tất cả các bộ phận của con rắn. Với điều đó, bây giờ chúng ta có thể thực hiện phương thức DrawSnake() của mình:
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);
}
}
}
Như bạn có thể thấy, phương pháp này không đặc biệt phức tạp: Chúng tôi lặp qua List snakeParts và đối với từng phần, chúng tôi kiểm tra xem UIElement bổ sung đã được chỉ định cho phần này chưa - nếu không, chúng tôi tạo nó (được biểu thị bằng Hình chữ nhật) và thêm nó vào khu vực trò chơi, trong khi lưu tham chiếu đến nó trên thuộc tính UiElement của phiên bản SnakePart. Lưu ý cách chúng tôi sử dụng thuộc tính Position của đối tượng SnakePart để định vị phần tử thực tế bên trong Canvas GameArea.
Thủ thuật ở đây tất nhiên là các bộ phận thực sự của con rắn sẽ được xác định ở nơi khác, cho phép chúng ta thêm một hoặc một số bộ phận vào con rắn, cho chúng vị trí mong muốn và sau đó phương thức DrawSnake() thực hiện công việc thực tế cho chúng ta. Chúng tôi sẽ làm điều đó như là một phần của quá trình tương tự được sử dụng để di chuyển con rắn.
Di chuyển con rắn
Để cung cấp một cái gì đó cho phương thức DrawSnake(), chúng ta cần điền vào danh sách snakeParts. Danh sách này liên tục đóng vai trò là cơ sở để vẽ từng phần tử của con rắn, vì vậy chúng tôi cũng sẽ sử dụng nó để tạo chuyển động cho con rắn. Quá trình di chuyển con rắn về cơ bản bao gồm thêm một yếu tố mới vào nó, theo hướng con rắn hiện đang di chuyển, sau đó xóa phần cuối cùng của con rắn. Điều này sẽ làm cho nó trông giống như chúng ta thực sự di chuyển từng yếu tố, nhưng thực tế, chúng ta chỉ thêm những cái mới trong khi xóa những cái cũ.
Vì vậy, chúng tôi sẽ cần một phương thức MoveSnake() mà tôi sẽ chỉ cho bạn chỉ sau một phút, nhưng trước tiên, chúng tôi cần thêm một số chi tiết vào đầu định nghĩa lớp Window của chúng tôi:
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;
......
Chúng tôi đã thêm một bảng liệt kê mới, được gọi là SnakeDirection. Chúng ta có một trường riêng để giữ hướng thực tế, hướng hiện tại (snakeDirection), và sau đó chúng ta có một biến số nguyên để giữ độ dài mong muốn của con rắn (snakeLength). Với điều đó, chúng tôi đã sẵn sàng để thực hiện phương thức MoveSnake(). Code hơi dài, vì vậy tôi đã thêm nhận xét nội tuyến cho từng phần quan trọng của nó:
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();
}
Với điều đó, giờ đây chúng ta có tất cả logic cần thiết để tạo ra chuyển động cho con rắn. Lưu ý cách chúng tôi liên tục sử dụng hằng số SnakeSquareSize trong tất cả các khía cạnh của trò chơi, từ việc vẽ mẫu bảng kiểm tra nền cho đến việc tạo và thêm vào con rắn.
Tổng kết
Từ bài viết đầu tiên, bây giờ chúng ta đã có một nền tảng và từ bài viết này, chúng ta có mã để vẽ và di chuyển con rắn. Nhưng ngay cả với logic này, vẫn không có chuyển động thực sự hoặc thậm chí là một con rắn thực sự trong khu vực trò chơi, bởi vì chúng tôi chưa gọi bất kỳ phương pháp nào trong số này.
Kêu gọi hành động cho sự di chuyển của con rắn phải đến từ một nguồn lặp lại, bởi vì con rắn phải liên tục di chuyển miễn là trò chơi đang chạy - trong WPF, chúng ta có lớp DispatcherTimer sẽ giúp chúng ta điều đó. Chuyển động liên tục của con rắn, sử dụng bộ đếm thời gian, sẽ là chủ đề cho bài viết tiếp theo.