Nick Chung gửi vào
- 14861 lượt xem
Nói tới Game này thì ai cũng biết, là một trong số những Game của người Việt có tiếng vang lớn trong vài năm trước, cách chơi đơn giản,đồ họa 2D basic... vậy còn lập trình nó với ARDUINO thì sao nhỉ ?
FLAPPY BIRD
Vượt qua cái ngưỡng “đơn giản” đó đã khó , dành điểm số để đạt TOP thì còn hơn cả thử thách là những gì người chơi sẽ trải qua, chúng đã thực sự đem lại nhiều cung bậc cảm xúc cho người chơi.
May mắn thay, việc lập trình Game này còn dễ hơn cả chơi nó. Hãy cùng mình bắt đầu nhé.
Phần cứng
Tải về thư viện đồ họa
Bạn hãy tải về thư viện tại bài viết
ST7565 | Hướng dẫn sử dụng glcd ST7565 homephone và chia sẻ thư viện
Chuẩn bị phần cứng
- Arduino chip atm328 trở lên, mình sử dụng arduino uno r3
- Lcd st7565 128x64 homephone spi
- Nút bấm: 6 nút
Nối mạch
Bạn hãy tham khảo cách nối mạch và nút bấm tại bài viết giới thiệu lcd nhé:
Quản lí đối tượng
Bạn có thể cần đọc các bài viết về quản lí đối tượng Game :
- Phần 1: Hiệu ứng đồ họa Game trên lcd
- Phần 2: Chuyển động và lập trình cơ bản
- Phần 3: Quản lí các đối tượng Game
- Phần 4: Xử lí va chạm của của các đối tượng Game
Chương trình viết theo phong cách hướng đối tượng.
Chỉ có 2 loại đối tượng cần quan tâm.
Đối tượng
|
Chức năng, nhiệm vụ
|
Thuộc tính hình học
|
Thuộc tính khác.
|
Chú chim
|
Người dùng nhấn chạm vào màn hình ( với Project này là nhấn vào nút ấn), điều khiển chim bay lên, vượt qua các chướng ngại vật.
|
Tọa độ : X,Y
|
Sự tồn tại.: Sống/Chết.
Hướng : Bay lên/ bay xuống.
|
Ống nước
|
Là các chướng ngại vật, xuất hiện cả ở trên và dưới màn hình, chú chim bay và vượt qua các khe hở tạo bởi 2 ống nước.
|
Tọa độ: X,Y.
|
Sự tồn tại: Còn hoặc không còn ảnh hưởng tới chú chim.
Hướng: di chuyển từ phải sang trái.
|
Sự tồn tại
Như đã biết để điều khiển các ống nước và chú chim (các đối tượng) ta cần phải tìm cách quản lí chúng, bằng cách sử dụng bảng thống kê( biến X,Y,…) hoặc thực thể hóa thành với các thuộc tính gần gũi (hướng, tồn tại, kích thước, vị trí.). Mỗi đối tượng luôn mang theo những thuộc tính của riêng nó, việc tạo ra các đối tượng sẽ đi cùng với việc sử dụng thêm tài nguyên trên lưu trữ của ARDUINO (máy tính), cụ thể ở đây là tăng thêm các biến giá trị để lưu và tính toán trên chúng.
Trên hành trình của chú chim, chú phải bay qua rất nhiều ống nước rồi ghi điểm mỗi khi vượt qua, ngoài đời thực, ngoài đời thực, nếu như có n ống nước, thì đó đương nhiên là n đối tượng , mỗi đối tượng cần 1 trang giấy để ghi lại thông tin thì ta cần n trang giấy cho từng đó đối tượng.
Trong lập trình Game, ta cần cụ thể hơn khi xem xét chúng:
Đối tượng cần quan tâm.
|
Đối tượng không cần quan tâm.
|
Chú chim .
|
Với những chiếc ống ống nước mà chim đã bay qua .
|
Chiếc ống nước gần chim nhất.
|
Với những ống nước ở quá xa so với chim.
|
Những gì cần vẽ lên màn hình.
|
Những đối tượng bị gắn mác là “ không tồn tại”
|
Đó là ý nghĩa của “sự tồn tại”, nó giúp ta đơn giản hóa và tiết kiệm dữ liệu nhớ khi chỉ quan tâm quản lí đến các đối tượng "gần gũi" nhất.
Đơn giản hóa vấn đề:
- Hình dáng chi tiết của ống nước được phác họa bởi một hình chữ nhật đơn giản dạng ống .
- Hệ quan sát của ta (Camera) gắn với hệ của chú chim, do đó , khi biểu diễn quá trình di chuyển của chim , đối tượng này luôn giữ một hoành độ cố định, trong khi đó những chiếc ống nước sẽ di chuyển từ phải qua trái, tạo ra chuyển động tương đối giữa chim và cột.
- Sẽ không phải là n chiếc ống di chuyển, mà sẽ là chú chim vượt qua chướng ngại vật n lần.
Khi lập trình, điều này có nghĩa là chỉ có một số ít ống nước cần quan tâm, cụ thể 6 ống nước di chuyển cách đều. Sau mỗi chu kì di chuyển (đi hết từ phải qua trái), chúng lại trở về vị trí xuất phát rồi lại di chuyển nhưng với độ cao thay đổi. Chúng ta dùng hàm Random() để khởi tạo giá trị độ cao một cách ngẫu nhiên tạo ra những chiếc ống khác nhau trên hành trình. Đây là ý tưởng để giải quyết vấn đề quản lí đối tượng như đã nêu.
Phần ý tưởng đã khá chi tiết, dù sao cũng chỉ là ý tưởng của mình mà thui. Bạn sẽ hiểu kĩ hơn khi đọc các đoạn Code bên dưới.
Class cơ sở
Chứa các thuộc tính chung nhất và phương thức truy cập (đọc/ghi) .
Các thuộc tính chung của các đối tượng Game (chim/ ống nước) bao gồm:
- Vị trí : tọa độ x,y
- Sự tồn tại.
- Hướng.(đang di chuyển theo hướng nào)
Class “cột”
Ý mình “cột” là các cột ống nước.
Mỗi cột là một bộ ống nước xếp trên dưới . (Tuy vẽ là 2 ống nước nhưng chỉ quy định là 1 đối tượng mà thôi).
Phương thức
- Vẽ/xóa cột: vẽ và xóa hình ảnh trên màn hình.
- Tạo cột: đặt một độ cao Random() khi nó quay về điểm xuất phát.
- Tạo một biến có tên “met” (mét) để đo quãng đường di chuyển của các ống nước, chúng ta dãn khoảng cách và xét tồn tại dựa vào quãng đường “met” này.
- Gọi Serial.println(met); // để kiểm tra mét
- Gọi Serial.print(chiec_cot[i].get_ton_tai()); // để kiểm tra sự tồn tại của các đối tượng
Code tạo 3 cột di chuyển auto trên màn hình
#include "ST7565_homephone.h" // add a bitmap library of a 16x16 fruit icon #include "bmps.h" ST7565 lcd(3, 4, 5, 6); //cài đặt chân input #define fight_b A5 /* #define select_b A4 #define right_b A3 #define up_b A2 #define left_b A1 #define down_b A0*/ #ifdef __AVR__ #include <avr/io.h> #include <avr/pgmspace.h> #endif unsigned int met; // quãng đường đi được // Chương trình chạy 1 lần void setup() { Serial.begin(9600); lcd.ON(); lcd.SET(23, 0, 0, 0, 4); pinMode(fight_b, INPUT_PULLUP); lcd.clear(); } class object { // lớp cơ sở public: // biến toàn cục (dùng ở mọi nơi) byte Xmin = 0, Ymin = 0, Xmax = 125, Ymax = 63; //biên byte cot_max = 3; // số lượng cột trên màn hình byte do_rong = 21; //khoảng cách 2 cột trên và dưới // tập hàm ghi thông tin void set_ton_tai(boolean value) { ton_tai = value; } void set_x(byte value) { x = value; } void set_y(byte value) { y = value; } void set_huong(byte value) { huong = value; } //tập hàm lấy thông tin boolean get_ton_tai() { return ton_tai; } byte get_x() { return x; } byte get_y() { return y; } byte get_huong() { return huong; } private: // chỉ 2 tập hàm thành viên viên trên mới sử đụng được 4 tham số này: boolean ton_tai; byte x; byte y; byte huong; }; /* class bird: public object{//kế thừa lớp object //........... };//class */ class cot : public object { // kế thùa lớp object public: void ve_cot(byte x, byte y, byte do_rong) { //do_rong là khoảng cách của 2 cột trên-dưới lcd.rect(x, Ymin, 7, y - Ymin, BLACK); //vẽ cột trên lcd.rect(x, y + do_rong, 7, Ymax - y, BLACK); //vẽ cột dưới lcd.display(); } void xoa_cot(byte x, byte y, byte do_rong) { lcd.rect(x, Ymin, 7, y - Ymin, WHITE); lcd.rect(x, y + do_rong, 7, Ymax - y, WHITE); } cot() { // tạo đối tượng bằng constructor } void tao_cot() { //tạo random với tham số truyền vào là đồng hồ hệ thống unsigned int seed; seed = millis() % 1000; randomSeed(seed); byte y_random; y_random = random((Ymin + 10), (Ymax - 10)); //lưu thông tin cho cột "đó" set_x(Xmax); //hoành độ ban đầu set_y(y_random); //tung độ đầu set_ton_tai(1); //cấp tồn tại } void di_chuyen_cot() { // chính cột đó //vẫn tồn tại //lấy thông tin byte x_cu = get_x(), y_cu = get_y(), x_moi; //tọa độ cũ-mới // dịch sang bên trái x_moi = x_cu - 1; //xóa hình cũ xoa_cot(x_cu, y_cu, do_rong); //vẽ hình mới ve_cot(x_moi, y_cu, do_rong); //lưu thông tin set_x(x_moi); if (get_x() == 250) { // nếu cột đi đến biên trái màn hình //thì không cho cột chạy nữa // x nhỏ nhất là 0, tuy nhiên nếu tiếp tục trừ x thì nó sẽ đém từ 254 ->0 // đây là hiện tượng tràn số set_ton_tai(0); // tước quyền tồn tại } } //di chuyen cot }; //class object data; // lấy biến toàn cục cot chiec_cot[3]; //tạo cột void loop() { for (byte i = 0; i < data.cot_max; i++) { if ((chiec_cot[i].get_ton_tai() == 0)) { if (met >= (127 / data.cot_max)) { //mõi cột được tạo khi đi được 1 đoạn cố định:127/(data.cot_max), //xắp sếp đều trên màn hình chiec_cot[i].tao_cot(); met = 0; // dat lai } } else { chiec_cot[i].di_chuyen_cot(); } Serial.print(chiec_cot[i].get_ton_tai()); } delay(10); Serial.println(met); met++; }
Kiểm tra một vài thông số
OK, thuật toán của chúng ta có tính tổng quát, ta có thể tùy chỉnh số lượng ống nước.
Ví dụ trên Cot_max = 3, ở đây mình cài Cot_max = 10 xem như thế nào ^^.
Đến dòng 37 sửa Cot_max=10; Đến dòng 124 sửa khởi tạo đối tượng bằng 10 ;
Kết quả
Có vẻ ARDUINO của chúng ta đã hơi Lag? Điều này có thể dễ hiểu vì nó phải xử lý 10 đối tượng cùng lúc !
Không sao, đó chỉ là vấn đề cấu hình máy chơi Game mà thôi.
Chúng ta có thể tùy chỉnh tốc độ chuyển khung hình bằng cách sửa lại giá trị của Delay();
Xem Serial có gì nào :
Class “bird”
Cũng giống như class “cot”, class”bird” sẽ kế thừa từ class cơ sở những thuộc tính chung nhất.
Các phương thức
- Vẽ / xóa ảnh bitmap.
- chim_bay(): dựa vào các thông số cũ về hướng và tọa độ để cho chim bay.
- dk_chim(): nhấn nút ấn để điều khiển hướng ( đặt hướng cho phương thức chim_bay())
- “Sự tồn tại” của chim là thuộc tính giúp ta kiểm soát tiếp tục Game / đặt “GAME OVER”.
Chim bay lên :
Chim bay xuống:
Code
#include "ST7565_homephone.h" ST7565 lcd(3, 4, 5, 6); //cài đặt chân input #define fight_b A5 // Chương trình chạy 1 lần void setup() { lcd.ON(); lcd.SET(21, 0, 0, 0, 4); pinMode(fight_b, INPUT_PULLUP); } #ifdef __AVR__ #include <avr/io.h> #include <avr/pgmspace.h> #endif const static unsigned char __attribute__((progmem)) bird_up_15x8[] = { 0x30, 0x58, 0x5E, 0xFB, 0x81, 0x85, 0x9B, 0x91, 0x91, 0x7D, 0x71, 0x7E, 0x60, 0x60, 0x60, }; const static unsigned char __attribute__((progmem)) bird_down_15x8[] = { 0x1E, 0x32, 0x22, 0x6E, 0x99, 0x85, 0x9B, 0x91, 0x91, 0x7D, 0x71, 0x7E, 0x60, 0x60, 0x60, }; class object { // lớp cơ sở public: // biến toàn cục (dùng ở mọi nơi) byte Xmin = 0, Ymin = 0, Xmax = 125, Ymax = 63; //biên // tập hàm ghi thông tin void set_ton_tai(boolean value) { ton_tai = value; } void set_x(byte value) { x = value; } void set_y(byte value) { y = value; } void set_huong(byte value) { huong = value; } //tập hàm lấy thông tin boolean get_ton_tai() { return ton_tai; } byte get_x() { return x; } byte get_y() { return y; } byte get_huong() { return huong; } private: // chỉ 2 tập hàm thành viên viên trên mới sử đụng được 4 tham số này: boolean ton_tai; byte x; byte y; byte huong; }; class bird : public object { //kế thừa lớp object public: bird() { // hàm tạo constructor set_x((Xmax - Xmin) / 3); //hoành độ ban đàu set_y(Ymin + 20); // tung độ ban đàu set_huong(4); // hướng ban đầu(xuống) } //vẽ chim void ve_chim(byte x, byte y, byte huong) { if (huong == 2) { //hướng lên lcd.bitmap(x, y, 15, 8, bird_up_15x8, BLACK); } if (huong == 4) { //hướng xuống lcd.bitmap(x, y, 15, 8, bird_down_15x8, BLACK); //xoay ảnh 270 độ } lcd.display(); } // void xoa_chim(byte x, byte y) { lcd.fillrect(x, y, 15, 8, DELETE); lcd.display(); } // void chim_bay() { //lấy thông tiin byte x_cu = get_x(), y_cu = get_y(), huong_cu = get_huong(); byte x_moi, y_moi, huong_moi; if (huong_cu == 2) { //bay lên y_moi = y_cu - 4; } if (huong_cu == 4) { //bay xuống y_moi = y_cu + 2; } //xóa ảnh cũ: xoa_chim(x_cu, y_cu); //vẽ ảnh mới ve_chim(x_cu, y_moi, huong_cu); //lưu thông tin set_y(y_moi); } void dk_chim() { //điều khiển chim byte bay_len; bay_len = digitalRead(fight_b); // đọc giá trị button if (bay_len == 0) { //nhấn nút fight //bay lên //ghi thông tin set_huong(2); } if (bay_len == 1) { //ko nhấn //bay lên //ghi thông tin set_huong(4); } //cho chim bay chim_bay(); } }; //class bird bird con_chim; // tạo thực thể con_chim void loop() { con_chim.dk_chim(); delay(200); }
TEST VA CHẠM VÀ HOÀN THIỆN GAME
Bước cuối là xét va chạm.
Vì có chúng ta có ít đối tượng ống nước (từ 3 -> 10 cột tùy bạn) nên chúng ta sẽ xét cả thảy tọa độ và độ cao của tất cả các ống nước với chim.
Copy đoạn code này và chơi thôi !
//GAME FLAPPY BIRD // Viết bởi: Thái Sơn , 6/7/2016 // Chia sẻ tại arduino.vn - 3/2/2017 #include "ST7565_homephone.h" // add a bitmap library of a 16x16 fruit icon #include "bmps.h" #ifdef __AVR__ #include <avr/io.h> #include <avr/pgmspace.h> #endif ST7565 lcd(3, 4, 5, 6); //cài đặt chân input #define fight_b A5 /* #define select_b A4 #define right_b A3 #define up_b A2 #define left_b A1 #define down_b A0*/ const static unsigned char __attribute__((progmem)) bird_up_15x8[] = { 0x30, 0x58, 0x5E, 0xFB, 0x81, 0x85, 0x9B, 0x91, 0x91, 0x7D, 0x71, 0x7E, 0x60, 0x60, 0x60, }; const static unsigned char __attribute__((progmem)) bird_down_15x8[] = { 0x1E, 0x32, 0x22, 0x6E, 0x99, 0x85, 0x9B, 0x91, 0x91, 0x7D, 0x71, 0x7E, 0x60, 0x60, 0x60, }; unsigned int met; // quãng đường đi được // Chương trình chạy 1 lần void setup() { lcd.ON(); lcd.SET(23, 0, 0, 0, 4); pinMode(fight_b, INPUT_PULLUP); lcd.clear(); start_game(); } //================================================= class object { // lớp cơ sở public: // biến toàn cục (dùng ở mọi nơi) byte Xmin = 0, Ymin = 0, Xmax = 125, Ymax = 63; //biên byte cot_max = 4; // số lượng cột trên màn hình byte do_rong = 28; //khoảng cách 2 cột trên và dưới static unsigned int diem, diem_cao; // biến tĩnh // tập hàm ghi thông tin void set_ton_tai(boolean value) { ton_tai = value; } void set_x(byte value) { x = value; } void set_y(byte value) { y = value; } void set_huong(byte value) { huong = value; } //tập hàm lấy thông tin boolean get_ton_tai() { return ton_tai; } byte get_x() { return x; } byte get_y() { return y; } byte get_huong() { return huong; } private: // chỉ 2 tập hàm thành viên viên trên mới sử đụng được 4 tham số này: boolean ton_tai; byte x; byte y; byte huong; }; unsigned int object::diem = 0; unsigned int object::diem_cao = 0; //================================================= class bird : public object { //kế thừa lớp object public: bird() { // hàm tạo constructor tao_chim(); } void tao_chim() { set_x((Xmax - Xmin) / 3); //hoành độ ban đàu set_y(Ymin + 20); // tung độ ban đàu set_huong(4); // hướng ban đầu(xuống) set_ton_tai(1); } //vẽ chim void ve_chim(byte x, byte y, byte huong) { if (huong == 2) { lcd.bitmap(x, y, 15, 8, bird_up_15x8, BLACK); } if (huong == 4) { lcd.bitmap(x, y, 15, 8, bird_down_15x8, BLACK); } lcd.display(); } // void xoa_chim(byte x, byte y) { lcd.fillrect(x, y, 15, 8, DELETE); lcd.display(); } // void chim_bay() { if (get_ton_tai() == 0) { tao_chim(); } else { //lấy thông tiin byte x_cu = get_x(), y_cu = get_y(), huong_cu = get_huong(); byte x_moi, y_moi, huong_moi; if (huong_cu == 2) { y_moi = y_cu - 4; } // bay lên if (huong_cu == 4) { y_moi = y_cu + 2; } //bay xuống xoa_chim(x_cu, y_cu); //xóa ảnh cũ: ve_chim(x_cu, y_moi, huong_cu); //vẽ ảnh mới set_y(y_moi); //lưu thông tin } } void dk_chim() { //điều khiển chim byte bay_len; bay_len = digitalRead(fight_b); // đọc giá trị button if (bay_len == 0) { set_huong(2); } if (bay_len == 1) { set_huong(4); } chim_bay(); //cho chim bay } }; //class //================================================ class cot : public object { // kế thùa lớp object public: void ve_cot(byte x, byte y, byte do_rong) { //do_rong là khoảng cách của 2 cột trên-dưới lcd.rect(x, Ymin, 7, y - Ymin, BLACK); //vẽ cột trên lcd.rect(x, y + do_rong, 7, Ymax - y, BLACK); //vẽ cột dưới lcd.display(); } void xoa_cot(byte x, byte y, byte do_rong) { lcd.rect(x, Ymin, 7, y - Ymin, DELETE); lcd.rect(x, y + do_rong, 7, Ymax - y, DELETE); lcd.display(); } cot() { // tạo đối tượng bằng constructor } void tao_cot() { //tạo random với tham số truyền vào là đồng hồ hệ thống unsigned int seed; seed = millis() % 1000; randomSeed(seed); byte y_random; y_random = random((Ymin + 10), (Ymax - 10)); //lưu thông tin cho cột "đó" set_x(Xmax); //hoành độ ban đầu set_y(y_random); //tung độ đầu set_ton_tai(1); //cấp tồn tại } void di_chuyen_cot() { // chính cột đó //lấy thông tin byte x_cu = get_x(), y_cu = get_y(), x_moi; //tọa độ cũ-mới // dịch sang bên trái x_moi = x_cu - 1; xoa_cot(x_cu, y_cu, do_rong); ve_cot(x_moi, y_cu, do_rong); set_x(x_moi); if (get_x() == 250) { set_ton_tai(0); // tước quyền tồn tại } } //di chuyen cot boolean va_cham(object& chim) { //hàm kiểm tra chạm của cột i với chim //lấy thông tin int hieu_x, hieu_y; hieu_x = chim.get_x() - get_x(); // x chim trừ x cột i, 12 là độ rộng ảnh bitmap hieu_y = chim.get_y() - get_y(); // y chim trừ y cột i // và nếu chim không rơi xuống đất thì cũng ko có va chạm if ((hieu_x >= (-12)) && (hieu_x <= 7)) { // chim đang bay trong cột i //(12 là độ rộng ảnh bitmap, 7 là đường kính cột) if ((hieu_y > 0) && (hieu_y < (do_rong - 8))) { // chim đang bay trong vùng không có cột return 0; // không va trạm } else { return 1; // trả về có } } else { // chim không bay trong cột i return 0; // không va chạm } } // va cham }; //class //====================================================== class computer : public object { // lớp này để tính toán public: void in_diem() { if (diem >= diem_cao) { diem_cao = diem; } lcd.number_ulong(Xmin + 5, 0, diem, ASCII_NUMBER, BLACK); lcd.display(); lcd.number_ulong(Xmax - 15, 0, diem_cao, ASCII_NUMBER, BLACK); lcd.display(); } void lap_vo_han() { // dừng dòng chảy chính bằng vòng lặp vô hạn //thoát lặp khi nút Fight được nhấn int y = 55; lcd.Asc_String(10, y + 2, Asc("Fight!"), BLACK); lcd.display(); while (digitalRead(fight_b) != 0) { lcd.rect(8, 55, 40, 10, BLACK); lcd.display(); if (digitalRead(fight_b) == 0) { break; // thoát ngay } delay(250); lcd.rect(8, y, 40, 10, WHITE); lcd.display(); if (digitalRead(fight_b) == 0) { break; // thoát ngay } delay(250); } } //đóng lặp vô hạn void gameover() { //reset điểm diem = 0; // chờ nhấn nut fight lap_vo_han(); //reset màn hình lcd.clear(); } }; //============================================ computer co; bird con_chim; // tạo thực thể con_chim cot chiec_cot[4]; //tạo cột object data; // lấy biến toàn cục void loop() { //di chuyển chim con_chim.dk_chim(); //di chuyển cột for (byte i = 0; i < data.cot_max; i++) { if ((chiec_cot[i].get_ton_tai() == 0)) { if (met >= (127 / data.cot_max)) { //mõi cột được tạo khi đi được 1 đoạn cố định:127/(data.cot_max), xắp sếp đều trên màn hình chiec_cot[i].tao_cot(); met = 0; // dat lai } } else { chiec_cot[i].di_chuyen_cot(); } // mở rộng hàm for ////////////va chạm và tính điểm////////////// //khi hoành độ của chim == hoành độ cột i, cộng điểm 1 điểm if (con_chim.get_x() == chiec_cot[i].get_x()) { data.diem++; } // nếu có va chạm hoặc chim chạm đát thì gameover if ((chiec_cot[i].va_cham(con_chim) == 1) || (con_chim.get_y() >= data.Ymax)) { co.gameover(); con_chim.set_ton_tai(0); for (byte i = 0; i < data.cot_max; i++) { chiec_cot[i].set_ton_tai(0); chiec_cot[i].set_x(0); } break; } // hiển thj điểm } //for co.in_diem(); delay(10); met++; } void start_game() { lcd.rect(8, 22, 118, 11, BLACK); lcd.Asc_String(10, 24, Asc("A R D U I N O . V N"), BLACK); lcd.rect(58, 54, 70, 11, BLACK); lcd.Asc_String(60, 55, Asc("By Thai Son"), BLACK); lcd.display(); co.lap_vo_han(); lcd.clear(); }
Kết
Game là để giải trí, với Flappy Bird, nó còn cho ta một thông điệp đáng quý trong cuộc sống về sự kiên trì: “Có công mài sắt, có ngày nên kim”
Chúc các bạn gặt hái được nhiều kĩ năng với C++ và Gaming với arduino
<Thái Sơn>