日本熟妇hd丰满老熟妇,中文字幕一区二区三区在线不卡 ,亚洲成片在线观看,免费女同在线一区二区

貪吃蛇

實驗介紹

貪吃蛇是一個起源于1976年的街機游戲 Blockade。此類游戲在1990年代由于一些具有小型屏幕的移動電話的引入而再度流行起來,在現在的手機上基本都可安裝此小游戲。版本亦有所不同。 在游戲中,玩家操控一條細長的蛇,它會不停前進,玩家只能操控蛇的頭部朝向(上下左右),一路拾起觸碰到食物,并要避免觸碰到自身或者其他障礙物。每次貪吃蛇吃掉一件食物,它的身體便增長一些。

涉及知識點

OLED繪圖 按鍵事件

開發環境準備

硬件

開發用電腦一臺
HAAS EDU K1 開發板一塊
USB2TypeC 數據線一根

軟件

開發環境的搭建請參考《AliOS Things集成開發環境使用說明之搭建開發環境》,其中詳細的介紹了AliOS Things 3.3的IDE集成開發環境的搭建流程。

本案例的代碼下載請參考《AliOS Things集成開發環境使用說明之創建工程》

> 選擇解決方案:“HaaS EDU K1教育開發案例合集”

> 選擇開發板:haaseduk1 board configure

-- 編譯固件可參考《AliOS Things集成開發環境使用說明之編譯固件》

-- 燒錄固件可參考《AliOS Things集成開發環境使用說明之燒錄固件》

設計思路

游戲空間映射到邏輯空間

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

對象定義

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

typedef struct
{
    uint8_t length;                // 當前長度
    int16_t *XPos;                // 邏輯坐標x 數組
    int16_t *YPos;                // 邏輯坐標y 數組
    uint8_t cur_dir;        // 蛇頭的運行方向
    uint8_t alive;                // 存活狀態
} Snake;

食物

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;
}

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;
            }
        }
    }
}

對象繪畫

這一步其實是將邏輯空間重新映射到游戲空間,理應是整個游戲邏輯的最后一步,但是在我們開發過程中,也需要來自游戲空間的反饋,來驗證我們的實現是否符合預期。因此我們在這里提前實現它。

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
    );        // 蛇頭一定使用純色方塊
}

食物

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)。 這里以向上走為例。

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();
}

死亡判定

在蛇每次運動的過程中,都涉及到對整個游戲新的更新,包括上述過程中出現的 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);
        }
    }
}

實現效果

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