實驗介紹
飛機大戰作為一款經典的街機游戲,是很多人的童年回憶。我們的 HaaS EDU K1 開發板專門設計了街機樣式的按鍵排列,很適合我們做這類游戲的開發。
涉及知識點
OLED繪圖
按鍵事件
開發環境準備
硬件
開發用電腦一臺
HAAS EDU K1 開發板一塊
USB2TypeC 數據線一根
軟件
開發環境的搭建請參考《AliOS Things集成開發環境使用說明之搭建開發環境》,其中詳細的介紹了AliOS Things 3.3的IDE集成開發環境的搭建流程。
本案例的代碼下載請參考《AliOS Things集成開發環境使用說明之創建工程》,
> 選擇解決方案:“HaaS EDU K1教育開發案例合集”
> 選擇開發板:haaseduk1 board configure
-- 編譯固件可參考《AliOS Things集成開發環境使用說明之編譯固件》。
-- 燒錄固件可參考《AliOS Things集成開發環境使用說明之燒錄固件》。
游戲設定
不同于規則簡單的貪吃蛇,在飛機大戰這類游戲中,往往需要對游戲中出現的每個對象進行數值、行為的設定。在開發游戲前期,梳理好這些設定也有助于我們更清晰地進行開發。有時,優秀的設定也是吸引玩家的重要因素。
角色設定
行為設定
在本游戲中,玩家將控制阿克琉斯級戰艦,在持續不斷的敵機中通過閃避或攻擊開辟出自己的路。
玩家可以通過 HaaS EDU K1 的四個按鍵控制,阿克琉斯級戰艦進行前后左右運動。
在游戲進行過程中,玩家的戰艦會不斷發射炮彈。被炮彈攻擊的敵方戰艦會損失響應的裝甲。
若玩家戰艦被敵方戰艦撞擊,雙方均會損失裝甲。
玩家有三次緊急修復戰艦的機會。
游戲實現
游戲流程
在開始之前,我們先使用一個簡單的流程,幫助大家理解本游戲的刷新機制。這個大循化即游戲刷新所需要的所有流程。
// 游戲中所有對象的更新判定由大循環維護
void aircraftBattle_task()
{
while (1)
{
OLED_Clear(); // 清理屏幕數據
global_update(); // 刷新全局對象,如更新對象的貼圖狀態,發射子彈,撞擊判斷等
global_draw(); // 繪制刷新完后的所有對象
OLED_Refresh_GRAM();// 將繪制結果顯示在屏幕上
aos_msleep(40); // 40ms 為一個游戲周期
}
}
貼圖實現
對于每個對象,我們希望能夠將其定位到游戲地圖上的每一點,而不是單純使用貼圖函數。因此,每個對象有一個“控制坐標”,而我們相對這個“控制坐標”計算出貼圖坐標。這樣,如果一個對象需要變換不同尺寸的貼圖,我們可以更方便地計算出它的貼圖坐標。
如圖,紅色為該對象的控制坐標,藍色為該貼圖的貼圖坐標。
typedef struct
{
map_t *map; // 貼圖
int cur_x;
int cur_y; // 飛行物對象的控制坐標
} dfo_t; // 飛行物對象
/*
-> x
____________________
| | icon|
| | of_y |
\/ | | |
y |--of_x--cp |
|__________________|
*/
typedef struct
{
icon_t *icon; // 貼圖對象
int offset_x;
int offset_y; // 相對于控制坐標的偏移
} map_t; // 貼圖
注意??,在開發過程中,我們使用的是豎屏模式,坐標系是以豎屏做處理。因此,在繪圖時,我們需要做坐標系的轉換。
void draw_dfo(dfo_t *dfo)
{
map_t *cur_map = get_cur_map(dfo); // 獲取當前對象的貼圖
// 計算對象邊界
int top = dfo->cur_y + cur_map->offset_y;
int bottom = dfo->cur_y + cur_map->offset_y + cur_map->icon->width;
int left = dfo->cur_x + cur_map->offset_x;
int right = dfo->cur_x + cur_map->offset_x + cur_map->icon->height;
// 若對象超出屏幕,則不繪制
if (top > 132 || bottom < 0 || left > 64 || right < 0)
return;
// 繪制坐標轉換后的貼圖對象
OLED_Icon_Draw(
dfo->cur_y + cur_map->offset_y,
64 - (dfo->cur_x + cur_map->offset_x + cur_map->icon->height),
cur_map->icon,
2);
}
這樣,就可以實現在OLED上繪制我們設定的戰艦圖片了。
移動戰艦
接下來,我們要實現的是根據用戶的按鍵輸入來移動戰艦的貼圖。在此之前,我們需要對 dfo_t 結構體進行更多的補充。我們額外定義一個 speed 屬性,用于定義在用戶每次操作時移動一定的距離。
注意,這里的前后左右均是在游戲坐標系中。
typedef struct
{
// 艦船坐標
int cur_x; // 運動
int cur_y;
// 艦船速度
uint8_t speed; // 絕對固定
// 艦船貼圖
map_t *map;
} dfo_t; // Dentified Flying Object
typedef enum
{
UP,
LEFT,
RIGHT,
DOWN
} my_craft_dir_e_t;
void move_MyCraft(dfo_t *my_craft, my_craft_dir_e_t dir)
{
// 獲取艦船當前的貼圖對象
map_t *cur_map = get_cur_map(my_craft);
// 計算貼圖邊界
int top = my_craft->cur_y + cur_map->offset_y;
int bottom = my_craft->cur_y + cur_map->offset_y + cur_map->icon->width;
int left = my_craft->cur_x + cur_map->offset_x;
int right = my_craft->cur_x + cur_map->offset_x + cur_map->icon->height;
// 判斷方向
switch (dir)
{
case UP:
// 如果這次移動不會超過地圖邊界,則移動
if (!(top - my_craft->speed < 0))
my_craft->cur_y -= my_craft->speed;
break;
case DOWN:
if (!(bottom + my_craft->speed > 132))
my_craft->cur_y += my_craft->speed;
break;
case LEFT:
if (!(left - my_craft->speed < 0))
my_craft->cur_x -= my_craft->speed;
break;
case RIGHT:
if (!(right + my_craft->speed > 64))
my_craft->cur_x += my_craft->speed;
break;
default:
break;
}
}
將按鍵回調函數關聯至移動艦船函數。注意,這里的前后左右均是在游戲坐標系中。
void aircraftBattle_key_handel(key_code_t key_code)
{
switch (key_code)
{
case EDK_KEY_4:
move_MyCraft(my_craft, LEFT);
break;
case EDK_KEY_1:
move_MyCraft(my_craft, UP);
break;
case EDK_KEY_3:
move_MyCraft(my_craft, DOWN);
break;
case EDK_KEY_2:
move_MyCraft(my_craft, RIGHT);
break;
default:
break;
}
}
加一點特效
作為一個注重細節,精益求精的開發者,我們希望給我們的艦船加上一些特效。而這需要艦船對象不斷改變重繪自己的貼圖。為了這個功能,我們額外創建了一個新的結構體用于管理“動畫”。
typedef struct
{
map_t **act_seq_maps; // 貼圖指針數組,該動畫的所有貼圖(例如爆炸動作包含3幀)
uint8_t act_seq_len; // 貼圖指針數組長度
uint8_t act_seq_index; // 用于索引幀
uint8_t act_seq_interval; // 幀間延遲
uint8_t act_seq_interval_cnt; // 用于延遲計數
uint8_t act_is_destory; // 用于標記該動畫是否是毀滅動畫,若是則不再重復
} act_seq_t;
同時,每個艦船對象新增了一系列屬性 act_seq_type, 用于顯示當前的貼圖狀態。例如,當 act_seq_type = 0 時,表示艦船處于正常狀態,每隔 act_seq_interval 個周期切換顯示一次貼圖,即第一行的三幀貼圖。當 act_seq_type = 1 時,表示艦船處于爆炸狀態,每隔 act_seq_interval 個周期切換顯示一次貼圖,即第二行的三幀貼圖。
目前 act_seq_type 的含義由每個艦船對象自己定義和維護。也可以歸納成統一的枚舉量,這一步讀者可以自行完成。
typedef struct
{
int cur_x;
int cur_y;
uint8_t speed;
act_seq_t **act_seq_list; // 動畫數組包含了多個動作序列
uint8_t act_seq_list_len; // 動畫數組長度
uint8_t act_seq_type;
} dfo_t;
// 正常動作序列
act_seq_t *achilles_normal_act = (act_seq_t *)malloc(sizeof(act_seq_t));
achilles_normal_act->act_seq_maps = achilles_normal_maplist;
achilles_normal_act->act_seq_len = 3; // 該動作序列包含3幀圖片
achilles_normal_act->act_seq_interval = 10; // 該動畫幀間延遲10周期
achilles_normal_act->act_is_destory = 0; // 該動畫不是毀滅動畫,即一直重復
// 毀滅動作序列
act_seq_t *achilles_destory_act = (act_seq_t *)malloc(sizeof(act_seq_t));
achilles_destory_act->act_seq_maps = achilles_destory_maplist;
achilles_destory_act->act_seq_len = 3;
achilles_destory_act->act_seq_interval = 4; // 該動畫幀間延遲4周期
achilles_destory_act->act_is_destory = 1;
// 動作序列數組
act_seq_t **achilles_act_seq_list = (act_seq_t **)malloc(sizeof(act_seq_t *) * achilles->act_seq_list_len);
achilles_act_seq_list[0] = achilles_normal_act;
achilles_act_seq_list[1] = achilles_destory_act;
// 將艦船對象屬性指向該動作序列數組
achilles->act_seq_list = achilles_act_seq_list;
achilles->act_seq_type = 0;
定義完成后,我們需要在游戲的每一次循環中,更新戰艦狀態和貼圖。
void craft_update_act(dfo_t *craft)
{
act_seq_t *cur_act_seq = craft->act_seq_list[craft->act_seq_type];
if (cur_act_seq->act_seq_interval == 0)
return; // 若當前戰艦無動作序列,則不進行更新
++(cur_act_seq->act_seq_interval_cnt);
if (cur_act_seq->act_seq_interval_cnt >= cur_act_seq->act_seq_interval)
{
cur_act_seq->act_seq_interval_cnt = 0;
++(cur_act_seq->act_seq_index); // 切換貼圖
if (cur_act_seq->act_seq_index >= cur_act_seq->act_seq_len)
{
cur_act_seq->act_seq_index = 0;
if (cur_act_seq->act_is_destory == 1)
{
// 在這里處理毀滅的艦船
}
}
}
}
這樣,我們就為戰艦添加了噴氣的特效。
移動敵機
移動敵機的方式更簡單。只需要將其向下移動即可。實現方式如下。
void move_enemy(dfo_t *craft)
{
map_t *cur_map = get_cur_map(craft);
craft->cur_y += craft->speed;
int top = craft->cur_y + cur_map->offset_y;
if (top > 132) // 當敵機飛過屏幕下方
reload_dfo(craft, AUTO_RELOAD, AUTO_RELOAD); // 重載敵機
}
重載敵機
在飛機大戰中,會有持續不斷的敵機生成,并且敵機的出現順序和位置都隨機。為了實現這種效果,我們采用的方式是維護一個敵機數組,當敵機飛過屏幕下方或是被擊落后,我們會回收敵機并重新加載,將其重新顯示在屏幕上。
void reload_dfo(dfo_t *craft, int pos_x, int pos_y)
{
craft->cur_x = craft->pos_x;
craft->cur_y = craft->pos_y;
if (pos_x == AUTO_RELOAD) // 如果指定重載坐標為自動重載
{
uint16_t height = get_cur_map(craft)->icon->width;
craft->cur_x = random() % (64 - height) + height / 2; // 則隨機生成一個坐標,且保證對象顯示在地圖內
}
if (pos_y == AUTO_RELOAD)
{
uint16_t width = get_cur_map(craft)->icon->height;
craft->cur_y = -(random() % 1000) - width / 2;
}
}
這樣,就能夠實現源源不斷的敵機了。
發射子彈
對于子彈而言,它和戰艦的屬性非常相似,因此我們在現有的艦船對象 dfo_t 上稍加改動即可。
typedef enum
{
Achilles, // 阿克琉斯級
Venture, // 沖鋒者級
Ares, // 阿瑞斯級,戰神級
TiTan, // 泰坦級
Bullet, // 子彈
} dfo_model_e_t; // 飛行物型號
typedef struct
{
int offset_x;
int offset_y; // 炮臺的相對位置
} arms_t; // 武裝結構體
typedef struct
{
dfo_model_e_t model; // 型號
// 運動相關
int start_x; // 飛行物的起始位置,用于計算飛行距離
int start_y;
int cur_x; // 飛行物的當前位置
int cur_y;
uint8_t speed; // 飛行物的運動速度
unsigned int range; // 射程
// 顯示相關
act_seq_t **act_seq_list; // 動畫數組
uint8_t act_seq_list_len; // 動畫數組長度
uint8_t act_seq_type; // 動畫狀態
// 攻擊相關
arms_t **arms_list; // 武器裝備數組
uint8_t arms_list_len; // 武器數組長度
} dfo_t;
那么,目前 dfo_t 結構體不僅僅可以用于艦船,也可以用于定義子彈。接下來,我們為艦船定義炮臺和子彈。
dfo_t *create_achilles() // 定義阿克琉斯級戰艦
{
// 貼圖等其他定義
achilles->damage = 8; // 定義撞擊傷害
achilles->full_life = 10; // 定義完整裝甲值
achilles->cur_life = 10; // 初始化裝甲值
achilles->arms_list_len = 2; // 設定炮臺數為2
achilles->arms_list = achilles_arms_list; // 定義炮臺數組
return achilles;
}
dfo_t *create_bullet()
{
// 貼圖等其他定義
bullet->damage = 1; // 定義射擊傷害
bullet->full_life = 1; // 定義完整裝甲值
bullet->cur_life = 0; // 初始化子彈時,默認不激活
bullet->start_x = -100; // 初始化子彈時,將其移出屏幕外不做處理
bullet->start_y = -100;
bullet->cur_x = -100;
bullet->cur_y = -100;
return bullet;
}
為了生成持續不斷的子彈,我們也采用重載的方式去生成子彈。
// 檢索未被激活的子彈
dfo_t *get_deactived_bullet()
{
for (int i = 0; i < MAX_BULLET; i++)
{
if (bullet_group[i]->cur_life <= 0)
return bullet_group[i];
}
return NULL;
}
// 觸發艦船射擊子彈
void shut_craft(dfo_t *craft)
{
if (craft->arms_list == NULL || craft->arms_list_len == 0)
return;
// 從每個炮臺重載子彈
for (int i = 0; i < craft->arms_list_len; i++)
{
dfo_t *bullet = get_deactived_bullet();
if (bullet == NULL)
return;
reload_dfo(bullet, craft->cur_x + craft->arms_list[i]->offset_x, craft->cur_y + craft->arms_list[i]->offset_y);
}
}
// 在每一次刷新時移動所有子彈
void move_bullet(dfo_t *bullet)
{
if (bullet->cur_life <= 0)
return;
map_t *cur_map = get_cur_map(bullet);
bullet->cur_y -= bullet->speed;
int bottom = bullet->cur_y + cur_map->offset_y + cur_map->icon->width;
if (bottom < 0 || (bullet->start_y - bullet->cur_y) > bullet->range)
{
bullet->cur_life = 0; // 對超出射程的子彈,取消激活
bullet->cur_x = -100;
}
}
撞擊判定
在這一步,我們將會實現對于所有對象的撞擊判定,并對對象的屬性做出對應的處理。簡單而言,撞擊判定只需要檢查兩個對象是否有像素點的重疊即可。
// 判斷兩個dfo對象 bullet craft 是否發生撞擊
int hit_check(dfo_t *bullet, dfo_t *craft)
{
if (craft->cur_y <= 0 || craft->cur_x <= 0)
return 0;
if (craft->cur_life <= 0)
return 0;
if (bullet->cur_life <= 0)
return 0;
act_seq_t *cur_act_seq = bullet->act_seq_list[bullet->act_seq_type];
map_t *cur_map = cur_act_seq->act_seq_maps[cur_act_seq->act_seq_index];
for (int bullet_bit_x = 0; bullet_bit_x < (cur_map->icon->height); bullet_bit_x++)
{
for (int bullet_bit_y = 0; bullet_bit_y < (cur_map->icon->width); bullet_bit_y++)
{
uint8_t bit = (cur_map->icon->p_icon_mask == NULL) ? cur_map->icon->p_icon_data[bullet_bit_x / 8 + bullet_bit_y] & (0x01 << bullet_bit_x % 8) : cur_map->icon->p_icon_mask[bullet_bit_x / 8 + bullet_bit_y] & (0x01 << bullet_bit_x % 8);
if (bit == 0)
continue;
int bit_cur_x = bullet->cur_x + cur_map->offset_x + cur_map->icon->height - bullet_bit_x;
int bit_cur_y = bullet->cur_y + cur_map->offset_y + bullet_bit_y;
act_seq_t *cur_craft_act_seq = craft->act_seq_list[craft->act_seq_type];
map_t *cur_craft_map = cur_craft_act_seq->act_seq_maps[cur_craft_act_seq->act_seq_index];
for (int craft_bit_x = 0; craft_bit_x < (cur_craft_map->icon->height); craft_bit_x++)
{
for (int craft_bit_y = 0; craft_bit_y < (cur_craft_map->icon->width); craft_bit_y++)
{
uint8_t craft_bit = (cur_craft_map->icon->p_icon_mask == NULL) ? cur_craft_map->icon->p_icon_data[craft_bit_x / 8 + craft_bit_y] & (0x01 << craft_bit_x % 8) : cur_craft_map->icon->p_icon_mask[craft_bit_x / 8 + craft_bit_y] & (0x01 << craft_bit_x % 8);
if (craft_bit == 0)
continue;
// 找到有效點對應的絕對坐標
int craft_bit_cur_x = craft->cur_x + cur_craft_map->offset_x + cur_craft_map->icon->height - craft_bit_x;
int craft_bit_cur_y = craft->cur_y + cur_craft_map->offset_y + craft_bit_y;
// 開始遍歷所有可撞擊對象
if (craft_bit_cur_x == bit_cur_x && craft_bit_cur_y == bit_cur_y)
{
return 1;
}
}
}
}
}
return 0;
}
全局撞擊判定,判斷地圖上所有存活對象的撞擊情況。
void global_hit_check(void)
{
// 子彈撞擊檢測
for (int j = 0; j < MAX_BULLET; j++)
{
dfo_t *bullet = bullet_group[j];
if (bullet->cur_life <= 0)
continue;
for (int i = 0; i < MAX_L_CRAFT + MAX_M_CRAFT + MAX_S_CRAFT; i++)
{
dfo_t *craft = enemy_crafts[i];
if (craft->cur_life <= 0)
continue;
if (hit_check(bullet, craft))
{
craft->cur_life -= bullet->damage;
bullet->cur_life = 0;
bullet->cur_x = -100;
if (craft->cur_life <= 0)
{
destory(craft);
}
continue;
}
}
}
// 我方飛機撞擊檢測
for (int i = 0; i < MAX_L_CRAFT + MAX_M_CRAFT + MAX_S_CRAFT; i++)
{
dfo_t *craft = enemy_crafts[i];
if (craft->cur_life <= 0)
continue;
if (hit_check(my_craft, craft))
{
craft->cur_life -= my_craft->damage;
my_craft->cur_life -= craft->damage;
// 如果艦船裝甲損毀,則摧毀艦船,將其動畫狀態置為毀滅動畫
if (craft->cur_life <= 0)
{
craft->act_seq_type = 1;
craft->cur_life = 0;
}
if (my_craft->cur_life <= 0)
{
my_craft->act_seq_type = 1;
my_craft->cur_life = 0;
g_chance--;
}
continue;
}
}
}
全局刷新
void global_update(void)
{
for (int i = 0; i < MAX_L_CRAFT + MAX_M_CRAFT + MAX_S_CRAFT; i++)
{
craft_update_act(enemy_crafts[i]); // 更新所有敵機貼圖狀態
move_enemy(enemy_crafts[i]); // 自動移動所有敵機
}
for (int i = 0; i < MAX_BULLET; i++)
{
move_bullet(bullet_group[i]); // 自動移動所有激活的子彈
}
craft_update_act(my_craft); // 更新玩家艦船狀態
shut_craft(my_craft); // 觸發玩家艦船射擊
global_hit_check(); // 全局撞擊判定
}
實現效果
接下來請欣賞筆者的操作。
文檔內容是否對您有幫助?