---
Tu 2023/6/26
***「綜合建築、音樂、繪畫、雕塑、詩歌、文學和舞蹈之後,將電影加入藝術的行列,名為『第八藝術』。而無數的電子遊戲前輩和工作者,將遊戲推上了『第九藝術』的行列。」*** **- 森納映畫 喬伊**

### **[本專題 github repo 連結](https://github.com/Eric-Tu32/2D-Game-Attempt-SDL2)**
## 一、前言
雖然我目前只有寫過機器學習相關的文章,但其實我對遊戲開發一直很有興趣,但開發一個有品質的遊戲往往需要大量的心血和時間,也因此一直沒有嘗試。
雖然之前有試過Unity,但我完全搞不懂那個UI怎麼用(有可能是我不常接觸)。而最近剛好暑假有時間,我想說就來玩玩看。
C是我第一個接觸到的程式語言,但後來的專案幾乎都用python實作因此生疏許多,最近剛好想學static type的語言,我就索性決定用C++來寫小遊戲,一方面熟悉C\++的使用方式,一方面滿足我的小願望。
這次的介紹與以往不太一樣,因為牽涉到太多檔案所以我會附上Github的連結,請搭配食用。我基本上只介紹大概念,細節的部分就自己看我的程式碼ㄅ。
## 二、SDL2 簡介
SDL2不像Unity或Unreal一樣是遊戲引擎,它是一個C++的涵式庫,功能只有接收輸入、渲染材質等,其他諸如碰撞偵測、實體創建、遊戲狀態等,都要自己從頭寫。因為我下學期對於比較低階的開發有股莫名的衝動,所以才會開始研究。
我先介紹一下我目前對遊戲開發的理解

遊戲狀態(gameState)是拿來決定比如開始畫面、遊戲畫面、結算畫面等的參數,而每個遊戲狀態下,都會去偵測玩家的操作決定是要更改遊戲狀態(update gameState)還是更新畫面內容(update + graphic)。
當然,我寫的程式碼可能不是那麼好,畢竟我也剛開始學,還沒使用design pattern去保證我的可延性(extensibility)和可維護性,可能之後會專門出一篇改吧。
## 三、實作過程
這次只是我覺得開始熟悉SDL2後的第一個紀錄,所以只會挑重點紀錄,但SDL2的相關資料沒有Unity多,所以我覺得對於需要的人應該會有相當大的幫助。
但後來我發現我有點懶,所以還是先對SDL2有些基本理解再回來翻ㄅ。
推薦自學資源:
* [Codergopher的SDL2系列](https://www.youtube.com/playlist?list=PL2RPjWnJduNmXHRYwdtublIPdlqocBoLS):可以學到SDL2一開始的基本設置和一些C++基本概念,而且他講蠻好笑的,相當推薦(但是該系列貌似還沒做完就腰斬了)
* [Mike Shah的SDL2系列](https://www.youtube.com/playlist?list=PLvv0ScY6vfd-p1gSnbQhY7vMe2rng0IL0):這個內容十分的完整詳細,還會導讀SDL2的Documentation,我沒有全部看完,基本上只有再遇到特定問題的時候會去查一下
我想紀錄(你們可能學到)的內容:
1. 初始化和創建視窗 + 渲染材質
2. 實體創建
3. 玩家創建和基本2D控制
4. 敵人創建和基本AI
5. 碰撞偵測
6. 遊戲狀態:重新開始
### 初始化和創建視窗 + 渲染材質
貌似是固定寫法,建議開始的時候先上網查一下,這裡的重點是確保初始化沒問題、創建空白視窗和創建渲染器(renderer),之後有關遊戲畫面的展示都是由Renderer負責。
我這邊把視覺相關的放到一個header file,在github中是RenderWindow的檔案。
```cpp
#pragma once
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include "Entity.hpp"
class RenderWindow
{
public:
RenderWindow(const char* p_title, int p_w, int p_h);
SDL_Texture* loadTexture(const char* p_filePath);
void cleanUp();
void clear();
void render(Entity* p_entity);
void render(int p_x, int p_y, SDL_Texture* p_tex);
void display();
private:
SDL_Window* window;
SDL_Renderer* renderer;
};
```
這裡在Marty的教學中大多都會講到,我這邊有多加東西的是render(Entity*)的部分,但和影片中提到的概念是一模一樣的。
### 實體創建
這邊指的是Entity Class,我有透過這個稍微理解C++的OOP怎麼用,還有像繼承、多形之類的。在操作的過程也難免碰到一些指標操作,雖然對使用方法多少有點概念,但還是好難。
程式碼請參見repo中Entity、Player、Enemy的檔案
Entity包含了實體應該要有的基本資料:位置、材質、角度、碰撞箱等。而Player和Enemy是Entity的子類別,Player和Enemy多了移動、衝刺相關的涵式,但重點是最後的update()
Player和Enemy的update會更新實體每幀的位置、速度、角度等信息,計算完後再由renderer更新遊戲的畫面。
## 玩家創建和基本2D控制
在main.cpp中的update()就是用來接收來自玩家的操作
```cpp=
void update()
{
lastTick = currentTick;
currentTick = SDL_GetPerformanceCounter();
deltaTime = (double)((currentTick - lastTick)*1000 / (double)SDL_GetPerformanceFrequency());
//keyboard detection
bool up_button_down = false;
bool down_button_down = false;
bool left_button_down = false;
bool right_button_down = false;
const Uint8 *state = SDL_GetKeyboardState(NULL);
if (state[SDL_SCANCODE_W]) up_button_down = true;
if (state[SDL_SCANCODE_A]) left_button_down = true;
if (state[SDL_SCANCODE_S]) down_button_down = true;
if (state[SDL_SCANCODE_D]) right_button_down = true;
//mouse detection (1-left key, 3-right key)
int mouse_x, mouse_y;
bool left_mouse_down = false;
bool right_mouse_down = false;
if(SDL_GetMouseState(&mouse_x,&mouse_y) & 1) left_mouse_down = true;
if(SDL_GetMouseState(&mouse_x,&mouse_y) & 4) right_mouse_down = true;
//player angle
Vector2f player_to_mouse;
if (mouse_x != 0 || mouse_y != 0)
{
player_to_mouse.x = mouse_x - (double)(player->getPos().x + player->getCurrentFrame().w / 2.0d * player->getScale());
player_to_mouse.y = mouse_y - (double)(player->getPos().y + player->getCurrentFrame().h / 2.0d * player->getScale());
}
//Get our controls and events
while (SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_QUIT:
gameRunning = false;
break;
}
}
bool playerHit = false;
for (long long unsigned int i=0; i<enemies.size(); i++)
{
if(player->isHit(enemies[i])) playerHit = true;
}
gameState = player->update(deltaTime, left_button_down, right_button_down, up_button_down, down_button_down, left_mouse_down, right_mouse_down, player_to_mouse, playerHit);
for (long long unsigned int i=0; i<enemies.size(); i++)
{
Vector2f enemy_to_player;
enemy_to_player.x = player->getPos().x - enemies[i]->getPos().x;
enemy_to_player.y = player->getPos().y - enemies[i]->getPos().y;
enemies[i]->update(deltaTime, enemy_to_player, false);
}
}
```
這裡能說的就非常多了:
* **3-5行**:熟悉遊戲開發的人應該都知道deltaTime能在不同framerate下控制東西的速度,更詳細的可以自己去查。
* **7-17行**:這裡是有關鍵盤輸入的部分,最重要的點就是不要使用SDL_Event中的鍵盤偵測,在操作上會出現巨大的延遲讓你完全沒有遊戲體驗。
* **19-32行**:偵測滑鼠輸入以及位置,每個鍵有各自代表的一個整數,要自己去查官方文件。
* **34-43行**:神奇的SDL_Event,我也還沒完全搞懂,但目前只把它拿來關閉視窗。
* **45-49行**:偵測玩家的碰撞信息,決定等等player->update的結果。
* **51行**:player->update()會更新玩家實體的所有資訊,並回傳新的gameState,決定要進入結束畫面還是繼續遊戲。
* **53-60行**:更新敵人實體的資訊。
### 敵人創建和基本AI
弄到這邊讓我頭很痛,因為我根本不清楚temporary object和那一堆指標怎麼處理,我也還沒弄懂要怎麼釋放敵人占存的記憶體空間,目前就先這樣寫著,希望有人能在留言中指點我。
```cpp
//Entities
std::vector<class Entity*> entities;
Player* player = new Player(Vector2f(500,300), playerTexture, 2, 0.5f);
//enemy vectord
std::vector<class Enemy*> enemies;
//enemies
void load_Entities()
{
for (int i=0; i<1; i++)
{
Vector2f p_pos;
p_pos.x = 100+100*i;
p_pos.y = 200;
Enemy* enemy = new Enemy(p_pos, enemyTexture, 2, 0.3f);
enemies.emplace_back(enemy);
entities.emplace_back(enemy);
}
entities.emplace_back(player);
}
```
### 碰撞偵測
```cpp
SDL_bool Entity::isHit(Entity* obj)
{
hitBox.x = pos.x;
hitBox.y = pos.y;
hitBox.w = textureWidth * scale;
hitBox.h = textureHeight * scale;
SDL_bool isCollided;
SDL_Rect objhitbox = obj->getHitBox();
const SDL_Rect* selfhitboxptr = &hitBox;
const SDL_Rect* objhitboxptr = &objhitbox;
SDL_Rect inter;
inter.x = 0;
inter.y = 0;
inter.w = 0;
inter.h = 0;
isCollided = SDL_IntersectRect(selfhitboxptr, objhitboxptr, &inter);
return isCollided;
}
```
沒啥好說,有問題問我
### 遊戲狀態:重新開始
上面有提到player實體會在update的時候回傳新的gameState,我這邊的設定是當碰到敵人後就會進結束畫面,然後按一下滑鼠左鍵就會重新開始遊戲。
```cpp=
void endScreen()
{
//Get our controls and events
while (SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_QUIT:
gameRunning = false;
break;
}
}
int mouse_x, mouse_y;
bool left_mouse_down = false;
if(SDL_GetMouseState(&mouse_x,&mouse_y) & 1) left_mouse_down = true;
if (left_mouse_down) gameState-=1;
window.clear();
window.render(0, 0, bgTexture);
window.display();
}
const int fps = 60;
const int frameDelay = 1000/fps;
Uint32 frameStart;
int frameTime;
int main(int argc, char* args[ ])
{
load_Entities();
while (gameRunning)
{
frameStart = SDL_GetTicks();
if (gameState == 1)
{
update();
graphics();
}
if (gameState == 2)
{
endScreen();
}
frameTime = SDL_GetTicks() - frameStart;
if (frameDelay > frameTime) SDL_Delay(frameDelay - frameTime); //Cap to 60 fps
}
window.cleanUp();
SDL_Quit();
return 0;
}
```
最後提幾個重點
* **fps/frameDelay**:每次GameLoop的時候會做一次計時,透過SDL_Delay限制最大fps(60),以保證電腦不會將所有CPU資源投入到遊戲中。
* **main函數的引數**:SDL2的要求寫法,不然跑不動。
## 四、未來方向
這篇雖然涵蓋了很多基本內容,但知道遊戲開發的就知道其實要研究的多到數不清,我這邊列幾個之後有時間想繼續研究的項目。
* 加入design pattern,提高程式碼品質
* 角色、場景動畫(sprite animation)
* 光影、粒子特效
* 敵人實體的生成與消滅
* 敵人AI
* 基本戰鬥系統
* 劇情、玩法設計
遠期目標(不太可能,有時間+心血來潮有可能)
* 多人連機(ENet套件)
* 發佈到Steam上
* 參加Game Jam或黑客松
3D遊戲的話還是乖乖去用遊戲引擎,從頭開始開發成本太高了。
### 五、結語
我很喜歡的2D遊戲很多。Undertale、To the moon的劇情,元氣騎士、The Eldest Soul的roguelike風格和Rain World、Hollow Knight之類玩法豐富的遊戲。但我也深諳要做出這種品質所需要的人力和熱情,以及面對可能血本無歸的勇氣。
這個專案我本來只是拿來熟悉C++用的,而我也確實收穫了很多。遊戲開發確實不是我目前想首位鑽研的目標,但我希望我能一直保持著這個興趣。(就看這個系列文之後能不能繼續就知道了)