シーン管理とは?
ゲームの開発をしていると必ずシーンの管理をする必要が出てきます。
- タイトル画面のシーン
- ゲーム本編のシーン
- ゲームクリアのシーン
簡単なゲームでもこれぐらいのシーンはあるんじゃないでしょうか?
シーン管理とは簡単に言えば画面遷移の管理みたいなものですね。
実際にはどの単位でシーン遷移するのかゲームによって異なりますが、上記のような範囲でシーンを別ける事が多いです。
このシーン管理はシーン数が多くなってくるとソースコードが増えてどんどん大変になってきます。
シーン管理の実装例
最初にプログラムを始めた人がやるシーン管理は大体こんな感じになるかと思います。
// シーン enum SCENE { SCENE_TITLE, SCENE_GAME, SCENE_CLEAR, SCENE_NONE = -1, }; // シーンの切り替え void SetScene(SCENE scene); // 現在のシーン int GetScene(void);
ゲームのシーン管理処理としてシーンの設定関数
そしてゲームループでswitch-case文を使用して
void MainLoop(void) { SetScene(SCENE_TITLE); while( ~メインループ~ ) { // 現在のシーンを取得 int CurrentScene = GetScene(); switch( CurrentScene ) { case SCENE_TITLE: ~タイトル更新処理~ break; case SCENE_GAME: ~ゲーム更新処理~ break; case SCENE_CLEAR: ~クリア画面更新処理~ break; } // シーンが切り替わっていた場合に if( CurrentScene != GetScene() ) { ~シーンの解放と次のシーンの初期化~ } } }
これだとシーンが増えるたびにメインループが増えていって大変ですよね。
でもこうやってシーン管理する人が多いと思います。
自分もC言語でプログラムをしていた学生の頃はこんなプログラムを書いていました。
でもこれだとcaseがどんどん長くなってきて読みにくくなりますよね。
なので今回はもう少し見やすいシーン管理システムをC言語で作ってみましょう!
switch-case文にしていた理由は主に4つの関数(初期化・解放・更新・描画)がそれぞれ異なるからです。
それでは異なる関数を処理を分けずに呼び出すにはどうしたらいいでしょうか?
方法の一つとして関数ポインタがあります。
今回はこの関数ポインタを使ってシーン管理システムを実装してみたいと思います。
関数ポインタとは?
関数ポインタとは関数のアドレスを保持する事の出来る型のことです。
触ったことのない人は?が頭の中に出たかと思います。
変数ポインタは変数のアドレスを保持するものでしたが、関数にもアドレスが割り当てられています。
その関数のアドレスを保持する型のポインタを作成することで関数を別の場所から呼び出すことができます。
具体例としては下記のようになります。
// テスト関数 void Print(const char* str) { printf(str); } // エントリーポイント void main(void) { // 戻り値無し。(const char*)を引数に持つ関数ポインタのFuncを作成 void (*Func)(const char*); // FuncにPrint関数のアドレスを設定 Func = Print; // Funcに設定されているPrint関数の呼び出し Func("test"); }
Funcとして定義した関数ポインタにPrint関数のアドレスを渡すことでFunc(“test”)でPrint関数を呼び出しています。
これをコンソールで実行すると結果に[test]と表示されるはずです。
このように関数ポインタを使うことで間接的に関数を呼び出すことが出来ます。
そして関数ポインタを応用することで様々な実装方法が可能になります。
// PrintFuncとしてvoid (*)(const char*)の型を定義 using PrintFunc = void(*)(const char*); void main(void) { // PrintFunc型の関数ポインタのFuncを作成 PrintFunc Func = Print; Func("test"); }
usingで定義することにより関数ポインタの型だけを定義することもできます。
これでも結果は同じです。
関数ポインタは実際の開発の現場でも多用されていることが多いです。
他の言語でも似た仕様が色々とあるので汎用性の高さが伺えます。
それでは実際にシーン管理システムを作っていきましょう。
まずswitch-case文の際にも必要になった4つの関数を関数ポインタとして定義しましょう。
SceneManager.h
// 初期化用関数 using InitFunc = bool (*)(void); // 解放用関数 using FinalFunc = void (*)(void); // 更新用関数 using UpdateFunc = void (*)(void); // 描画用関数 using RenderFunc = void (*)(void);
これで4つのベースとなる関数の定義が出来ました。
InitFunc以外は全て同じ型なので同じでも大丈夫なのですが、用途が違うので今回は別々に定義しています。
次に管理するための構造体を作成します。
// シーン処理設定用構造体 struct SceneProc { const char* Name; InitFunc Init; FinalFunc Final; UpdateFunc Update; RenderFunc Render; }; // シーン遷移用の関数型 using SetupFunc = SceneProc (*)(void);
SceneProc構造体はシーンごとの必要な関数を保持する構造体です。
1シーンにつき1構造体があれば事足りることになります。
ちなみにNameはオマケなので無くても大丈夫です。
今回はシーン名を保持させるために持たせていますがお好みでどうぞ。
SetupFuncについては次に出てきますが、シーン遷移を楽にするための関数を定義しています。
では次にシーン管理システムの実装を行っていきます。
// シーン管理の初期化 void InitializeSceneManager(void); // シーン管理の解放 void FinalizeSceneManager(void); // シーンの更新 void UpdateSceneManager(void); // シーンの描画 void RenderSceneManager(void); // シーンの遷移 bool JumpScene(SetupFunc Func); // シーン名の取得 const char* GetSceneName(void);
シーン管理システムも基本の4つの処理が必要となります。
そしてシーンの遷移関数とオマケのシーン名取得処理があります。
次に実装部分です。
SceneManager.cpp
#include "SceneManager.h" // 現在のシーン情報保持用 static SceneProc g_currentScene; // シーン管理の初期化 void InitializeSceneManager(void) { ZeroMemory(&g_currentScene, sizeof(g_currentScene)); } // シーン管理の解放 void FinalizeSceneManager(void) { // 最後は解放して終わる if( g_currentScene.Final ) g_currentScene.Final(); ZeroMemory(&g_currentScene, sizeof(g_currentScene)); } // シーンの更新 void UpdateSceneManager(void) { if( g_currentScene.Update ) g_currentScene.Update(); } // シーンの描画 void RenderSceneManager(void) { if( g_currentScene.Render ) g_currentScene.Render(); }
基本の処理はこの通りです。
管理用の構造体を初期化したのちに現在のセットされている処理があれば更新と描画の処理を呼び出すようになっています。
これで関数ポインタに対して関数さえ渡してしまえば勝手に関数を呼び出してくれるようにできます。
次にシーンの遷移処理です。
// シーンの遷移 bool JumpScene(SetupFunc Func) { // 現在のシーンを解放する if( g_currentScene.Final ) g_currentScene.Final(); ZeroMemory(&g_currentScene, sizeof(g_currentScene)); bool ret = true; // 次のシーンがあれば初期化する if( Func ) { g_currentScene = Func(); if( g_currentScene.Init ) ret = g_currentScene.Init(); } return ret; } // シーン名の取得 const char* GetSceneName(void) { if( g_currentScene.Name ) return g_currentScene.Name; return "Unknown"; }
JumpSceneでは下記の手順でシーンの切り替えを行っています。
- 現在のシーンを解放
- シーン情報のリセット
- SetupFunc型で渡された関数ポインタを実行して次のシーンの情報取得
- 次のシーンの初期化
手順としてはこんな感じになっています。
ここまで来たらある程度はどうやって実装するのか予想できた人も多いと思います。
では次のページで実際にシーンを作っていきましょう。
シーンの実装
シーンはどんどん増えていくものなので実装はなるべく単純かつ手間を減らせる形で対応したいです。
これまでのシステム実装はそのための布石として準備をしてきました。
ただ必要な4種類の基本処理と遷移処理。
シーン側でもこれは必須です。
でも実装するたびに呼び出す場所を増やす必要は無くしています。
ではテスト用のシーンを実装してみましょう。
TestScene.h
#pragma once #include "SceneManager.h" // テストシーンのセットアップ関数 SceneProc SetupTestScene(void);
ヘッダーに必要な記述はこれだけです。
かなり簡単になりましたね!
では次に実装部分を見ていきましょう。
TestScene.cpp
#include "TestScene.h" // 初期化 static bool SceneInit(void) { return true; } // 解放 static void SceneFinal(void) { } // 更新 static void SceneUpdate(void) { } // 描画 static void SceneRender(void) { } // シーン処理の設定 SceneProc SetupTestScene(void) { SceneProc proc = { "Test", SceneInit, SceneFinal, SceneUpdate, SceneRender, }; return proc; }
実装側もこれだけです。
Setup以外の各関数はゲームごとの処理なので今回は空ですが、そこは今まで通りですね。
SetupTestScene関数ではTestSceneで使用する関数とシーン名を設定して戻り値として返しています。
これでもう使い方も想像つきましたよね?
ではDirectX9の『テクスチャーを使った描画』までの記事で実装したWinMainにこのシーンシステムを組み込んでみましょう。
#include "Window.h" #include "DirectX.h" #include "TestScene.h" // エントリーポイント int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { int width = 640; int height = 480; if( !CreateSimpleWindow(TEXT("DirectX9"), 0, 0, width, height) ) { return false; } InitializeDirectX(GetWindowHandle(), width, height, false); // シーンシステムの初期化 InitializeSceneManager(); // テストシーンへ遷移する JumpScene(SetupTestScene); // メインループ while( !IsQuitMessage() ) { if( !UpdateWindowMessage() ) { UpdateSceneManager(); RenderSceneManager(); } } // シーンシステムの終了 FinalizeSceneManager(); FinalizeDirectX(); return 0; }
これでWindowの作成とDirectXの初期化が完了した後にシーンシステムの管理下に入りました。
後は各シーンでゲームの更新や描画をしていけばキレイなコードになったんじゃないでしょうか?
時間に余裕のある人は『テクスチャーを使った描画』で実装していたRenderやリソースの初期化をシーン側で実装してみましょう。
意外と簡単にできてしかもコードがスッキリ気持ちよくなりますよ!
今回の記事の実装まで行ったサンプルプロジェクトを下記にアップしてあります。
プロジェクトファイル