Tại sao code lại ăn ram

Trước khi đi vào chi tiết hướng dẫn trò chơi, chúng ta sẽ xem qua sản phẩm cuối cùng mà chúng ta sẽ nhận được nhé. Ở đây mình có quay một video ở bước gần hoàn thiện sản phẩm. Nếu bạn để ý thì trong clip có bị một lỗi nhỏ. Tuy nhiên, nó đã được khắc phục ở sản phẩm cuối cùng.

Nếu bạn thấy hứng thú với sản phẩm này thì hãy tiếp tục đọc bài hướng dẫn nhé. À, tất nhiên là bạn có thể tiếp tục hoàn thiện nó rồi. Nhìn thì cũng ok đấy nhưng cần cải tiến nhiều cả về chức năng lẫn thiết kế. Hi vọng sau hướng dẫn này chúng ta sẽ có nhiều phiên bản nâng cấp, hay hơn so với bản gốc.

Kiến thức cần có

Để hoàn thành trò chơi rắn săn mồi này, chúng ta cần rất nhiều các kiến thức. Tuy nhiên bạn không cần bắt buộc phải nắm được các kiến thức dưới đây mà chỉ cần hoàn thành khóa học lập trình C/C++ là có thể lao vào chiến đấu được rồi. Khi gặp kiến thức mới các bạn cần tự tìm hiểu bổ sung nhé. Dưới đây là các kiến thức cần có mình liệt kê ra để các bạn chuẩn bị trước, hoặc khi dùng tới nó thì tìm hiểu:

  • Kiến thức lập trình C/C++ và tư duy lập trình cơ bản
  • Kiến thức lập trình đồ họa trong C, thư viện graphics.h/ winbgim.h (xem Chương 9 tài liệu lập Kỹ thuật lập trình C thầy Phạm Văn Ất)
  • Kỹ thuật đọc ghi file
  • Thao tác với bàn phím và chuột sử dụng ngôn ngữ C++
  • Các kiến thức còn lại tự học là chủ yếu (Cách điều khiển chuột, Phát nhạc trong C++,...)

Trừu tượng hóa, xác định các đối tượng của game

Để có thể triển khai code, trước tiên chúng ta cần phân tích trò chơi này, xem nó có những đối tượng nào. Mỗi đối tượng có những hành vi gì,...

Hệ tọa đồ của màn hình đồ họa

Bạn cần nắm được hệ tọa độ của màn hình đồ họa, bởi chúng ta làm việc với nó rất nhiều. Bạn muốn vẽ cái gì lên màn hình thì đều cần chỉ định tọa độ mà bạn cần vẽ.

Tại sao code lại ăn ram

Hướng của trục tọa độ trong màn hình Console, trục Oy có chiều hướng xuống dưới.

Phân tích đối tượng rắn trong game

Con rắn săn mồi của chúng ta sẽ là một chuỗi các hình tròn nhỏ (các đốt của con rắn) nối lại với nhau (số hình tròn nhỏ chính là độ dài của con rắn). Khởi tạo trò chơi, chúng ta có thể đặt độ dài ban đầu của nó là 3 chẳng hạn. Trong quá trình trò chơi diễn ra, ta phải lưu vết được tọa độ của từng hình tròn đó.

Tại sao code lại ăn ram

Đối tượng con rắn có thể mô phỏng là chuỗi các hình tròn nhỏ (các chấm xanh liên tiếp trong ảnh chính là đối tượng rắn của chúng ta).

Tại mỗi bước dịch chuyển của rắn, mỗi đốt thân của rắn sẽ dịch chuyển đi 1 đơn vị độ dài bằng nhau. Trong đó, đốt thân đầu tiên (đầu của rắn) sẽ tiến lên theo hướng dịch chuyển, các  đốt thân phía sau di chuyển đến vị trí cũ của đốt thân phía trước nó. Ví dụ:

Giả sử con rắn có 3 đốt và tọa độ của nó hiện tại là: x1(3,0) – đầu, x2(2,0) và x3(1,0) và đang đi theo hướng trục Ox. Bây giờ rắn đổi hướng di chuyển sang bên trái. Khi đó tọa độ mới của từng đốt là: x1(3,-1), x2 sẽ là tọa độ của x1 cũ (3, 0) và x3 chuyển sang vị trí của x2 cũ là (2, 0).

Trừu tượng hóa các đối tượng trong game

Như vậy, ta sẽ xây dựng một đối tượng Điểm. Đối tượng này giúp ta lưu được tọa độ của một điểm trên trục tọa độ 2 chiều Oxy. Ta có cấu trúc Điểm như sau:

struct Point {
    int x, y;
    int x0, y0;
};

Khi đó:

  • Một con rắn sẽ là 1 mảng các đối tượng Point tương ứng với các đốt của rắn. Tại mỗi đốt, cặp tọa độ (x,y) sẽ lưu vị trí đốt hiện tại và cặp tọa độ (x0,y0) sẽ lưu ví trí trước đó của đốt hiện tại để các đốt sau đó của con rắn có thể sử dụng.
  • Mỗi đối tượng thức ăn sẽ là 1 đối tượng Point. Ta chỉ cần sử dụng cặp biến (x, y) để lưu 1 đối tượng thức ăn. Tại một thời điểm trên màn hình chỉ có 1 thức ăn, và thức ăn đó xuất hiện ở 1 vị trí ngẫu nhiên bất kỳ.
  • Để rắn di chuyển được trên màn hình thì ta cần thêm một biến lưu hướng đi của nó. Ta sẽ tận dụng luôn đối tượng Point để xác định hướng theo tọa độ (x,y). Ví dụ nếu rắn đang di chuyển theo hướng trái sang phải, như vậy đối tượng direction của ta sẽ là (10, 0). Tức là ở mỗi bước đi, tọa độ x sẽ tăng thêm 10 đơn vị và tọa độ y không đổi. Khi ta thay đổi hướng đi, ta chỉ cần thay đổi giá trị (x, y) của đối tượng direction như hình dưới đây:

Tại sao code lại ăn ram

Mô phỏng hướng đi của rắn, tại mỗi bước đi ta sẽ cho Point đầu của rắn cộng thêm với Point hướng đi của nó. Các bạn lưu ý rằng trục Oy trong Console của chúng ta hướng xuống dưới nhé (Tức là y tăng khi xuống dưới và giảm khi đi lên trên).

Vậy rắn di chuyển như thế nào?

Giả sử rắn của chúng ta có chiều dài tối đa là 100 (chơi lên 100 chắc khủng lắm), ta sẽ khai báo mảng Point:

Point snake[100]; // con rắn của chúng ta
Point direction; // hướng đi hiện tại của rắn.
Point food; // đối tượng thức ăn
const int DIRECTION = 10; // khoảng cách di chuyển

Vậy để di chuyển con rắn, ta cần di chuyển đầu của con rắn trước tiên:

snake[0].x += direction.x;
snake[0].y += direction.y;

Nhưng trước đó ta phải lưu lại tọa độ cũ của nó để đốt kế sau nó lần theo:

snake[0].x0 = snake[0].x;
snake[0].y0 = snake[0].y;

Vậy tổng quát, để di chuyển toàn bộ phần thân còn lại của rắn ta sẽ chạy một vòng lặp hết phần thân rắn và thực hiện:

for (int i = 0;i < snakeLength;i++){
    # Nếu là đầu rắn
    if (i == 0){
        snake[0].x0 = snake[0].x;snake[0].y0 = snake[0].y;
        snake[0].x += direction.x;
        snake[0].y += direction.y;
    }
    else {
        snake[i].x0 = snake[i].x;snake[i].y0 = snake[i].y;
        snake[i].x = snake[i-1].x0;snake[i].y = snake[i-1].y0;
    }
}

Rắn sẽ ăn được thức ăn khi tọa độ đầu snake[0] trùng với tọa độ thức ăn food. Khi đó ta cần :

  1. Tăng chiều dài của rắn
  2. Khởi tạo ngẫu nhiên tọa độ thức ăn mới – Lưu ý không khởi tạo trùng vào các đốt thân của rắn.
  3. Tăng điểm số (game phải có điểm chứ)

Nếu đầu Rắn chạm tường hoặc chạm thân thì game kết thúc ( Chế độ chơi hiện đại) và chỉ chết khi chạm vào thân (chế độ chơi cổ điển)

Vậy còn lưu điểm cao thì sao?

Để tăng tính hấp dẫn cho game rắn săn mồi, ta tạo thêm một đối tượng HighScore lưu thông tin Tên người chơi  Số điểm đạt được nếu người chơi đó đạt điểm cao.

struct HighScore {
    int score;
    char name[30];
};

Thông tin điểm cao sẽ được ghi ra một file txt và lưu lại để so sánh với các người chơi sau đó. Nếu người chơi mới đạt điểm cao hơn trong danh sách đã lưu thì sẽ thay thế thành tích của người chơi cũ tương ứng bởi thành tích của người chơi mới.

Trên đây là toàn bộ phân tích bài toán, bây giờ mình sẽ cùng các bạn đi vào code.

Bắt tay vào xây dựng game

Mình sẽ không đi sâu vào giải thích từng dòng code cho các bạn được. Thay vào đó, mình chỉ cho các bạn các đối tượng của game rắn săn mồi này, các chức năng cần xây dựng của game và cách thức vận hành game. Thông qua đó bạn đọc cố gắng tìm hiểu thêm để hoàn thiện game dựa trên ý tưởng của mình. Các bạn cũng có thể tham khảo source code của mình.

Lưu ý là source code này mình viết từ hồi năm 2 đại học, thế cho nên nó không được sạch và dễ đọc cho lắm đâu. Nhưng bạn hiểu những gì mình nói ở trên thì khi đó đọc code sẽ đơn giản hơn nhiều.

Một số biến toàn cục cần thiết

Biến nào làm gì mình đều đã comment (chú thích) bên cạnh rồi nha.

int level; # level của game , đi nhanh hay đi chậm đó
bool endGame; # Lưu trạng thái của game: kết thúc hay chưa
int snakeLength; # Độ dài hiện tại của rắn
Point snake[100]; # Con rắn
Point direction; # Hướng đi
Point food; # Đồ ăn
const int DIRECTION = 10; # Khoảng cách của 1 lần dịch chuyển
HighScore  highscore[5]; # Lưu 5 người chơi có điểm cao nhất

Danh sách các hàm mình xây dựng cho trò chơi rắn săn mồi này:

void initGame ();
bool checkPoint ();
void drawPoint (int x,int y,int r);
void moveSnake ();
void drawSnake ();
void drawFood ();
void drawGame ();
void classic();
void modern();
void mainLoop (void (*gloop)());
void run ();
void changeDirecton (int x);
void showHighScore();
void getHighScore();
void checkHighScore(int score);
void initScore();
bool isEmpty();
void showText(int x,int y,char *str);
void showTextBackground(int x,int y,char *str,int color);

Các hàm và vai trò của nó

void initGame ();

  • Vẽ khung của trò chơi
  • Khởi tạo tọa độ ban đầu cho Snake (Gồm 3 Point) và Food
  • Khởi tạo hướng đi ban đầu
  • Vẽ màu backgound và các đối tượng hiển thị không thay đổi vị trí.

bool checkPoint ();

Hàm này có chức năng kiểm tra trùng lặp khi tạo mới đối tượng Food. Kiểm tra chắc chắn rằng tọa độ thức ăn không trùng với thân Rắn

void drawPoint (int x,int y,int r);

Hàm này có chức năng vẽ một đối tượng Point (hình tròn) lên màn hình đồ họa có tọa độ tâm (x,y) và có bán kính r

void drawSnake ();

Hàm này gọi làm hàm drawPoint () có các chức năng :

  • Vẽ đối tượng Snake lên màn hình đồ họa.
  • Xóa bỏ đốt cuối cùng của thân rắn mỗi khi rắn di chuyển được thêm một bước.

void drawFood ();

Hàm này gọi lại hàm drawPoint () và hàm checkPoint (); để thực hiện nhiệm vụ hiển thị thức ăn lên màn hình đồ họa.

void drawGame ();

Hàm này gọi lại hai hàm drawSnake() và hàm drawFood() và thực hiện thêm công việc hiển thị điểm của người chơi lên màn hình.

void classic();

Hàm này thực hiện chức năng chơi cổ điển – Người chơi có thể đi xuyên tường.
Chức năng của hàm:

  • Di chuyển Rắn theo điều khiển hướng đi
  • Nếu Rắn đi ra ngoài mép tường – cho rắn xuất hiện lại ở phía bên kia
  • Kiểm tra rắn ăn mồi : tăng chiều dài, điểm số và random lại tọa độ thức ăn
  • Kiểm tra game kết thúc

void modern();

Hàm này tương tự hàm classic() nhưng thay vào đó rắn không thể đi xuyên tường và đó là một trong những điều kiện bổ sung vào kiểm tra game kết thúc của hàm này.

void changeDirecton (int x);

x là biến kiểu nguyên lưu giữ giá trị ASCII tương ứng của của phím ở kiểu int. Đây là hàm thực thi việc nhận lệnh điều khiển hướng đi của rắn từ bàn phím, cụ thể là các phím mũi tên điều hướng và phím ESC. Hàm sẽ nhận vào mã phím nhập từ bàn phím và thực hiện thay đổi hướng đi theo mã phím tương ứng. Nếu người dùng nhấn phím ESC – Game sẽ kết thúc.

void mainLoop (void (*gloop)());

Hàm này chính là hàm lặp cho phép game chạy và thực thi toàn bộ các hành động trong khi game chạy
Tại đây, mình sử dụng đệ quy để hàm gọi lại chính nó. Tham số truyền vào là một trong hai hàm classic() hoặc hàm modern() tùy theo lựa chọn của người chơi. Hàm này còn có chức năng Pause/Resum game khi đang chơi nếu người dùng ấn phím SPACE.

bool isEmpty();

Hàm này kiểm tra file lưu điểm cao. Nếu file là rỗng thì trả về giá trị true.

void initScore();

Hàm này kiểm tra file lưu điểm cao có rỗng hay không bằng cách gọi hàm isEmpty(). Nếu file rỗng thì hàm sẽ khởi tạo 5 giá trị mặc định có tên PLAYER và điểm cao là 0. Nếu hàm có chứa thông tin hàm sẽ đọc thông tin điểm cao lưu vào mảng kiểu HighScore.

void showHighScore();

Hàm này có chức năng đọc thông tin của file điểm cao và xuất ra thông tin lên màn hình đồ họa khi được gọi.

void getHighScore ();

Hàm này có chức năng ghi lại thông tin điểm cao của người chơi + lưu vào file.

Hàm checkHighScore (int _score)

Hàm này thực hiện nhiệm vụ nhận điểm cao của người chơi, lấy thông tin tên người chơi nếu người chơi đó đạt thành tích cao. Hàm sẽ lưu giá trị vào mảng kiểu HighScore và gọi lại hàm getHighScore() để lưu lại thông tin.

Ngoài ta chương trình còn có một số hàm nhỏ thực hiện một số chức năng nhất định, bao gồm:

  • Thêm tính năng click chuột thay cho nhấn bàn phím.
  • Thêm nhạc cho game trở nên sống động

Cài đặt mã nguồn của mình như nào?

Phần này không hướng dẫn gì cả, chỉ hướng dẫn bạn nào thích chạy thử code của mình thôi.

Game rắn của mình có tính năng gì?

 Play/Pause trong khi chơi game

 Lưu điểm cao của người chơi ra tệp

 Điều khiển bằng chuột & bàn phím

 Thêm file setup trong Release

 Âm thanh sống động

Cách cài đặt

  • Nơi tải mã nguồn: https://github.com/nguyenvanhieuvn/Snake
  • IDE phát triển: Dev-C++ (Chưa kiểm tra với IDE khác nhé, nhưng miễn dùng được thư viện winbgim.h là chạy được à)
  • OS: Windows x64, chắc đa số dùng cái này rồi
  • Bản cài đặt, chạy luôn bạn có thể vào link mã nguồn trên và tải từ tab Release của dự án. Tải về cài như cài đặt các phần mềm thông thường. Mình tạo file cài bằng Visual Studio Deploy gì đó (lâu quá quên rồi).

Đầu tiên, hãy chắc rằng bạn đã thêm thư viện winbgim.h cho IDE của mình. Hướng dẫn thêm thư viện đồ họa cho Dev-C++, nếu bạn chưa cài thì xem ở đây. Còn với CodeBlocks thì xem ở đây.

Với Dev-C++:

  1. Mở project bằng cách vào: File -> Open -> Chọn file .dev trong thư mục source code của mình, sau đó mở nó lên.
  2. Đi tới Menu project > project options > parameters and type "-lwinmm" in the LINKER section. Cái này để Dev-C++ có thể play audio của game (Chỉ chơi được file .wav thôi nha). Cái này ở IDE khác bạn tìm giúp mình nhé, nó chỉ là một tham số compiler thôi nên chắc IDE nào cũng có thể thêm.
  3. Chạy thôi.

Tổng kết bài học

Mình nghĩ cái nội dung quan trọng nhất mà bạn có thể học qua bài viết này đó là cái tư duy lập trình. Hi vọng phần hướng dẫn của mình có thể giúp bạn hiểu được các trừu tượng hóa, đơn giản hóa và đưa một bài toán thực tế vào trong code của bạn như thế nào. Mình hi vọng các bạn có thể tiếp tục làm các tựa game tương tự như: Crazy Math, Tetris, Dò mìn, Caro, 2048, ... mà không cần phải đọc bất cứ một hướng dẫn nào.

Cuối cùng, nếu có thắc mắc đừng ngại để lại bình luận ở cuối bài viết. Mình hơi bận nhưng sẽ trả lời bạn khi có thể (Vui lòng không inbox facebook cá nhân mình nhé). Cảm ơn và chúc các bạn sớm đạt lương ngàn đô!

Tài liệu tham khảo

Dưới đây là một số tài liệu liên quan đến bài học sẽ hữu ích đối với bạn, giúp bạn tra cứu các kiến thức cần thiết.

[1]. Giáo trình Kỹ thuật lập trình C, thầy Phạm Văn Ất

[2]. Tài liệu, Hướng dẫn & Ví dụ về sử dụng chuột trong C++