Tự làm game Snake - Rắn ăn mồi với Arduino - Ví dụ về việc sử dụng thư viện XỬ LÝ BẤT ĐỒNG BỘ

I. Giới thiệu

Nếu là một người theo dõi cộng đồng Arduino Việt Nam trong thời gian dài, bạn sẽ để ý rằng, mảng Game là một mảng nhận đươc khá ít sự quan tâm vì độ khó của nó. Điển hình là chỉ có bài viết hướng dẫn làm game Flappy bird và Cá ăn mồi của bạn nguoimegame. Tuy nhiên, hôm nay, khi mình cảm thấy đã đủ lượng kiến thức và lượng thư viện nền tảng mình đã viết trước đó, mình sẽ hướng dẫn các bạn cách viết một game đơn giản với Arduino.

Hiển nhiên là game chơi trên nút bấm và màn hình LCD Nokia rồi. Sẽ hướng dẫn các bạn bung nó ra màn hình to hơn trong mục mở rộng.

II. Cần chuẩn bị kiến thức gì

Để đọc, làm được và quan trọng nhất là hiểu mình đang làm cái gì với đống code dưới đây, mình khuyến nghị các bạn nên tìm hiểu trước về thư viện xử lý bất đồng bộ của mình và quan trọng hơn cảkiến thức về lập trình hướng đối tượng căn bản bao gồm các phần: kế thừa, tầm vực, liên kết động và mẫu thiết kế singleton. Vì mục đích của bài này là hướng dẫn các bạn viết game và hoàn thiện game đơn giản nên mình sẽ lược bớt các phần quản lý bộ nhớ (cấp phát) để giúp các bạn tránh bị "rối" bởi phần khó này trong lúc tiếp thu một cách xử lý mới. Vì vậy, nếu muốn đi xa hơn nữa, bạn hãy chú ý đến quy tắc 3 ông lớn trong lập trình hướng đối tượng nhé heart.

1. Tổng quát về thư viện xử lý bất đồng bộ

Mình viết thư viện này với một mục đích duy nhất là hướng các bạn đến việc suy nghĩ theo hướng chu trình khi lập trình Arduino. Tức là, mọi chức năng của bạn khi làm ra đều có nhiều "quy trình cụ thể" và nó sẽ được thực hiện lại sau mỗi bao nhiêu mili giây do bạn đề nghị trước. Và trong thực tế, các chương trình thời gian thực rất cần chức năng này. Ví dụ như:

Khi bạn muốn làm một game thì bạn cần phải tương tác với người dùng trong thời gian thực nghĩa là khi người dùng nhấn nút bấm thì họ phải "thấy" nhân vật của mình nó du hí.

2. Hiện tượng lưu ảnh của mắt

Để làm được việc này, bạn cần phải quan tâm đến việc làm thế nào tạo ra chuyển động của nhân vật mình đang điều khiển. Để tạo ra chuyển động, bạn cần phải cập nhập hình ảnh liên tục để mắt con người nhận biết được chuyển động. Để mắt nhận biết được chuyển động, ta cần làm cho độ dịch giữa 2 ảnh là nhỏ nhất có thể, để mắt chúng ta cảm nhận được chuyển động do hiện tượng lưu ảnh của mắt. Theo như thực nghiệm cho thấy, để lừa mắt chúng ta tin rằng các vật thể đang chuyển động <=> ta để độ dịch (khoảng cách thời gian giữa 2 ảnh) bé hơn 40ms. Tức là, nếu ta cho ảnh luôn được cập nhập mới sau mỗi 40ms thì ta sẽ có được một chuyển động, và thêm nhiều ảnh hơn nữa ta sẽ có được một chuỗi chuyển động, và đó chính là GAME!

Như vậy, chúng ta đã thấy được một chu trình trong toàn bộ vấn đề làm game. Đó là chu trình cập nhập ảnh (frame), làm sao cho khi cập nhập ảnh cộng thời gian xử lý để cập nhập ảnh nó bé hơn 40ms là được. Mà thực ra, mấy game của chúng ta làm trên những cái màn hình bé tẻo teo thì vấn đề này không phải là vấn đề chính. Vấn đề chính, đó là: làm thế nào để cập nhập nó theo đúng quy trình trong khi còn rất nhiều vấn đề nữa!

Nhưng không sao, với thư viện bất đồng bộ của mình. Viết theo hướng xử lý vấn đề chu trình nên không có gì là không thể! Và thực tế là nó hoạt động rất tốt và tạo ra những hình ảnh chuyển động khá đẹp trên màn hình LCD Nokia 5110.

3. Hướng đối tượng

Để tiếp tục với bài viết, mình khuyên bạn nên tìm hiểu về các vấn đề trong lập trình hướng đối tượng, cụ thể như sau: tầm vựckế thừa, liên kết động và mẫu thiết kế singleton. Tuy nhiên, với mục đích khuyến khích các bạn phát triển lập trình hướng đối tượng trên Arduino, mình sẽ làm nói theo một ngôn ngữ gần tự nhiên nhất để mô tả các quá trình từ lúc hình thành ý tưởng cho đến code. Tuy nhiên, để biến nó thành những thiết kế kỹ thuật của riêng bạn, thì bạn phải xem kĩ 4 cái mình bôi đậm ở trên.

4. Tinh thần phân tích vấn đề theo hướng quy trình có thể lập nhau

Thư viện xử lý bất đồng bộ của mình được thiết kế theo hướng xử lý các quy trình độc lập, nghĩa là các quy trình này không cần quan trọng thứ tự với nhau. Nghĩa là quy trình này hoạt động trước quy trình kia sẽ vẫn không thay đổi đến kết quả. 

Hãy nhìn vấn đề theo một cách khác, đừng để các ràng buộc bắt bạn suy nghĩ những vấn đề phụ thuộc nhau. Và khi bạn suy nghĩ mọi vấn đề theo hướng độc lập với nhau thì bạn hoàn toàn có thể phân rã thuật toán ra và xử lý song song, và trong trường hợp của chúng ta Arduino chỉ có 1 nhân và 1 luồng thì chúng ta sẽ gọi là bất đồng bộ - nói nôm na hơn nữa là: các tiến trình song song trên một thiết bị đơn luồng, được ứng dụng trong các nhiệm vụ xử lý thời gian thực nhưng có có thuật toán tính toán không quá phức tạp, độc lập hoặc phụ thuộc với nhau.

III. Nền móng

Thực ra, mình đã từng làm mọi thứ theo kiểu vừa làm vừa độ thêm, và hầu hết các dự án hồi cấp 3 của mình đều như vậy. Tuy có vài dự án cũng đạt được những thành công nhất định, nhưng để đưa nó ra tiếp cận với thế giới và hơn thế nữa để hướng dẫn các bạn làm lại nó là cả một vấn đề nan giải. Bởi vì, nó là kết quả của bản vẽ thiết kế tồi và thợ xây chỉ thích làm đẹp và thêm thắc nhiều chức năng mà không hề nghĩ đến tương lai xa. Nói một cách khác, ở các dự án khởi động của mình hồi cấp 2, 3, mình chỉ quan tâm đến chức năng và khi làm thì rất chú trọng đến nó, mà không quan tâm nhiều đến nền tảng phía dưới. Vì vậy, mỗi khi code thêm một chức năng mới mà nó hao hao giống giống cái cũ thì mình phải copy code và paste lại broken heart. Nói chung là rất lộn xộn và mỗi khi lỗi của những đoạn nó thì phải sửa từng dòng sad. Rất chi là nản!

Nhưng trải qua những kinh nghiệm như vậy, mình cũng rút được một kinh nghiệm là: hãy luôn phát thảo trước ý tưởng, giới hạn vấn đề và lên mô hình trên giấy (nếu có thể). Vì vậy, nếu bạn thực sự muốn làm game rắn săn mồi, thì các bạn phải chấp nhận xây dựng nền móng thật kĩ cho mình. Không nên quá nóng vội làm xong game này và nhảy qua làm một game khác ngay lập tức. Hãy thuân thủ theo các bước: phát thảo, giới hạn thiết kế mô hình. Từ đó, bạn code theo là xong ngay. 

Thực ra, mình đặt nền móng, giới hạn và thiết kế mô hình chỉ mất tầm 1 tiếng, và khi code thì mất tổng cộng 6 tiếng để hoàn thành bộ game. Tất nhiên, là không kể khoảng thời gian mình ngồi test thư viện lcd từ bài flappy bird heart. Nó cũng mất thêm 1 giờ nữa để cái lcd (chuẩn bị kiến thức nền tảng rất quan trọng mà).

1. Phát triển ý tưởng

Ý tưởng của mình rất đơn giản: mình muốn có một cọn rắn với cái đầu và nhiều khúc mình, nhiệm vụ của nó là đi ăn các miếng thịt nằm vươn vãi trên bản đồ. Mình sẽ sử dụng 4 nút điều hướng lênxuốngtráiphải để điều hướng. Nếu con rắn cắn vào mình nó thì game sẽ kết thúc. Hệ thống game sẽ có những mức độ khác nhau để chơi.

Ý tưởng chỉ đơn giản vậy thôi, nào cũng đến bước giới hạn nào.

2. Giới hạn vấn đề

Chúng ta sẽ cùng nhau xác định các vấn đề CẦN PHẢI GIẢI QUYẾT. Bạn phải thống kê được bạn cần giải quyết vấn đề gì thì mới tìm tài liệu từ đó giải quyết nó. Những vấn đề khác có thể phát sinh thêm trong quá trình thực hiện dự án. Nhưng không có nghĩa, bạn nghĩ như vậy thì nó sẽ như vậy. Chính bước giới hạn này chính là các yêu cầu mà bạn đã xác định cụ thể. Từ đó bạn biết các rõ các ràng buộc mà mình phải thực hiện, như vậy sẽ tránh được tình trạng phát triển rối rắm, phát triển thêm những chức năng mới trong quá trình làm dự án. Nếu cứ vừa làm xong một chức năng trong dự án, bạn lại nãy ra một ý tưởng, xin đừng làm nó vội mà hãy lưu vào notepad, sau này khi đã hoàn thiện những cái vấn đề mà bạn đã giới hạn thì mới lôi ra làm tiếp.

a. Nhân vật là gì?

Nhân vật của chúng ta là một con rắn với:

  • 01 đầu
  • 19 mảnh mình (độ dài phần thân)

Con rắn có thể rẻ các hướng:

  • Lên 
  • Xuống
  • Trái
  • Phải

Yêu cầu đối với con rắn:

  • Nó phải chuyển động được.
  • Nó phải ăn thịt được.
  • Nó phải di chuyển với tốc độ mà mình thiết đặt trước.

b. Miếng thịt là gì?

Miếng thị là đồ ăn để con rắn ăn.

Các yêu cầu với miếng thịt:

  • Miếng thịt sẽ không di chuyển.
  • Số lượng miếng thịt có thể thay đổi được theo cài đặt của người dùng.
  • Miếng thịt sẽ chỉ xuất hiện ở những ô trống.
  • Mỗi khi con rắn ăn miếng thịt sẽ sẽ được một số điểm nhất địng

c. Nhận thao tác từ người chơi như thế nào?

Để nhận thao tác từ người, trong đó có:

  • 4 nút:
    • Lên: khi nhấn vào thì con rắn sẽ chuyển hướng lên trên màn hình. Cụ thể: đầu rắn đang ở vị trí (x, y), khi nhấn, nó sẽ chuyển tới vị trí (x, y - 1)
    • Xuống: khi nhấn vào thì con rắn sẽ chuyển hướng xuống dưới. Tương tự như trên, nó sẽ chuyển tới vị trí (x, y + 1)
    • Trái: tương tự (x - 1, y)
    • Phải: tương tự (x, y + 1)
  • 1 biến trở
    • Dùng để điều chỉnh tốc độ của con rắn

d. Chương trình gì sẽ quản lý các miếng thịt và con rắn tiếp nhận những lệnh yêu cầu của người chơi?

Để các miếng thịt, con rắn và thao tác của người chơi được xử lý một cách đơn giản và tập trung. Ta sẽ viết một lớp đối tượng nhằm để xử lý những vấn đề chung của các đối tượng này. Nhiệm vụ của lớp này bao gồm:

  • Hiển thị được con rắn và chuyển động của nó.
  • Hiển thị được miếng thịt và tạo miếng thịt mới khi nó bị ăn.
  • Tính điểm khi ăn thịt.
  • Kiểm tra game đang chạy hay người chơi đã bị GAME OVER!

Và chương trình này, mình sẽ gọi là Game Controller!

e. Kết luận

Vậy chúng ta có tổng cộng 4 vấn đề và các giới hạn đặt ra. Tất nhiên, mình đặt giới hạn nhỏ như vậy để dễ làm và cũng dễ truyền cảm hứng cho các bạn. Chứ ban đầu khó quá thì bài này sẽ chẳng đến được trong bookmark của bạn. Tiếp đến, chúng ta sẽ đến phần phác họa mô hình. Phần này mỗi người sẽ có những cách tiếp cận khác nhau, nhưng mình xin đề xuất cách mình tiếp cận, và theo mình nghĩ nó cũng tương đối dễ dàng với các bạn nếu các bạn chưa biết gì về lập trình hướng đối tượng. Tuy nhiên, nếu có thời gian, thì bạn nhớ google tìm hiểu ở các trang Việt Nam chuyên về lập trình C++ (OOP) 4 từ khóa sau:  tầm vựckế thừaliên kết động và mẫu thiết kế singleton.

3. Phác họa mô hình

a. Mô hình phần cứng

Như những gì mình đã đề xuất ở phần trên, chúng ta cần chuẩn bị các phần cứng sau:

LCD Nokia 5510

Chắc hẳn là các bạn đã biết sử dụng nút nhấn với biến trở rồi, còn LCD thì sao? Với những gì đã được trình bày ở bài Hướng dẫn làm game Flappy Bird, bạn sẽ biết được ngay thôi mà. Tuy nhiên, mình cũng xin trích dẫn lại một tí để các bạn test con LCD này.

Đây là thư viện được viết sẵn dành cho việc làm game trên arduino kết nối màn hình LCD download, ngoài ra các bạn phải tải thêm thư viện adafruit GFX, vì thư viện adafruit pcd8544 kế thừa thư viện này.

Bạn nối chân LCD Nokia 5510 với Arduino như hình sau:

Arduino LCD Nokia 5110
3 Serial clock out (CLK)
4 Serial data out (DIN)
5 Data/Command select (D/C)
6 Chip enable (CE)
7 LCD reset (RST)
5V VCC, BLK (có một điện trở 220 nối tiếp)
GND GND

Sau đó bạn chạy đoạn code sau đây để test màn hình

Sau khi test xong, màn hình LCD sẽ hiển thị nội dung là logo của adafruit và các bộ test đường kẻ, hình tròn và chuyển động trái tim như video của họ dưới đây.

Nút nhấn

Tiếp đến, mình sẽ lên kế hoạch nối 4 button lần lượt và các chân digital 10, 11, 12, 13 (lên, xuống, trái, phải). Mình sử dụng INPUT_PULLUP để cho nhanh. Vì vậy, mình sẽ vẽ mô hình và lắp mạch như sau. 

Biến trở

Mình sẽ sử dụn biến trở ở chân A0 như hình.

Trong hình, mình có sử dụng một điện trở 220ohm để hạn dòng đi qua biến trở => đoạn giá trị đọc được ở chân A0 sẽ là đoạn con của đọn 0 - 1023. Vì vậy, bạn cần đọc giá trị min - max của đoạn con này.

b. Phần mềm

Hình ảnh trên đây là sơ đồ UML (Unified Modeling Language) - sơ đồ mô hình hóa ký hiệu - một dạng ký hiệu đồ họa để giúp thiết kế các hệ thống hướng đối tượng một cách nhanh chóng. Ở đây, mình đã thiết kế khá nhiều lớp. Tuy nhiên, chúng ta hãy loại trừ lớp Timer, FallingButton, template<class T> Deque, WorkScheduler, kJob và kCalendar ra. Vì nó là nền tảng mình đã thiết kế để các bạn có thể xây dựng những loại game tương tự như thế này. Các bạn chỉ cần quan tâm đến các đối tượng Display, ObjectFallingButtonSnakeComponentMeatSnakeID  GameController

Nhiệm vụ của các đối tượng chính cần quan tâm
  1. Display: lớp singleton để thay thế cho biến toàn cục display thuộc kiểu Adafruit_PCD8544. Nhiệm vụ của nó là để ta giao tiếp với màn hình LCD. Giao tiếp lúc nào cũng được, miễn là có include thư viện "Display.h"
  2. Object: đối tượng đại diện cho các Object trên màn hình. Các đối tượng kế thừa từ lớp Object này, để có thể được xuất hiện trên màn hình LCD. Qua phước thức render.
  3. FallingButton: thư viện do mình viết để xác nhận một trạng thái falling (từ HIGH xuống LOW) của chân digital (trong trường hợp này là dùng để xác định trạng thái nhấn nút khi dùng INPUT_PULLUP)
  4. SnakeComponent: đối tượng kế thừa IS-A của lớp Object, nó là thành phần sẽ giúp chúng ta render được con rắn.
  5. Meat: đối tượng kế thừa IS-A của lớp Object, nó là thành phần giúp chúng ta xây dựng miếng thịt và render miếng thịt đó,...
  6. Snake: đối tượng kế thừa HAS-A của lớp SnakeComponent, nó sẽ giúp chúng ta quản lý các đối tương SnakeComponent, ví dụ như Snake thì có đầu, có mình và có đuôi, nhưng thay vì tạo mới các đối tương đầu, mình, đuôi thì mình dùng đối tượng SnakeComponent để cho nó nhanh. Thực ra, khi bạn thiết kế game thật thì bạn phải thiết kế các đối tượng đầu, đuôi và mình riêng, ví dụ như: SnakeHeadComponent, SnakeBodyComponent và SnakeTailComponent. Rồi dùng liên kết động trỏ đến, nhưng mình không thiết kế như thế mà sẽ để cho các bạn làm. Như vậy mới lên trình heart.
  7. ID: mối đối tương Object khi được tạo mới đều sẽ được gắn một địa chỉ ID để đảm bảo tính duy nhất của mọi đối tượng.
  8. GameController: Quản lý mọi đối tượng của game trừ cái display ra, mọi thứ gì liên quan tới game như rắn, thịt, điểm số, nút nhấn,... đều sẽ được lớp này quản lý! Như vậy sẽ tạo ra một đối tượng chịu trách nhiệm cho cả game. Sau này có vấn đề gì thì cứ lôi nó ra mà tra lỗi, tìm bugs và phát triển thêm các chức năng xung quanh như high score,... Hứng thú chưa nào... các bạn sẽ hoàn thiện nhé. Gợi ý để lưu dữ liệu khi bị mất điện trên Arduino, chúng ta dùng thư viện EEPROM.
  • : tầm vực private
  • : tầm vực protected
  • : tầm vực public
Các kiến thức cơ bản được tổng lược

Để đọc được sơ đồ trên, mình yêu cầu các bạn cần có sẵn 4 kiến thức về tầm vựckế thừaliên kết động và mẫu thiết kế hướng đối tượng singleton. Để chắc chắn các bạn đi tiếp được đến phần tiếp theo, mình xin nói một cách tổng quát dễ hiểu nhất để các bạn nắm được ý tưởng. Tuy nhiên, bạn cần phải đọc các bài liệu chuyên biệt về 4 từ khóa trên từ đó mới thấy được vấn đề. Và trong phạm vi này, mình chỉ đề cập đến duy nhất 4 vấn đề đó thôi, các vấn đề khác liên quan của hướng đối tượng xin các bạn vui lòng tìm hiểu thêm nhé heart.

  1. Tầm vực
    • Trong lập trình hướng đối tượng trên C++, chúng ta có 3 tầm vực lần lượt là public, protected và private.
    • Trong đó, ở tầm vực public, bạn có thể truy xuất mọi biến / hàm trong đối tượng mà không bị ngăn cản từ vị trí truy xuất. Thường được dùng cho các hàm cần được truy xuất thường xuyên từ bên ngoài. Thường thì mọi nguồn mở họ đều sẽ cung cấp cái này cho bạn.
    • Ở tầm vực protected, bạn chỉ có thể truy xuất các biến / hàm trong lớp đối tượng đó và lớp đối tượng kế thừa kiểu IS-A với đối tượng đó (xem thêm ở phần kế thừa). Thường được dùng để viết các hàm getter (dùng để truy xuất các biến dữ liệu kín- private trong tầm vực private) để các đối tượng con kế thừa có thể đọc được! Muốn sửa được giá trị thì viết setter (mình sẽ ví dụ ở các đối tượng phía dưới).
    • Còn ở tầm vự private, bạn chỉ có thể truy xuất các biến / hàm bên trong chính đối tượng đó, các đối tượng con không truy xuất được, cơ mà các đối tượng bạn bè lại truy xuất được broken heart. Vụ này để biết rõ hơn, bạn nên tìm từ khóa friend OOP.
  2. Kế thừa
    • Trong lập trình hướng đối tượng trong C++ có 2 kiểu kế thừa:
      • Đơn kế thừa: nhiều con cùng kế thừa 1 cha. Ví dụ như trên là đối tượng Object là cha, còn đối tượng Meat và Snake Component là con.
      • Đa kế thừa: 1 con lại có để nhiều cha (ca này khó angry), nên mình sẽ không nói về vấn đề này, vì nó còn nhiều vấn đề khác như vấn đề hình thoi,... các bạn muốn biết thêm thì cứ google nha cool. Cái này lại có 2 cái kế thừa con là IS-A và HAS-A. Cụ thể: Meat kế thừa trực tiếp từ Object thì gọi là kế thừa IS-A. Nôm na là Meat IS-A là Object. Còn Snake lại HAS-A SnakeComponent (con rắn có các thành phần)
    • Trong bài viết này, mình viết game cực kì đơn giản vì chỉ có 2 đối tượng nhân vật chính để biểu diễn là SnakeComponent và Meat nên chả có đa kế thừa gì cả.
    • Kế thừa được dùng để co các bạn viết code nhanh hơn, những phươn thức (hàm) chung sẽ được gợp lại để tránh phải ngồi code lại. Ví dụ như trong trường hợp này, cả SnakeComponent và Meat đều các điểm chung mà trong đối tượng Object mình đã liệt kê, cụ thể: vị trí của đối tượng, kích thước, phương thức render, phương thức move, phương thức lấy tọa độ, gán tọa độ mới,...
    • Để rõ hơn, các bạn nên google thêm các bài viết học thuật liên quan về kế thừa, cụ thể là đơn kế thừa trên C++ nhé. Vì một số ngôn ngữ bậc cao khác như Java và C# thì lại không có đa kế thừa hehe.
  3. Liên kết động
    • Khi làm việc với hướng đối tượng thì sẽ có những phương thức dùng chung. Cái này mình không biết nói như thế nào cho dễ hiểu nữa, vì mình cũng ít dùng nó trong bài viết này, vì mình giới hạn đối tượng sử dụng rất ít. Vì khó quá các bạn sẽ không đọc làm gì nữa mà chỉ nạp code mà chạy thôi.
    • Nhưng nói nôm na ứng dụng của liên kết động để phát triển bài này đó là:
      • Phát triển nhiều miếng thịt ngon hơn
      • Phát triển nhiều con rắn để chơi đa người chơi!
      • Phát triển nhiều thể loại rắn
      • Phát triển các dòng game khác dựa trên controller và thư viện bất đồng bộ!
  4. Singleton
    • Trước khi nói đến singleton, mình xin nói trước về các mẫu thiết kế hướng đối tượng có vai trò gì trong lập trình hướng đối tượng. Các mẫu thiết kế hướng đối tượng là các bài giải mẫu cho các vấn đề kinh điển trong lập trình hướng đối tượng. 
    • Singleton là cách giải được dùng để giải quyết vấn đề biến toàn cục. Trong lập trình C++, tối kỵ nhất là biến toàn cục, vì nó sẽ chiếm vùng nhớ trong vùng static. Như vậy, khi không dùng đến nó nữa thì ta vẫn phải trả một chi phí RAM để lưu trữ nó. Thật lãng phí! Nhưng khổ nỗi, có những thứ cứ đeo đuổi chúng ta trong suốt chương trình mà việc dùng một biết toàn cục để quán lý thực quá phức tạp. Vì một biến toàn cục chỉ có thể được gọi trong file .cpp hoặc sketch đó, chứ một file .cpp hay file .h sẽ "không dùng" được (nói vậy thôi chứ dùng được mà hơi lắc nhắc broken heart). Để giải quyết vấn đề đó, singleton ra đời, nó đơn thuần là tạo là một ánh xạ (instance) của một lớp đối tượng. Nói một các nôm na, nó là biến toàn cục, cụ thể hơn là lớp toàn cục. Tuy nhiên, nó lại được lưu ở vùng nhớ heap. Đã chưa nào? Vì sao thì các bạn xem code là thấy thui à hehe.

IV. Lập trình

Thực ra phần này mỗi người sẽ có một cách giải quyết khác nhau, có một cách tiếp cận khác nhau. Nếu bây giờ mình nói ra quy trình mình làm các bước như thế nào thì không hay tí nào, vì thực sự với những gì mình biết, mình nghĩ sẽ khiến các bạn đi vào lối mòn của mình từ đó giới hạn khả năng sáng tạo của bạn. Nhưng, nếu bạn muốn có một cơ sở ban đầu để từ đó phát triển hơn nữa thì mình xin trình bày một cách tổng lược những suy nghĩ của mình từ lúc nhìn vào bản thiết kế trên (phần cứngm phần mềm) và tiến hành code.

Mình sẽ chưa chia sẻ đoạn code ở mục này, mà chỉ gợi ý các bạn những điều mà theo mình nghĩ là vấn đề bạn có thể trải qua. Vì vậy, khi đã đọc hết, bạn có thể tham khảo ở mục V để chiến đấu!

0. Cách lưu trữ bitmap để render (vẽ) trên LCD

Phần này thực ra là do Adafruit họ đã làm rất tốt cho mình rồi nên chúng ta có thể dễ dàng phát triển lên. Nhưng nó cũng có một số vấn đề các bạn cần chúng ý để dễ dàng nắm vững được được LCD loại này.

Thống nhất kích thước bitmap dùng trong game

Trong game này, mình sẽ sử dụng các ô vuông 5x5 cho mỗi đối tượng, vì vậy màn hình sẽ có vài điểm chết không được sử dụng đến (48x84 là kích thước màn hình này). Nhưng không sao, bạn có thể dùng nó cho các chức năng khác tốt hơn.

Nhưng vấn đề nảy sinh ở chỗ, thư viện Adafruit chỉ render các bitmap là một bội số của 8, tức là có kích thước 8x8, 16x16 hoặc 32x32. Như vậy, ta sẽ phải giải quyết vấn đề này!

Thiết kế các đối tượng rắn và thịt

Với kích thước 5x5, mình sẽ sử dụng chương trình paint truyền thống để vẽ. Cụ thể, mình đã vẽ hình này  để làm đầu cho con rắn. Sau khi vẽ, các bạn sẽ dùng một hình vuôn có kích thước 8x8 ô hoặc 16x16 tùy thuộc vào kíc thước file bitmap của bạn để lưu trữ hình trên vào trong Arduino (Các bạn xem tại file config.h nhé). Khi chuyển file hình trên sang bitmap kích thước 8x8 ta sẽ được bảng sau.

1 1 1 0 0 0 0 0
1 1 0 1 0 0 0 0
1 0 0 0 1 0 0 0
1 1 0 1 0 0 0 0
1 1 1 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0

Phần mình bôi đen là phần ảnh bitmap 5x5, còn phần đỏ chính là các ô ảnh sẽ được tô màu khi in hình này ra. Thật đơn giản phải không nào, để lưu trữ bảng trên thì bạn chỉ việc lưu trữ 8 byte bằng cách thêm chữ phía trước, như B11100000, B11010000,... Xem file config.h bạn sẽ thấy ngay à.

Tương tự với phần thân và thịt con rắn, các bạn nhé!

1. Ban đầu phải xây dựng gì?

Đầu tiên, các bạn cần luôn nhớ rằng mà mình đã viết thư viện xử lý bất đồng bộ trước đó khá lâu, vì vậy khi mình tiếp cận bài toán này, mình sẽ không đi từ A - Z nữa mà mình đứng trên vai của mình để nhìn vào việc xử lý vấn đề game này - một vấn đề mà mình luôn chán nhất.

Với việc phát thảo mô hình trên, nếu các bạn để ý một tí thì đối tượng Object chính là đối tượng quan trọng nhất. Vì nếu không có nó thì sẽ không có SnakeComponent, Meat, Snake, GameController. Một đối tượng thần thánh như vậy thì phải được chú ý và xây dựng đầu tiên. Và nó cần được xây dựng một các kỹ càng. Và tất nhiên, với việc xây dựng sẵn khung sườn UML như trên, mình sẽ cài đặt các phương thức tương ứng.

Kiểu dữ liệu Pos, thực ra chỉ là một struct đơn giản gồm 2 byte để lưu vị trí x, y mà thôi!

Các bạn xem thêm cài đặt của đối tượng này tại file Object.cpp ở phần V nha heart.

2. Tiếp theo là con rắn

Phần hay nhất vẫn là con rắn, phải không nào?

Bây giờ, nếu không có sơ đồ phát thảo ở trên, bạn sẽ dễ lạc lỗi vào suy nghĩ: bây giờ chúng ta sẽ làm cơ rắn thế nào đây với rất nhiều câu trả lời. Như, cho con rắn kế thừa object, nghĩa là toàn bộ con rắn sẽ cho object kế thừa, lúc này nó sẽ quản lý toàn bộ phần thân con rắn với đầu, mình đuôi luôn. Nói chung là đóng trong lớp đó luôn, muốn phát triển lên thêm cũng khó, vì nó cụ thể quá rồi! Nhưng lại được một cái là tiết kiệm bộ nhớ vì chỉ cần sử dụng deque (hàng đợi 2 đầu) để quản lý các hướng của các bộ phậm mà thôi. Nhưng khổ cái, mình không thích sự cứng và thiết kế phương pháp sử dụng SnakeComponent và thiết kế Snake kế thừa HAS-A của nó.

Từ đó, mình sẽ quản lý Snake như một đói tượng riêng và có thể phát triển tùy thích cho sau này (Ví dụ như rắn 3 đầu heart, chuyên đi quét thịt nhưng lại dễ ăn thịt bản thân devil).

Các bạn có thể tham khảo phần cài đặt trong file SnakeComponent.h, SnakeComponent.cpp, Snake.hSnake.cpp.

3. Bây giờ là miếng thịt

Đặc điểm của miếng thịt là đứng yên. Nhưng trong đối tượng Object thì luôn phải cài đặt hàm move. Tức là miếng thịt phải luôn luôn chuyển động chăng? Nhưng không phải vậy, chúng ta chỉ cần cài đặt một hàm "rỗng" tức là chỉ khai báo cài đặt thôi nhưng không có một dòng mã nào trong đó cả. Lúc này thì Meat đã kế thừa được đối tượng Object rồi. Các bạn xem thêm ở Meat.hMeat.cpp nha.

4. Cuối cùng, GameController và ứng dụng thư viện xử lý bất đồng bộ

Như đã nói ở phần mô hình, GameController thực ra chỉ là một class để giúp chúng ta quản lý toàn bộ các đối tượng liên quan tới game con rắn. Vì vậy, đừng lo lắng, bạn hãy tự sáng tạo phần này, vì phần này mình cũng viết không được hay cho lắm và còn lãng phí bộ nhớ RAM. Tuy nhiên, mình cũng xin nói cách mà mình ứng dụng thư viện xử lý bất đồng bộ của mình.

Như các bạn đã biết, thư viện bất đồng bộ của mình sẽ xử lý các chu trình theo một chu kỳ nhất định. Nghĩa là khi cứ deadline (polling để kiểm tra - các bạn chuyên ngành có thể hiểu rõ hơn nha), thì các tiến trình sẽ được thực hiện. Như vậy, chúng ta cần thống kê các tiến trình cần có. Và tất nhiên, trong mô hình ở trên không nói rõ phần này, vì mỗi bạn sẽ có nhiều cách làm khác nhau, như bạn nguoimegame đã làm trong bài viết flappy bird của anh ấy!

Thay vì vẽ hình, mình sẽ dùng chữ vì vẽ có thể sẽ gây rối cho bạn trong trường hợp này.

Các chu trình:

  • Chu trình render các object. Sẽ được gọi sau mỗi 20ms. Thực ra chỉ cần < 40ms là được. Nhưng mình thích để như vậy để xem thư viện chạy ngọt không hehe.
  • Chu trình nhận các sự kiện nút bấm. Chu kỳ 10ms. Sẽ lưu lại trạng thái cuối cùng của nút bấm được nhấn sau đó gửi đến cho gamecontroller.
  • Chu trình cập nhập nút bấm và cập nhập vị trí của các đối tượng. Chu kỳ phụ thuộc vào biến trở.
  • Chu trình in ra số RAM. Chu kỳ 1500ms. Để mình chắc chắn số RAM còn bao nhiêu để dễ debug chứ hết RAM thì tự nhiên Arduino nó đơ mà không biết vì sao luôn đó.
  • Chu trình in ra số điểm. Chu kỳ 1500ms. Mình cũng chỉ để làm để test thư viện thôi :D, in ra số điểm cũng vui vui, nó sẽ in cùng lúc với số RAM luôn.

V. Thành quả

1. Lắp mạch

Bạn hãy đăng nhập và trở thành thành viên để sử dụng chức năng mục lục và quay lên mục III. 3. a. Phần cứng để lắp mạch nha hehe.

2. Phần mềm

Các bạn có thể download toàn bộ sketch của mình tại đây.

Hoặc sử dụng codebender tại.

Các bạn nhớ mở file config.h để thiết đặt các thông số nếu muốn nhé haha.

3. Video

VI. Lời kết

Hãy luôn tạo cho mình một tinh thần sáng tạo, một sản phẩm mới lạ đẹp mắt không thể ra đời trong ngày một ngày hai, mà có là cả một quá trình rèn luyện. Hãy làm theo 3 bước của mình nếu bạn không biết làm thế nào để thực hiện ý tưởng của mình. Nó sẽ giúp ích cho bạn rất nhiều đấy.

Những hình ảnh về dự án: 
lên
19 thành viên đã đánh giá bài viết này hữu ích.
Các dự án được truyền cảm hứng

Select any filter and click on Apply to see results

Các bài viết cùng tác giả

Điều khiển 8 đèn LED sáng theo ý muốn của bạn, dễ hay khó ?

Hôm nay, chúng ta sẽ học cách điều khiển 8 đèn LED. Vấn đề này, vừa dễ lại vừa khó, vậy nó dễ chỗ nào, khó chỗ nào, chúng ta cùng nhau tìm hiểu nhé!

Qua bài học này, bạn sẽ hiểu được cách làm thế nào để điều khiển nhiều led bằng cách sử dụng các chân digital, hoặc sử dụng IC HC595!

lên
123 thành viên đã đánh giá bài viết này hữu ích.

{} dấu ngoặc nhọn

Rất đơn giản, {} là một cặp dấu ngoặc nhọn, nên một khi bạn đã mở ngoặc thì phải đóng ngoặc lại cho nó!

Nhiệm vụ của nó là cung cấp một cú pháp để gọi những lệnh cho những cấu trúc đặc biệt như (if, while, for,...)

lên
4 thành viên đã đánh giá bài viết này hữu ích.