この記事は、chatgptに文字の色付け、装飾を手伝ってもらっています。記事の内容は自分でまとめたものになります。
学習内容と記載内容
書籍「ゲームプログラミングC++」の第一章で作成するPongゲームを作っていきます。
初めに、学習環境を作成し、ゲームを動かす環境を作るためにゲームクラスの実装を行っていきました。
GameClassの実装、描画の仕組みについて、描画設定の実装について記載します。ゲームの実装に関しては、すでに知っている内容のため説明を省きます。
https://www.shoeisha.co.jp/book/detail/9784798157610
Game Classの実装
◇Game::Initialize ①
Initialize関数は、初期化が成功したらTrue,失敗したらfalseを返す関数。ここでは、SDKライブラリのSDL_Init関数で初期化をする必要があります。
■SDL_Initとは
SDLのサブシステムの初期化関数。
SDLには複数の機能があり、必要な機能を明示的に初期化する必要があります。
int SDL_Init(Uint32 flags);
SDL_Initフラグの一部について紹介をします。
- SDL_INIT_TIMER:タイマーAPIを使う
- SDL_INIT_AUDIO:サウンド再生機能を使う
- SDL_INIT_VIDEO:ウィンドウを表示・描画を行う
- SDL_INIT_JOYSTICK:ジョイスティック入力
- SDL_INIT_GAMECONTROLLER:ゲームパッド入力
- SDL_INIT_EVENT:イベント処理
- SDL_INIT_EVERYTHING:上記まとめて全部初期化
などがあります。今回は、ウィンドウを表示するだけなので、SDL_INIT_VIDEOのフラグだけ立てます。
◇Game::Initialize ②
SDLの初期化が完了したら、SDL_CreateWindow関数を宣言します。
mWindow = SDL_CreateWindow(
"Game Programing C++ (1章)",
WINDOW_POSITION_X,//ウィンドウのX位置
WIMDOW_POSITION_Y,//ウィンドウのY位置
WINDOW_SIZE_X,//ウィンドウサイズX
WINDOW_SIZE_Y,//ウィンドウサイズY
0//ウィンドウフラグ設定
);
■SDL_CreateWindow
SDL_CreateWindowとは、ウィンドウを作る関数
-
title
: ウィンドウのタイトル文字列 -
w
: ウィンドウの幅 -
h
: ウィンドウの高さ -
flags
: ウィンドウの種類や機能を指定するフラグ(例:フルスクリーン、リサイズ可など) - 戻り値 : 成功なら
SDL_Window*
、失敗ならNULL
SDL_Window* SDL_CreateWindow(const char *title,
int w,
int h,
Uint32 flags);
詳細な関数の紹介はここで行われています。
https://maruhiro-ver0.github.io/sdl2manual-ja/SDL_CreateWindow.html
◇Game::ShutDown
GameShutDownでは、Initializeの逆のことを行います。 SDL_DestoroyWindow関数を使用してWindowを破棄し、SDL_QuitでSDLを終了させます。
■SDL_DestroyWindow
void SDL_DestroyWindow(SDL_Window *window);
この関数は、作成したウィンドウの破棄をします。
■SDL_Quit
SDLライブラリのクリーンアップ処理を行う関数です。
SDL_Initで初期化したサブシステムをすべて終了します。SDLを使い終わったら最後に呼ぶべき処理です。
https://maruhiro-ver0.github.io/sdl2manual-ja/SDL_Quit.html
◇Game::RunLoop
RunLoop関数ではゲームループを繰り返し実行します。GameクラスのmIsRunnningがfalseになったら繰り返し処理を終了するようになっています。
void Game::RunLoop()
{
while (mIsRunning())
{
ProcessInput();
UpdateGame();
GenerateOutput();
}
}
これでゲームを動かすための環境が出来ました。次は、ゲームの実装の入り口であるmain関数の実装をします。
Main関数の実装
GameClassではゲームのふるまいの処理を書きました。C++では、どんなプログラムもmain関数で行うので、GameClassの初期化処理、ループ処理を実装していきます。
#include "Game.h"
int main(int argc, char** argv)
{
Game game;
bool success = game.Initialize();
if (success)
{
game.RunLoop();
}
game.Shutdown();
return 0;
}
Main関数はこのような実装を行いました。最初にGameクラスのインスタンスを作り、gameの初期化を行います。それが成功したら、ゲームループを回し、ゲームループが終了したらシャットダウンを行うようにしました。
これを実装することで白い背景のウィンドウが表示されるようになりました。
Pongの基本的な入力処理
デスクトップマシンのOSでは、ユーザーがアプリケーションウィンドウに対して行える操作がいくつかあります。例えば、ウィンドウを動かしたりサイズを変えたり、消したりなどです。
これらの様々な処理の実装はイベントを使用するのが一般的です。
そうすることで、ユーザーが何かしらの操作をしたときに、OSからイベントを受け取り自身がイベントだけに応答することが出来ます。
SDLでは、OSから受け取ったイベントを内部のキューで管理をするそうです。
このキューには、入力デバイスに関するイベントだけではなく、様々なウィンドウ操作のイベントも含まれます。フレームごとにイベントがないかを調べ、キューに入っている個々のイベントについて処理を行うか、無視するかを選びます。
◇Game::ProcessInput() ①ウィンドウを閉じる処理の実装
イベントは一種の入力なので、GameクラスにProcessInputにイベント処理を書いていきます。
void Game::ProcessInput()
{
SDL_Event event;
//キューにイベントがあれば繰り返す
while (SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_QUIT:
mIsRunning = false;
break;
default:
break;
}
}
}
キューからイベントを読み込むためには、空のSDL_Eventを生成します。
そして、次にキューにイベントがないかをチェックするために、SDL_PollEventを使ってイベントがあるかどうかをチェックします。
SDL_Eventのメンバーであるtypeには、SDL_EventTypeの列挙データが入っています。
この値を使ってswitchを使って各イベントごとの処理を実装していくのがセオリーになります。
今回は、ウィンドウ閉じるボタンを押したときにゲームを終了させたいので、mIsRunnningをfalseになる処理を実装します。
■SDL_Event
SDL_Eventは異なる種類のイベントを持つ共用体です。
ディスプレイイベントや、マウス移動イベント、コントローラー関連のイベントなどを持っているクラスです。
https://maruhiro-ver0.github.io/sdl2manual-ja/SDL_Event.html
■SDL_PollEvent
未処理のイベントをキューから得ます。未処理のイベントがあるときは1,ない時は0を返します。
https://maruhiro-ver0.github.io/sdl2manual-ja/SDL_PollEvent.html
◇Game::ProcessInput() ②エスケープキーでゲームを終了させる
次は、エスケープキーが押されたときもゲームを終了させるようにします。
SDL_GetKeyboardStateでキーボード全体の状態を把握する必要があります。
//キーボード状態を取得
const Uint8* state = SDL_GetKeyboardState(NULL);
if (state[SDL_SCANCODE_ESCAPE])
{
mIsRunning = false;
}
■SDL_GetKeyboardState
この関数はキーボードの状態を得ることが出来る関数。
const Uint8* SDL_GetKeyboardState(int* numkeys)
ここで、SDL_GetKeyboardStateが返すポインターを開放してはいけません。理由としては、そのポインターがSDLライブラリ内部で保持している配列だからです。ユーザーのプログラムがdeleteを読んでしまうとSDLがアクセスする際に「解放済み」の領域に破棄してしまうことになるので、クラッシュが発生してしまいます。
また、返す値は変えてはいけないのでconstで受け取るようにします。
このstateにキーボードの現在の状態が格納された配列を受け取ることが出来たので、この配列のインデックス参照をつかって、特定のキーを確認する。
特定のキーはSDL_SCANCODE列挙体を使う。
これで、エスケープキーを押したときにゲームを終了できるようになりました。
基本的な2Dグラフィックスについて
◇モニターに画面を映す仕組み
ディスプレイはラスターグラフィックスを使用されています。ピクセルと呼ばれる画素が2次元の格子状に並び、個々の異なる色と輝度の組み合わせによりは画面をつながった1つの画像として認識をします。
ラスターディスプレイの解像度は、ピクセルグリッドの幅と高さのことで、1980×1080は、横に1920個のピクセル、縦に1080個並べているという意味になります。
カラーディスプレイは個々のピクセルが発する色を足し合わせて色彩を作っていきます。一般的にはRGBを組み合わせて作ります。
◇カラーバッファ
ディスプレイがRGBの画像を映すためには、個々のピクセルの色を知る必要があります。コンピューターグラフィックスでは、メモリ内のカラーバッファと呼ばれる場所に画面全体の色情報がおかれます。ディスプレイはカラーバッファを参照して画面を描画していきます。
カラーバッファがどれほど多くのメモリを使うかは、個々のピクセルを表現するビット数に依存します。これを色深度といいます。
例えば、24ビットの色深度でRGBはそれぞれ8ビット使います。2の24乗色。すなわち16777216通りの色が使えます。アルファ値も格納したい場合は、32ビットになります。
通常のディスプレイ解像度が使用しているメモリは、1920×1080×4バイトでおよそ7,9MB使用していますね。
◇ダブルバッファ
ゲームは1秒に何十回も更新されます。更新した時にカラーバッファも同じ頻度で更新すれば動きの錯覚が生まれます。
しかし、現在のディスプレイ技術では、画面を一瞬のうちに更新することはできません。更新には何らかの順番があります。グリッドごとや列ごとに書き換えるなど。そのため、ディスプレイ全体を更新するには、わずかに時間がかかってしまいます。
例えば、ゲームがフレームAの画像データをカラーバッファに書き込むとします。次にディスプレイがフレームAを画面に映すためにカラーバッファから読み出しを始めます。ここで、ディスプレイがフレームAを書き出す前にフレームBの画像データに上書きをしたときには、フレームAとフレームBの一部が表示されてしまうため、つなぎ目で両者がずれてしまいます。これをティアリングといいます。この現象は、例えばFPSゲームで視点を振り回したときにディスプレイの上の部分と下の部分でずれてしまう現象のことですね。
これを防ぐためには、ダブルバッファと垂直同期を用いることで解決することが出来ます。
■ダブルバッファ
2枚のバッファを使って、描画する仕組みです。1枚のディスプレイを表示している間にもう1枚にフレーム描き込む。表示用と描画用を用意することで、画面の更新途中でティアリングを防ぎます。
フローのイメージ
- 描画開始:バックバッファに次のフレームをレンダリングする
- 描画完了:ディスプレイのリフレッシュタイミングに合わせて、バックとフロントをスワップ
- 表示:新しいフロントバッファが画面に映り、古いフロントは次のバックになります。
- 以後繰り返し。
■垂直同期 vsync
ダブルバッファを使用していたとしてもティアリングは、発生します。理由としてはゲームがバッファAの書き込みを開始するときに、バッファAの内容を読みだしていた時に発生してしまいます。
それを解決するのが垂直同期です。
垂直同期は、ディスプレイのリフレッシュタイミングに合わせてフレームを表示する仕組みです。画面が切り替わる瞬間にバッファを入れ替えることでティアリングが無くなります。ただ、この機能を使うと、リフレッシュタイミングを待つまで更新を待たないといけないため相対的にFPSが下がります。
ゲームでは、設定項目に垂直同期の項目を用意することでユーザーに画面の更新の変更を選択させる形式にさせていますね。
基本的な2Dグラフィックスの実装
◇初期化とシャットダウン
SDLのグラフィックスコードを使うには、SDL_CreateRendererを用いてSDL_Rendererを作成します。 何かを描画使用とするたびに、SDL_Rendererを参照する必要があるので、メンバー変数にmRendererを作成しアクセスしやすくします。
Game::Initializeでウィンドウを作成処理後にSDL_Rendererを作成します。
mRenderer = SDL_CreateRenderer(
mWindow,
-1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
レンダラーを閉じる際には、Game::ShutDownにSDL_DestoryRenderer()を実装し、引数にm_Renderer
を指定するだけでいいです。
◇描画設定
グラフィックスライブラリにおいて、描画の全体の流れは通常3つのステップになります。
- バックバッファを単色でクリアする。
- ゲームのシーン全体を描画する。
- フロントバッファとバックバッファを交換する。
今回は、1,3のステップを設定します。
処理は、Game::GenerateOutputに書きます。バックバッファをクリアするためには、描画色をSDL_SetRenderDrawColorで指定します。この関数はレンダラーへのポインターを受け取るほか、RGBAの成分を受け取ることが出来ます。
今回は黒の背景にします。
SDL_SetRenderDrawColor(mRenderer, 0, 0, 0, 255);//描画に使う色を指定
mRendererに入った値をSDL_RenderClearを呼び出して、バックバッファを現在の描画色でクリアさせます。
最後に、SDL_RenderPresentを使用して、フロントバッファを入れ替えることで描画の更新が行われていきます。
まとめ
今回は、Pongの実装に必要なGameクラスの実装、main関数の処理、2Dレンダリングの基礎知識、描画の設定について記事にまとめました。
インゲームの実装は、書いてはいませんが第一章の課題であった2人対戦の実装を実装できました。また、当たり判定用の処理を作成し、コードを短くできるようにしました。
ゲーム開発の基本的な知識を学びなおしをすることが出来たのでとても楽しく学ぶことが出来ました。
2章ではスプライトやゲーム開発の設計について学んでいきます。
最後まで記事を読んでいただきありがとうございました。