--- Tu 2023/6/26 ***「綜合建築、音樂、繪畫、雕塑、詩歌、文學和舞蹈之後,將電影加入藝術的行列,名為『第八藝術』。而無數的電子遊戲前輩和工作者,將遊戲推上了『第九藝術』的行列。」*** **- 森納映畫 喬伊** ![](https://hackmd.io/_uploads/H11DyJPun.jpg) ### **[本專題 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++的涵式庫,功能只有接收輸入、渲染材質等,其他諸如碰撞偵測、實體創建、遊戲狀態等,都要自己從頭寫。因為我下學期對於比較低階的開發有股莫名的衝動,所以才會開始研究。 我先介紹一下我目前對遊戲開發的理解 ![](https://hackmd.io/_uploads/rJXOoHD_h.jpg) 遊戲狀態(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++用的,而我也確實收穫了很多。遊戲開發確實不是我目前想首位鑽研的目標,但我希望我能一直保持著這個興趣。(就看這個系列文之後能不能繼續就知道了)