程式筆記:遊戲流程設計模型

前言

Super Meat Boy,應該沒幾個人會玩到100%

最早意識到製作遊戲需要一個“模式”,大概是2013年的暑假。那時候我自己在亂摸OpenGL(完全沒圖學概念,根本是浪費時間),好不容易讓畫面上出現一個正方形,於是開始嘗試製作經典的貪吃蛇遊戲。當時照着該blog的OpenGL教學弄出了框架,於是有樣學樣弄出了個“有點物件導向”的程式,雖然說現在回頭看,那真的是慘不忍睹...

不管如何,在我完成可以動的東西以後,腦中就跳出了問題:這堆程式毫無擴充性可言。當時我沒辦法加入選單畫面等額外的東西,整個程式的架構是有問題的,一直到2013下半年我嘗試用SDL重寫一個完整貪吃蛇,還是遇上程式難以擴充的問題。經過Johnson Lin前輩的指引(?),終於得到“設計模型-Design Pattern”這一個詞彙。

總之現在,我寫了一個暫時能用的框架,下面來看看。

來看Code

main.cpp
#include <iostream>

#inlcude "global.h"
#include "window.h"

#include "gameStatus.h"
#include "startScreen.h"
#include "menuScreen.h"
#include "timer.h"

bool gameIsRunning = true;

enum GameStatusFlag gameStatusFlag;

int
main(int argc, char* argv[])
{
        GameStatus* game = NULL;

        SDL_Event event;

        Timer timer;

        Window::Init("RainbowDOT", GAME_WINDOW_WIDTH, GAME_WINDOW_HEIGHT);

        gameStatusFlag = START_SCREEN;

        //Here we go!
        while (1) {

                //Swtich game status
                switch (gameStatusFlag) {
                        case START_SCREEN:
                                game = new StartScreen();
                                gameIsRunning = true;
                                break;

                        case MENU_SCREEN:
                                game = new MenuScreen();
                                gameIsRunning = true;
                                break;

                        case GAME_QUIT:
                                return 0;
                                break;
                }

                //Main game loop
                while (gameIsRunning) {                                                                                                                                                         
                        timer.Start();

                        while (SDL_PollEvent(&event))
                                        game->EventHandler(&event);

                        game->Update();
                        game->Render();

                        if(timer.GetTicks() < (1000 / GAME_FPS))
                                SDL_Delay((1000 / GAME_FPS) - timer.GetTicks());
                }

                delete game;
        }
        
        return 0;
}

檔案簡介

global.h

global.h
#ifndef GLOBAL_H
#define GLOBAL_H

#define GAME_WINDOW_RATIO       16 / 9                                                                                                                                                         
#define GAME_WINDOW_HEIGHT      600
#define GAME_WINDOW_WIDTH       GAME_WINDOW_HEIGHT * GAME_WINDOW_RATIO

#define GAME_FPS                60

//Recored current game status
enum GameStatusFlag
{
        START_SCREEN = 0,
        MENU_SCREEN,
        GAME_QUIT
};

#endif

主要是定義了一些會共同使用到的變數。
enum GameStatusFlag記錄了所有的遊戲狀態,讓程式處於某個遊戲狀態中時,可以指定要前往哪個遊戲狀態。

window.h

定義了一些SDL相關視窗和顯示的東西,不是重點不討論

gameStatus.h

gameStatus.h
#ifndef GAMESTATUS_H
#define GAMESTATUS_H

#include <iostream>
#include <SDL2/SDL.h>

using namespace std;

class GameStatus
{
        public:
                GameStatus(){};
                virtual ~GameStatus(){};

                virtual void EventHandler(SDL_Event* event){};
                virtual void Update(){};
                virtual void Render(){};

        private:
};

#endif                                                 

這個class做爲所有遊戲狀態(舉凡標題畫面,遊戲畫面,製作人名單等)的super class,能夠利用多形讓GameStatus的指標指向其他繼承到GameStatus的遊戲狀態,例如main.cpp中出現的startScren.h和menuScreen.h,來達到執行時期的動態記憶體配置和釋放。

在遊戲執行的時候,程式不太需要把“標題畫面的背景圖片”帶到“主遊戲內容”當中,因爲根本就用不到。

這樣的設計就可以把等等用不到的資源釋放掉。但缺點是,如果有共同使用到到資源,必須要重新讀取一次,造成程式的負擔。這部分還要在想辦法解決。

多形也能夠把“不同”的遊戲狀態配置到“相同”的結構上,簡化程式的結構複雜度和提升程式的再使用率。

startScreen.h 和 menuScreen.h

對應這樣的設計,各種遊戲狀態的class只需要繼承GameStatus這個super class,並且擁有EventHandler(),Update()和Render()這三個必要函式的定義,就可以套上main.cpp中的結構。

這兩個文件內容不是這麼重要,這邊就不貼出來,有須要可以參考文章底部的GitHub鏈接。

timer.h

就是個普通的計時器,背後是用SDL來測量時間的,沒啥好說。

流程簡介

  • main.cpp中的while(1)迴圈,是整個流程的開始

  • switch (gameStatusFlag)會判斷當前的遊戲狀態,去動態配置需要的資源,並且把gameIsRunning設爲ture(這邊我處理的還不是很好,要切換遊戲狀態時必須將gameIsRunning設爲flase,才能跳出下面的while(gameIsRunning)迴圈)。
    或是遊戲要求結束程式,直接傳回0,結束程式。

  • while (gameIsRunning)是整個遊戲架構的核心,開始“處理時間”,“更新資料”和“繪圖”的工作。
    多虧多型的幫助,我們可以寫出game->Update()這樣的架構來對應各種不同的遊戲狀態。
    執行完上述工作後,我用timer來控制FPS(frame per second)的大小,如果處理完這一個frame的速度比我要求的FPS還要快,就讓timer等一會兒,來固定每一次繪圖的時間。

  • 要跳出while (gameIsRunning),必須在內部將gameIsRunning設爲false(方才提到的設計不良問題)。
    跳出後,會使用delete game當前遊戲狀態用到的資源釋放掉,然後回到switch (gameStatusFlag)去配置要求的內容

一個跳出當前遊戲狀態的程式碼大概長這樣
其中gameIsRunning和gameStatusFlag都被宣告在main.cpp中,其他class必須使用extern來取得並且共用這兩份資料

test.cpp
...
//如果點擊滑鼠左鍵
if ((event->type = SDL_MOUSEBUTON) && (event->button.button == SDL_BUTTON_LEFT)) {
    //用來跳出迴圈
 gameIsRunning = false;
  
  //設定要進入的遊戲狀態
  gameStatusFlag = MENU_SCREEN;
}
...

外觀上不是非常美觀,操作上也稍顯麻煩,必須設定兩個變數才能成功

雜談

後來發現,我一開始看的SDL教學(Lazy Foo)裏面就有提到遊戲狀態的問題,裏面還提供了一些簡單的範本。
到頭來,繞圈圈的是我自己啊!

這樣的模式,目前比較明顯的問題是遊戲狀態切換時,沒辦法從當前遊戲狀態傳送資料給下一個遊戲狀態。
我能想到的解法,是帶入Lua,利用Lua的global variable來傳遞需要的資料。這部分還在嘗試,畢竟我對於Lua in C/C++的設計還不是相當熟練

參考資料

Lazy Foo' Productions: State Machines
Game Dev Geek: Managing Game States in C++

小弟的實作

Comments

comments powered by Disqus