天天看點

HaaS EDU場景式應用學習 - 貪吃蛇實驗介紹涉及知識點開發環境準備設計思路開發者支援

實驗介紹

貪吃蛇是一個起源于1976年的街機遊戲 Blockade。此類遊戲在1990年代由于一些具有小型螢幕的行動電話的引入而再度流行起來,在現在的手機上基本都可安裝此小遊戲。版本亦有所不同。

在遊戲中,玩家操控一條細長的蛇,它會不停前進,玩家隻能操控蛇的頭部朝向(上下左右),一路拾起觸碰到食物,并要避免觸碰到自身或者其他障礙物。每次貪吃蛇吃掉一件食物,它的身體便增長一些。

HaaS EDU場景式應用學習 - 貪吃蛇實驗介紹涉及知識點開發環境準備設計思路開發者支援

涉及知識點

OLED繪圖

按鍵事件

開發環境準備

硬體

開發用電腦一台
HAAS EDU K1 開發闆一塊
USB2TypeC 資料線一根
           
  • 1
  • 2
  • 3

軟體

AliOS Things開發環境搭建

開發環境的搭建請參考 @ref HaaS_EDU_K1_Quick_Start (搭建開發環境章節),其中詳細的介紹了AliOS Things 3.3的IDE內建開發環境的搭建流程。
           

HaaS EDU K1 DEMO 代碼下載下傳

HaaS EDU K1 DEMO 的代碼下載下傳請參考 @ref HaaS_EDU_K1_Quick_Start (建立工程章節),其中,
選擇解決方案: 基于教育開發闆的示例
選擇開發闆: haaseduk1 board configure
           

代碼編譯、燒錄

參考 @ref HaaS_EDU_K1_Quick_Start (3.1 編譯工程章節),點選 ✅ 即可完成編譯固件。
參考 @ref HaaS_EDU_K1_Quick_Start (3.2 燒錄鏡像章節),點選 "⚡️" 即可完成燒錄固件。
           

設計思路

遊戲空間映射到邏輯空間

當玩家在體驗遊戲時,他們能操作的都是遊戲空間,包括按鍵的上下左右,對象物體的運動等等。對于開發者而言,我們需要将這些設想的遊戲空間映射到邏輯空間中,做好對使用者輸入的判斷,對象運動的處理,對象間互動的判定,遊戲整體程序的把控,以及最終将邏輯空間再次映射回遊戲空間,傳回給玩家。

對象定義

這一步是将遊戲空間中涉及到的對象抽象化。在C語言的實作中,我們将對象抽象為結構體,對象屬性抽象為結構體的成員。

typedef struct
{
    uint8_t length;        // 目前長度
    int16_t *XPos;        // 邏輯坐标x 數組
    int16_t *YPos;        // 邏輯坐标y 數組
    uint8_t cur_dir;    // 蛇頭的運作方向
    uint8_t alive;        // 存活狀态
} Snake;
           
  • 4
  • 5
  • 6
  • 7
  • 8

食物

typedef struct
{
    int16_t x;
    int16_t y;            // 食物邏輯坐标
    uint8_t eaten;        // 食物是否被吃掉 
} Food;
           

地圖

typedef struct
{
    int16_t border_top;
    int16_t border_right;
    int16_t border_botton;
    int16_t border_left;    // 邊界像素坐标
    int16_t block_size;        // 網格大小 在本實驗的實作中 蛇身和食物的大小被統一限制進網格的大小中
} Map;
           

遊戲

typedef struct
{
    int16_t score;            // 遊戲記分
    int16_t pos_x_max;        // 邏輯最大x坐标    pos_x_max = (map.border_right - map.border_left) / map.block_size;
    int16_t pos_y_max;        // 邏輯最大y坐标    pos_y_max = (map.border_botton - map.border_top) / map.block_size;
} snake_game_t;
           

通過Map和snake_game_t的定義,我們将螢幕的 (border_left, border_top, border_bottom, border_right) 部分設定為遊戲區域,并且将其切分為 pos_x_max* pos_y_max 個大小為 block_size 的塊。繼而,我們可以在每個塊中繪制蛇、食物等對象。

對象初始化

在遊戲每一次開始時,我們需要給對象一些初始的屬性,例如蛇的長度、位置、存活狀态,食物的位置、狀态, 地圖的邊界、塊大小等等。

Food food = {-1, -1, 1};
Snake snake = {4, NULL, NULL, 0, 1};
Map map = {2, 128, 62, 12, 4};
snake_game_t snake_game = {0, 0, 0};

int greedySnake_init(void)
{
    // 計算出遊戲的最大邏輯坐标 用于限制遊戲範圍
    snake_game.pos_x_max = (map.border_right - map.border_left) / map.block_size;
    snake_game.pos_y_max = (map.border_botton - map.border_top) / map.block_size;
    // 為蛇的坐标數組配置設定空間 蛇的最大長度是填滿整個螢幕 即 pos_x_max* pos_y_max
    snake.XPos = (int16_t *)malloc(snake_game.pos_x_max * snake_game.pos_y_max * sizeof(int16_t));
    snake.YPos = (int16_t *)malloc(snake_game.pos_x_max * snake_game.pos_y_max * sizeof(int16_t));
    // 蛇的初始長度設為4
    snake.length = 4;
    // 蛇的初始方向設為 右
    snake.cur_dir = SNAKE_RIGHT;
    // 生成蛇的身體 蛇頭在邏輯區域最中間的坐标上 即 (pos_x_max/2, pos_y_max/2)
    for (uint8_t i = 0; i < snake.length; i++)
    {
        snake.XPos[i] = snake_game.pos_x_max / 2 + i;
        snake.YPos[i] = snake_game.pos_y_max / 2;
    }
    // 複活這條蛇
    snake.alive = 1;
    
    // 将食物設定為被吃掉
    food.eaten = 1;
    // 生成食物 因為食物需要反複生成 是以封裝為函數
    gen_food();

    // 遊戲開始分數為0
    snake_game.score = 0;
    
    return 0;
}
           
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
void gen_food()
{
    int i = 0;
    // 如果食物被吃了
    if (food.eaten == 1)
    {
        while (1)
        {
            // 随機生成一個坐标
            food.x = rand() % snake_game.pos_x_max;
            food.y = rand() % snake_game.pos_y_max;

            // 開始周遊蛇身 檢查坐标是否重合
            for (i = 0; i < snake.length; i++)
            {
                // 如果生成的食物坐标和蛇身重合 不合法 重新随機生成
                if ((food.x == snake.XPos[i]) && (food.y == snake.YPos[i]))
                    break;
            }
            // 周遊完蛇身 并未發生重合
            if (i == snake.length)
            {
                // 生成有效 終止循環
                food.eaten = 0;
                break;
            }
        }
    }
}
           

對象繪畫

這一步其實是将邏輯空間重新映射到遊戲空間,理應是整個遊戲邏輯的最後一步,但是在我們開發過程中,也需要來自遊戲空間的回報,來驗證我們的實作是否符合預期。是以我們在這裡提前實作它。

HaaS EDU場景式應用學習 - 貪吃蛇實驗介紹涉及知識點開發環境準備設計思路開發者支援
static uint8_t icon_data_snake1_4_4[] = {0x0f, 0x0f, 0x0f, 0x0f};    // 純色方塊
static icon_t icon_snake1_4_4 = {icon_data_snake1_4_4, 4, 4, NULL};

static uint8_t icon_data_snake0_4_4[] = {0x09, 0x09, 0x03, 0x03};    // 紋理方塊
static icon_t icon_snake0_4_4 = {icon_data_snake0_4_4, 4, 4, NULL};

void draw_snake()
{
    uint16_t i = 0;

    OLED_Icon_Draw(
        map.border_left + snake.XPos[i] * map.block_size, 
        map.border_top + snake.YPos[i] * map.block_size, 
        &icon_snake0_4_4, 
        0
    );    // 蛇尾一定使用紋理方塊

    for (; i < snake.length - 2; i++)
    {
        OLED_Icon_Draw(
            map.border_left + snake.XPos[i] * map.block_size, 
            map.border_top + snake.YPos[i] * map.block_size, 
            ((i % 2) ? &icon_snake1_4_4 : &icon_snake0_4_4), 
            0);
    }    // 蛇身交替使用純色和紋理方塊 來模拟蛇的花紋

    OLED_Icon_Draw(
        map.border_left + snake.XPos[i] * map.block_size, 
        map.border_top + snake.YPos[i] * map.block_size, 
        &icon_snake1_4_4, 
        0
    );    // 蛇頭一定使用純色方塊
}
           

HaaS EDU場景式應用學習 - 貪吃蛇實驗介紹涉及知識點開發環境準備設計思路開發者支援
static uint8_t icon_data_food_4_4[] = {0x06, 0x09, 0x09, 0x06};
static icon_t icon_food_4_4 = {icon_data_food_4_4, 4, 4, NULL};

void draw_food()
{
    if (food.eaten == 0)    // 如果食物沒被吃掉
    {
        OLED_Icon_Draw(
            map.border_left + food.x * map.block_size, 
            map.border_top + food.y * map.block_size, 
            &icon_food_4_4, 
            0);
    }
}
           

對象行為

蛇的運動

在貪吃蛇中,對象蛇發生運動,有兩種情況,一是在使用者無操作的情況下,蛇按照目前的方向繼續運動,而是使用者按鍵觸發蛇的運動。總而言之,都是蛇的運動,隻是運動的方向不同,是以我們可以将蛇的行為抽象為

void Snake_Run(uint8_t dir)。

這裡以向上走為例。

HaaS EDU場景式應用學習 - 貪吃蛇實驗介紹涉及知識點開發環境準備設計思路開發者支援
void Snake_Run(uint8_t dir)
{
    switch (dir)
    {
        // 對于右移
        case SNAKE_UP:
            // 如果目前方向是左則不響應 因為不能掉頭
            if (snake.cur_dir != SNAKE_DOWN)
            {
                // 将蛇身數組向前移
                // 值得注意的是,這裡采用數組起始(XPos[0],YPos[0])作為蛇尾,
                // 而使用(XPos[snake.length - 1], YPos[snake.length - 1])作為蛇頭
                // 這樣實作會較為友善
                for (uint16_t i = 0; i < snake.length - 1; i++)
                {
                    snake.XPos[i] = snake.XPos[i + 1];
                    snake.YPos[i] = snake.YPos[i + 1];
                }
                // 将蛇頭位置轉向右側 即 snake.XPos[snake.length - 2] + 1
                snake.XPos[snake.length - 1] = snake.XPos[snake.length - 2];
                snake.YPos[snake.length - 1] = snake.YPos[snake.length - 2] - 1;
                snake.cur_dir = dir;
            }
            break;
        case SNAKE_LEFT:
            ...
        case SNAKE_DOWN:
            ...
        case SNAKE_RIGHT:
            ...
            break;
    }
    
    // 檢查蛇是否存活
    check_snake_alive();
    // 檢查食物狀态
    check_food_eaten();
    // 更新完所有狀态後繪制蛇和食物
    draw_snake();
    draw_food();
}
           
  • 37
  • 38
  • 39
  • 40
  • 41

死亡判定

在蛇每次運動的過程中,都涉及到對整個遊戲新的更新,包括上述過程中出現的 check_snake_alive check_food_eaten 等。

對于 check_snake_alive, 分為兩種情況:蛇碰到地圖邊界/蛇吃到自己。

void check_snake_alive()
{
    // 判斷蛇頭是否接觸邊界
    if (snake.XPos[snake.length - 1] < 0 ||
        snake.XPos[snake.length - 1] >= snake_game.pos_x_max ||
        snake.YPos[snake.length - 1] < 0 ||
        snake.YPos[snake.length - 1] >= snake_game.pos_y_max)
    {
        snake.alive = 0;
    }
    
    // 判斷蛇頭是否接觸自己
    for (int i = 0; i < snake.length - 1; i++)
    {
        if (snake.XPos[snake.length - 1] == snake.XPos[i] && snake.YPos[snake.length - 1] == snake.YPos[i])
        {
            snake.alive = 0;
            break;
        }
    }
}
           

吃食判定

在貪吃蛇中,食物除了被吃的份,還有就是随機生成。生成食物在上一節已經實作,是以這一節我們就來實作檢測食物是否被吃。

void check_food_eaten()
{
    // 如果蛇頭與食物重合 
    if (snake.XPos[snake.length - 1] == food.x && snake.YPos[snake.length - 1] == food.y)
    {
        // 說明吃到了食物
        food.eaten = 1;
        // 增加蛇的長度
        snake.length++;
        // 長度增加表現為頭的方向延伸
        snake.XPos[snake.length - 1] = food.x;
        snake.YPos[snake.length - 1] = food.y;
        // 遊戲得分增加
        snake_game.score++;
        // 重新生成食物
        gen_food();
    }
}
           

綁定使用者操作

在貪吃蛇中,唯一的使用者操作就是使用者按鍵觸發蛇的運動。好在我們已經對這個功能實作了良好的封裝,即void Snake_Run(uint8_t dir)

我們隻需要在按鍵回調函數中,接收來自底層上報的key_code即可。

#define SNAKE_UP     EDK_KEY_2
#define SNAKE_LEFT     EDK_KEY_1
#define SNAKE_RIGHT EDK_KEY_3
#define SNAKE_DOWN     EDK_KEY_4

void greedySnake_key_handel(key_code_t key_code)
{
    Snake_Run(key_code);
}
           

遊戲全局控制

在這個主循環裡,我們需要對遊戲整體進行重新整理、繪圖,對玩家的輸赢、得分進行判定,并提示玩家遊戲結果。

void greedySnake_task(void)
{
    while (1)
    {
        if (snake.alive)
        {
             // 清除螢幕memory
            OLED_Clear();
            // 繪制地圖邊界
            OLED_DrawRect(11, 1, 118, 62, 1);
            // 繪制“SCORE”
            OLED_Icon_Draw(3, 41, &icon_scores_5_21, 0);
            // 繪制玩家目前分數
            draw_score(snake_game.score);
            // 讓蛇按目前方向運作
            Snake_Run(snake.cur_dir);
            // 将螢幕memory輸出
            OLED_Refresh_GRAM();
            // 間隔200ms
            aos_msleep(200);
        }
        else
        {
            // 清除螢幕memory
            OLED_Clear();
            // 提示 GAME OVER
            OLED_Show_String(30, 24, "GAME OVER", 16, 1);
            // 将螢幕memory輸出
            OLED_Refresh_GRAM();
            // 間隔500ms
            aos_msleep(500);
        }
    }
}
           

實作效果

接下來請欣賞筆者的操作。

HaaS EDU場景式應用學習 - 貪吃蛇實驗介紹涉及知識點開發環境準備設計思路開發者支援

開發者支援

HaaS官方:

https://haas.iot.aliyun.com/

HaaS技術社群:

https://blog.csdn.net/HaaSTech

開發者釘釘群和公衆号見下圖,開發者釘釘群每天都有技術支援同學值班。

HaaS EDU場景式應用學習 - 貪吃蛇實驗介紹涉及知識點開發環境準備設計思路開發者支援

繼續閱讀