GAME-ST7565 - Làm game PONG với Arduino

Mô tả dự án: 

PONG – một game đơn giản, funny và đòi hỏi chút kiên nhẫn. Hôm nay mình sẽ hướng dẫn các bạn làm game này trên lcd ST7565 bằng Arduino .

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

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 :

Chương trình được lập trình theo hướng thủ tục.

Có 3 loại đối tượng cần quan tâm:

Đối tượng
Chức năng, nhiệm vụ
Thuộc tính
Thanh trượt
Người dùng điều khiển di chuyển trái phải.
Tọa độ
Viên đạn
Di chuyển theo 4 hướng cơ bản (chéo): hướng 45o – 135o – 235o – 315o.   Chạm vào biên hoặc Thanh trượt thì phản xạ lại.Chạm vào gạch  thì công phá gạch rồi phản xạ lại .
Tọa độ, Hướng bay
Gạch
Thứ để viên đạn công phá.
Tọa độ
  • Sử dụng mảng 2 chiều quản lí 2 đối tượng chính “Thanh trượt và viên đạn”
  • Đơn giản hóa việc quản lí “Gạch” : Sử dụng hàm “GetPixel”  để kiểm tra điểm ảnh tọa độ (x,y) có được vẽ hay không, nếu có thì đó là “Gạch’, không thì thôi.
  • Quy ước lại hướng bay:  Để thuận tiện , hướng bay sẽ được quy ước giống như hướng địa lí.
Góc lệch vector 
Hướng
45o
Đông bắc
135o
Tây bắc
235o
Tây nam
315o
Đông nam
  • Quy ước phương thức phản ứng của viên đạn.

Bằng cách chơi lại game này, quan sát rất kĩ hành vi của viên đạn mình tổng kết hướng bay của viên đạn như sau.

Chạm tường

Chạm gạch

CODING

// GAME NÀY ĐƯỢC VIẾT BỞI :THÁI SƠN -12:38AM 7/9/2016
// GAME ĐƯỢC UP CHÍNH THỨC TẠI ARDUINO.VN
//SỬ DỤNG 6 BUTTON NỐI PULLUP
// VÀO HÀM setup, set_brick, set_begin ĐỂ CÀI ĐẶT THEO SỞ THÍCH
//không được bấm quá nhiều nút cùng lúc
#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
byte object[2][4];
/*mảng tĩnh object[2][3]
i 0: thanh trượt
i 1: vien đạn
object[i][0] : tồn tại hay không, (dùng khi muốn tạo mới đối tượng)
object[i][1] : hoành độ (x)
object[i][2] : tung độ (y)
object[i][3] : hướng:
+đông bắc: hướng = 10
+ tây bắc: hương = 20
+tây nam: hướng = 30
+đông nam: hướng = 40

*/

byte danSize, width_cc, high_cc;
byte Xmin, Xmax, Ymin, Ymax;
unsigned int diem, timedelay;
// Chương trình trượt 1 lần
void setup()
{

    lcd.ON();
    lcd.SET(23, 0, 0, 0, 4);

    pinMode(left_b, INPUT_PULLUP);
    pinMode(down_b, INPUT_PULLUP);
    pinMode(right_b, INPUT_PULLUP);
    pinMode(up_b, INPUT_PULLUP);
    pinMode(fight_b, INPUT_PULLUP);
    pinMode(select_b, INPUT_PULLUP);

    danSize = 3;
    // 3*3 pixel
    width_cc = 20;
    // chiều dài con trượt
    high_cc = 3;
    //chiều cao con truot

    Xmin = 5; //biên trái
    Xmax = 120; //biên phải
    Ymin = 5; //biên trên
    Ymax = 60; //biên dưới
    //CHÚ Ý: nếu đạn vượt quá Ymax thì thua
    diem = 0; // điểm ban đàu
    timedelay = 80; //chu kì delay(làm chễ khung hình)
    /*
Serial.begin(9600);
*/
}

void set_brick()
{ //vẽ gạch
    //||||||||||||||||||||||||||||||||||||||||||
    //Bạn có thể vẽ bất cứ thứ gì lên màn hình khi nó
    //start game vào đây
    int i1, i2;
    for (i1 = 0; i1 <= 50; i1++) {
        // khởi động-vẽ gạch

        lcd.drawline(Xmin, i1, Xmax, i1, BLACK);
        delay(10);
        lcd.display(); //hiển thị
    }
    // cả hình tròn nữa
    for (i2 = 2; i2 <= 50; i2 += 4) {
        lcd.circle(65, 30, i2, DELETE); // VÂN tròn màu trắng

        lcd.circle(65, 30, i2 + 1, DELETE); // VÂN tròn màu trắng
        delay(40);

        lcd.fillrect(8, 22, 118, 11, DELETE);
        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(); // nên nhớ cần đặt display sau lệnh delay
        //điều rất có ý nghĩa trong việc vẽ ở tốc đọ cao

    } //for
    //||||||||||||||||||||||||||||||||||||||||||

    lcd.fillrect(0, 45, 128, 63, DELETE);
    lcd.display();
}
void ve_dan(byte x, byte y)
{
    lcd.fillrect(x, y, danSize, danSize, BLACK);
    lcd.display();
} // VẼ ĐẠN

void xoa_dan(byte x, byte y)
{
    lcd.fillrect(x, y, danSize, danSize, DELETE);
    lcd.display();

    //vẽ chữ nhật màu trắng để xóa thanh trượt vị trí cũ
} //XÓA ĐẠN

void ve_con_truot(byte x, byte y, byte width, byte high)
{
    lcd.fillrect(x, y, width, high, BLACK);
    lcd.display();
} // VẼ CON trượt

void xoa_con_truot(byte x, byte y, byte width, byte high)
{
    lcd.fillrect(x, y, width, high, DELETE);
    lcd.display();
} //
void di_con_truot()
{
    //lấy thông tin
    byte x, y, x_cu, y_cu; //cũ

    x_cu = object[0][1];
    y_cu = object[0][2];

    //đọc giá trị 4 button
    if (lcd.Pullup_4(right_b, up_b, left_b, down_b) == 1) { // nút right

        xoa_con_truot(x_cu, y_cu, width_cc, high_cc); //xóa con trượt cũ trên màn hình

        if (x_cu <= (Xmax - width_cc)) { // trong biên
            x = x_cu + 5; //cộng hoành độ lên 5 đơn vị
        }
        else {
            x = x_cu;
        }
        ve_con_truot(x, y_cu, width_cc, high_cc);
        object[0][1] = x; // ghi lại thông tin hoành độ
    }

    if (lcd.Pullup_4(right_b, up_b, left_b, down_b) == 3) { // nút left
        xoa_con_truot(x_cu, y_cu, width_cc, high_cc); //xóa con trượt cũ trên màn hình

        if (x_cu >= Xmin) { // trong biên
            x = x_cu - 5; //trừ hoành độ xuống 5 đơn vị
        }
        else {
            x = x_cu;
        }
        ve_con_truot(x, y_cu, width_cc, high_cc);
        object[0][1] = x; // ghi lại thông tin hoành độ
    }

} //di chuyển con trượt

void di_vien_dan(byte x_cu, byte y_cu, byte huong)
{
    // viên đạn có 4 hướng đi
    byte x_moi, y_moi;
    //xóa ảnh cũ
    xoa_dan(x_cu, y_cu);
    //vẽ ảnh mới
    if (huong == 10) { //đông bắc
        x_moi = x_cu + 1;
        y_moi = y_cu - 1;
    }
    if (huong == 20) { //tây bắc
        x_moi = x_cu - 1;
        y_moi = y_cu - 1;
    }
    if (huong == 30) { //tây nam
        x_moi = x_cu - 1;
        y_moi = y_cu + 1;
    }
    if (huong == 40) {
        x_moi = x_cu + 1;
        y_moi = y_cu + 1;
    }
    //vẽ
    ve_dan(x_moi, y_moi);
    //ghi lại thông tin vào mảng

    object[1][1] = x_moi;
    object[1][2] = y_moi;

} //di chuyển vien đạn
byte test_huong(byte x, byte y, byte danSize, byte huong)
{
    //x,y,danSize: 3 thuộc tính của viên đạn
    //kiểm tra vật thể ( gạch hoặc con trượt) ở hướng nào so với viên đạn
    //phải=     1
    //trên=     2
    //trái=     3
    //dưới=     4
    //chỉ cần kiểm tra về hướng đang di chuyển là đủ (không phải xung quanh)
    switch (huong) {
    case 10:
        // nếu đang đi hướng đông bắc

        for (byte y0 = y; y0 <= y + danSize; y0++) { //kiểm tra hướng đông
            if (lcd.getpixel(x + danSize + 2, y0) == 1) {
                return 1;
            }
        }
        for (byte x0 = x; x0 <= x + danSize; x0++) { // kiểm tra hướng bắc
            if (lcd.getpixel(x0, y - 2) == 1) {
                return 2;
            }
        }
        break;
    case 20:
        // nếu đang đi hướng tây bắc

        for (byte x0 = x; x0 <= x + danSize; x0++) { // kiểm tra hướng bắc
            if (lcd.getpixel(x0, y - 2) == 1) {
                return 2;
            }
        }
        for (byte y0 = y - 1; y0 <= y + danSize; y0++) { // kiểm tra hướng tây
            if (lcd.getpixel(x - 2, y0) == 1) {
                return 3;
            }
        }
        break;
    case 30:
        //nếu đang đi hướng tây nam

        for (byte y0 = y - 1; y0 <= y + danSize; y0++) { // kierm tra hướng tây
            if (lcd.getpixel(x - 2, y0) == 1) {
                return 3;
            }
        }
        for (byte x0 = x - 1; x0 <= x + danSize; x0++) { //kiểm tra hướng nam
            if (lcd.getpixel(x0, y + danSize + 1) == 1) {
                return 4;
            }
        }
        break;
    case 40:
        // nếu đang đi hướng đông nam

        for (byte x0 = x - 1; x0 <= x + danSize; x0++) { //kiểm tra hướng nam
            if (lcd.getpixel(x0, y + danSize + 1) == 1) {
                return 4;
            }
        }
        for (byte y0 = y; y0 <= y + danSize; y0++) { //kiểm tra hướng đông
            if (lcd.getpixel(x + danSize + 2, y0) == 1) {
                return 1;
            }
        }
        break;
    default:

        break;
    } //switch

    //nếu tất cả đều ko thỏa mãm
    return 0;

} // kiểm tra hướng gạch

void doi_huong(byte bien)
{ //biên

    switch (bien) {
    case 1:
        //biên phải_đông
        if (object[1][3] == 10) {
            object[1][3] = 20;
        } // ĐB>TB
        if (object[1][3] == 40) {
            object[1][3] = 30;
        } //ĐN>TN
        break;
    case 2:
        //biên trên_bắc
        if (object[1][3] == 20) {
            object[1][3] = 30;
        } // TB>TN
        if (object[1][3] == 10) {
            object[1][3] = 40;
        } //ĐB>ĐN
        break;
    case 3:
        //biên trái_tây
        if (object[1][3] == 30) {
            object[1][3] = 40;
        } // TN>ĐN
        if (object[1][3] == 20) {
            object[1][3] = 10;
        } //TB>ĐB
        break;
    case 4:
        //biên dưới(con trượt)_nam
        if (object[1][3] == 30) {
            object[1][3] = 20;
        } // TN>TB
        if (object[1][3] == 40) {
            object[1][3] = 10;
        } //ĐN>ĐB
        break;
    default:
        //no
        break;

    } //switch

} //đổi hướng
void BIGBANG(byte x, byte y)
{
    //tạo vụ nổ công phá gạch = hcn màu trắng
    lcd.fillrect(x - danSize, y - danSize, danSize * 3, danSize * 3, DELETE);

    lcd.display();
}
void bang_diem()
{

    //THUA CUỘC
    // BẢNG ĐIỂM
    lcd.clear();
    lcd.fillrect(0, 5, 128, 10, DELETE);
    lcd.Asc_String(0, 6, Asc(" DIEM CUA BAN LA:"), BLACK);
    lcd.Number_Ulong(98, 6, diem, ASCII_NUMBER, BLACK);
    lcd.Asc_String(60, 55, Asc("By Thai Son"), BLACK);
    lcd.display(); // nên nhớ cần đặt display sau lệnh delay
    lcd.display();

    //Reset object[2][4]
    for (byte i = 0; i <= 1; i++) {
        for (byte j = 0; j <= 3; j++) {
            object[i][j] = 0;
        }
    }
    // reset diem
    diem = 0;
}
void lap_vo_han()
{

    //LẶP CÓ ĐIỀU KIỆN
    //Nhấn nút fight để chơi

    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, DELETE);
        lcd.display();

        if (digitalRead(fight_b) == 0) {
            break; // thoát ngay
        }
        delay(250);
    }
}
void set_begin()
{

    lcd.clear(); //làm sạch làm hình
    object[0][0] = 1; // cấp tồn tại cho con trượt
    object[0][1] = 60; //   cấp hoành độ ban đầu
    object[0][2] = Ymax - high_cc; //   cấp tung độ ban đầu
    object[1][0] = 1; // cấp tồn tại cho viên đạn
    object[1][1] = 60; // cấp hoành độ cho viên đạn
    object[1][2] = (Ymax - high_cc) - danSize; //cấp tung độ
    object[1][3] = 10; //hướng đông bắc

    /*
 //Bạn có thể gán thế này
object[][]={
{1,60,Ymax-high_cc,0},
{1,60,(Ymax-high_cc)-danSize,10}
};
*/
}

void loop()
{
    //Block 0: start
    if ((object[0][0] == 0) && (object[1][0] == 0)) {
        set_begin(); //cấp dữ liệu
        set_brick(); //vẽ gạch
    }

    //Block 1: di chuyển con trượt
    di_con_truot();
    //Block2: di chuyển đạn + kiểm tra cahmj biên

    byte x_cu, y_cu, huong_cu; // x cũ, y cũ, hướng cũ
    byte huong_moi; //mới
    //lấy thông tin
    x_cu = object[1][1];
    y_cu = object[1][2];
    huong_cu = object[1][3];
    //kiểm tra chạm biên
    if (x_cu >= Xmax) {
        doi_huong(1);
    }
    if (y_cu <= Ymin) {
        doi_huong(2);
    }
    if (x_cu <= Xmin) {
        doi_huong(3);
    }

    if (y_cu >= ((Ymax - danSize * 2) - (high_cc))) {
        if (test_huong(x_cu, y_cu, danSize, huong_cu) == 4) {
            //kiểm tra hướng nam
            doi_huong(4);
        }
    }
    //lấy thông tin
    huong_moi = object[1][3];
    //di chuyển
    di_vien_dan(x_cu, y_cu, huong_moi);
    //Block 3: phá gạch
    if (y_cu < ((Ymax - danSize * 2) - (high_cc))) {
        //nếu đạn ở phía trên con trượt
        byte kt_huong;
        kt_huong = test_huong(x_cu, y_cu, danSize, huong_moi);
        if (kt_huong != 0) {
            //phát hiện vật cản
            //b1: nổ
            BIGBANG(x_cu, y_cu);
            //b2: đỏi hướng giống như chạm biên
            doi_huong(kt_huong);
            //b3: cộng điểm
            diem++;
        }
    }
    //Block 4: Nếu quá biên dưới sẽ chết
    if (y_cu > Ymax) {
        bang_diem(); //in bảng điểm

        // nhấn nút fight để chơi
        lap_vo_han();
    }
    //Block5: LÀM CHỄ
    if (digitalRead(fight_b) == 0) { //nút fight được nhấn
        delay(timedelay / 4); //tăng tốc gấp 4
    }
    else {
        delay(timedelay);
    } //giữ nguyên
    /* Kiểm tra
  Serial.print(" Co vat can huong:");
   Serial.println(test_huong(x_cu,y_cu, danSize, huong_moi));
    
      Serial.println("______________ton_tai _____ X_______Y______huong");
  for(byte i =0; i<=1; i++){

      Serial.print(" Object(");
      Serial.print(i);
      Serial.print(")  ");
    for(byte j=0; j<=3; j++){
      
      Serial.print("_______");
      Serial.print(object[i][j]);
      
    } 
    Serial.println("");
  } 
  */
}

 

Một vài hàm set_brick() ;

Dưới đây là các thuật toán vẽ để tạo gạch trong game, bạn có thể copy và sửa lại hàm set_brick() trong game bằng các đoạn code dưới.

WALL

 

void set_brick()
{ //vẽ gạch
    //||||||||||||||||||||||||||||||||||||||||||
    //Bạn có thể vẽ bất cứ thứ gì lên màn hình khi nó
    //start game vào đây

    int i1, i2;
    for (i1 = 0; i1 <= 45; i1++) {
        // khởi động-vẽ gạch

        lcd.drawline(Xmin, i1, Xmax, i1, BLACK);
        delay(10);
        lcd.display(); //hiển thị
    }
    // cả hình tròn nữa
    for (i2 = 2; i2 <= 40; i2 += 4) {

        lcd.drawline(Xmin, i2, Xmax, i2, DELETE); //ĐƯỜNG KẺ TRẮNG

        lcd.rect(58, 54, 70, 11, BLACK);
        lcd.Asc_String(60, 55, Asc("By Thai Son"), BLACK);
        lcd.display(); // nên nhớ cần đặt display sau lệnh delay
        //điều rất có ý nghĩa trong việc vẽ ở tốc đọ cao

    } //for

    //||||||||||||||||||||||||||||||||||||||||||

    lcd.fillrect(0, 45, 128, 63, DELETE);
    lcd.display();
}

Brick matrix

void set_brick()
{ //vẽ gạch
    //||||||||||||||||||||||||||||||||||||||||||
    //Bạn có thể vẽ bất cứ thứ gì lên màn hình khi nó
    //start game vào đây

    lcd.rect(58, 54, 70, 11, BLACK);
    lcd.Asc_String(60, 55, Asc("By Thai Son"), BLACK);
    lcd.display(); // nên nhớ cần đặt display sau lệnh delay
    ////////////////////////////////////////////////

    for (int x = 0; x < Xmax; x += 10) {
        for (int y = 0; y < 50; y += 10) {
            lcd.FillRect(x, y, 5, 5, BLACK);
            lcd.display();
            delay(50);
        }
    }

    //||||||||||||||||||||||||||||||||||||||||||

    lcd.fillrect(Xmin, Ymax - 15, Xmax, 63, DELETE);
    lcd.display();
}

Random brick

void set_brick()
{ //vẽ gạch
    //||||||||||||||||||||||||||||||||||||||||||
    //Bạn có thể vẽ bất cứ thứ gì lên màn hình khi nó
    //start game vào đây

    lcd.rect(58, 54, 70, 11, BLACK);
    lcd.Asc_String(60, 55, Asc("By Thai Son"), BLACK);
    lcd.display(); // nên nhớ cần đặt display sau lệnh delay
    ////////////////////////////////////////////////
    int x, y, size_brick;
    for (int i = 0; i < 50; i++) {
        // 50 viên gạch
        randomSeed(millis()); // lấy random từ đồng hồ
        x = random(Xmin, Xmax);
        y = random(Ymin, Ymax - 15);
        size_brick = random(3, 10);
        lcd.FillRect(x, y, size_brick, size_brick, BLACK);
        lcd.display();
        delay(30);
    }

    //||||||||||||||||||||||||||||||||||||||||||

    lcd.fillrect(Xmin, Ymax - 15, Xmax, 63, DELETE);
    lcd.display();
}

Random circle

 

void set_brick()
{ //vẽ gạch
    //||||||||||||||||||||||||||||||||||||||||||
    //Bạn có thể vẽ bất cứ thứ gì lên màn hình khi nó
    //start game vào đây

    lcd.rect(58, 54, 70, 11, BLACK);
    lcd.Asc_String(60, 55, Asc("By Thai Son"), BLACK);
    lcd.display(); // nên nhớ cần đặt display sau lệnh delay
    ////////////////////////////////////////////////
    int x, y, R;
    for (int i = 0; i < 25; i++) {
        // 50 viên gạch
        randomSeed(millis()); // lấy random từ đồng hồ
        x = random(Xmin, Xmax);
        y = random(Ymin, Ymax - 15);
        R = random(3, 10);
        lcd.FillCircle(x, y, R, BLACK);
        lcd.display();
        delay(30);
    }

    //||||||||||||||||||||||||||||||||||||||||||

    lcd.fillrect(Xmin, Ymax - 15, Xmax, 63, DELETE);
    lcd.display();
}

Kết

Hi vọng bạn có thật nhiều niềm vui khi chơi game và thử sức lập trình nó với ARDUINO. Chúc bạn tích lũy thêm nhiều kĩ năng hơn trong lập trình C/C++ hơn nữa.

smileylaughangelblush

Thái Sơn

lên
9 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

Bộ điều khiển PID - ứng dụng phần 2 - xe dò line dùng thuật toán PID

Tiép nối bài viết về xe dò line cảm ơn Đỗ Hữu Toàn đã viết hộ mình phần 4. hôm nay mình sẽ làm cho chiếc xe dò line đi mượt và có hồn hơn 

lên
34 thành viên đã đánh giá bài viết này hữu ích.
Các bài viết cùng tác giả

Các hàm số học exp(), Idexp(), modf, ln(), log10(), ceil(), floor(), atoi(chuyển chữ thành số).

Bài này bổ xung các tập lệnh về các phép tính : exp(ex), Idexp(x*2exponent),  modf, ln(x), log10(x), floor(), ceil(), atoi()…

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

GAME-ST7565 - Làm Game xe tăng với Arduino

Hôm nay sẽ là Game xe tăng – TANK WAR – tựa Game quen thuộc nhưng đầy hấp hẫn. Điểm đặc biệt là bạn có chơi ở chế độ cân TEAM 1-25 cực FUN devil.

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