GameDev #1 ゲームプログラミング [Devlog #014]
Table of Contents
ゲーム開発者の教科書:Game Programming in C++ を読んで理解したことについてを要約します(内容の転載を避け、詳しく説明しすぎないように配慮します)
ゲームプログラミング in C++
環境構築
VisualStudio2022でプロジェクトの作成
- (新規作成->プロジェクト->)新しいプロジェクトの作成->空のプロジェクト
- プロジェクト名(ソリューション名)と保存パスを指定
“ソリューションとプロジェクトを同じディレクトリに配置する"にチェック
- 作成
- SDL等のライブラリを設置($(SolutionDir)\External\フォルダ以下)
- ソリューションのプロパティを変更(参考サイト)
- インクルードディレクトリの指定
- 構成プロパティ->C/C++->全般->追加のインクルードディレクトリ(注意点)
- 追加のライブラリディレクトリの指定
- 構成プロパティ->リンカー->全般->追加のインクルードディレクトリ
- 追加の依存ファイルの指定
- 構成プロパティ->リンカー->入力->追加の依存ファイル
- DLLをビルド時にコピー
- 構成プロパティ->ビルドイベント->ビルド後のイベント->コマンドライン
- インクルードディレクトリの指定
※サンプルソースコードではプラットフォームをWin32に設定しているので注意
ゲームプログラミング
ゲームループとゲームクラス
ゲームループ(game loop)は、ゲームプログラム全体の流れを制御するループである
シングルスレッドのゲームループに対して、マルチスレッドのゲームループは複雑になる
フレームの中身
ゲームは各フレームで次の3つのステップを実行する
- 入力があれば処理する
- 例:デバイス入力、インターネット経由で受信したデータ、カメラ映像、GPS情報
- ゲームワールドを更新する
- 例:キャラクター、ユーザインターフェースのパーツ、ゲームに与える全ての要素
- 出力するものを生成する
- 例:グラフィクス、オーディオ、フォースフィードバック(振動)、インターネット経由で送信するデータ
ゲームクラスの骨組みを実装する
Game.h
Gameクラス
Initialize()
関数はGameクラスを初期化する
RunLoop()
関数はゲームループを実行する
Shutdown()
関数はゲームを終わらせる
ProcessInput()
関数、UpdateGame()
関数、GenerateOutput()
関数はゲームループの3つのステップに対応する
mIsRunning
は、ゲームループを続行するか否かを示すフラグ
Game.cpp
Game::Initialize()
SDL_Init()
関数は、SDLライブラリを初期化する
今のところ、初期化する必要があるのはビデオサブシステムだけ(SDL_INIT_VIDEO
)
SDL_Log("Unable to initialize SDL: %s", SDL_GetError())
は、SDLでメッセージをコンソールに出力する簡単な方法である
SDL_GetError()
関数は、エラーメッセージを文字列で返す
SDL_CreateWindow()
関数でウィンドウを作る
ウィンドウ作成にはフラグを設定できる(SDL_WINDOW_FULLSCREEN
でフルスクリーンモードを使う)
Game::RunLoop()
もしmIsRunning
がfalseになったら繰り返しをやめる
Game::Shutdown()
SDL_DestroyWindow()
関数を使ってSDL_Window
を破棄する
SDL_Quit()
でSDLを終わらせる
メイン関数
main.cpp
どんなC++プログラムでも入り口はmain関数である
最初にGameクラスのインスタンスを作る
ゲームを初期化(game.Initialize()
)出来たら、game.RunLoop()
を呼び出してゲームループに入る
ループが終わったらgame.Shutdown()
を呼び出してゲームを終わらせる
基本的な入力処理
SDLでは、OSから受け取ったイベントを内部のキューで管理する
フレームごとにイベントがないかキューを調べて、キューに入っている個々のイベントについて処理を行う
ウィンドウを動かす操作などのイベントは、(ただ無視するだけで)SDLが自動的に処理する
Game.cpp
Game::ProcessInput()
イベントも一種の入力なので、イベント処理はGame::ProcessInput()
関数の中で実装する
SDL_PollEvent()
関数は、キューにイベントがあればtrue
を返す(Game::ProcessInput()
関数はこのtrue
が返される限り、SDL_PollEvent()を呼び出す
)
SDL_Event
のtype
メンバ変数には、受け取ったイベントの種類が入り、ここではSDL_QUIT
イベントを受け取るとmIsRunning
にfalse
を設定する(RunLoop()
関数内部のwhileループを停止させる)
SDL_GetKeyboardState(NULL)
関数は、キーボードの現在の状態が格納された配列へのポインタを返す
この配列のインデックス参照で特定のキーを確認できる(ここではSDL_SCANCODE_ESCAPE
でエスケープキーが押されたかを確認する)
基本的な2Dグラフィクス
ほとんどのディスプレイはラスターグラフィクスを使っており、最終的にほとんどのコンピュータゲームはピクセルごとにRGBの色を出力している
カラーバッファ
メモリ内のカラーバッファに画面全体の色情報が置かれ、ゲームループの"出力生成"の段階で毎フレーム書き込まれる
一般的な色深度は24ビットで、RGBの各要素に8ビット使われる(アルファも含めると色深度は32ビットになる)
各画素4バイトの場合、1080pで約7.9MB
最近では、RGBの各画素に16ビット使うゲームもある
色成分の値をコードで参照する方法として、各チャンネルの値を0~255の符号なし整数とするか0.0~1.0の小数に正規化するかの2種類がある(SDLライブラリは符号なし整数値を期待する)
ダブルバッファ
画面のティアリングを防止するため、フロントバッファとバックバッファを用いるダブルバッファと呼ばれる方法を利用する
ゲームがフロントバッファとバックバッファを交換する際に、バックバッファへの書き込みが完了するまで待機するアプローチを垂直同期(vsync)と呼ぶ
基本的な2Dグラフィクスの実装
Game.cpp
Game::Initialize()
SDL_CreateRenderer()
関数でSDLレンダラーを作成し、SDL_Renderer
オブジェクトを参照してレンダラーに描画させるようにする
Game::Shutdown()
SDL_DestroyRenderer()
関数を呼び出す
描画の全体的な流れは次の3つのステップとなる
- バックバッファを単色でクリアする
- ゲームのシーン全体を描画する
- フロントバッファとバックバッファを交換する
Game::GenerateOutput()
SDL_SetRenderDrawColor()
関数で、バックバッファをクリアする描画色を指定する
SDL_RenderClear()
関数で、バックバッファを現在の描画色でクリアする
ゲームシーン全体の描画は後で実装するとして、
最後にSDL_RenderPresent()
関数で、フロントバッファとバックバッファを交換する
壁とボールとパドルの描画
Game::GenerateOutput()
SDLで塗りつぶされた長方形を描くには、SDL_RenderFillRect()
関数を使う
Game.h
x
とy
の両成分を持つ単純なVector2
構造体を宣言し、
Game.cpp
Game::Initialize()
Gameクラスにパドルの位置mPaddlePos
とボールの位置mBallPos
を追加する
長方形と同様に、SDL_RenderFillRect()
関数を使ってパドルを描画する
ゲームの更新
ゲームループでは、1秒間に何度も実行され、離散的な時間のステップでゲームを更新する
実時間とゲーム時間
経過する実時間とゲームの世界で経過するゲーム時間とを区別することが重要
デルタタイムの関数としてのロジック
デルタタイムは、最後のフレームから今までに経過した時間の長さを表す
たとえば、理想的なスピードを毎秒150ピクセルとすると、
enemy.mPosition.x += 150 * deltaTime;
のように、プロセッサの速度に依存しない柔軟性の高いコードがかける
Game.cpp
Game::UpdateGame()
SDLは、SDL_Init()
関数の呼び出しから経過した時間をミリ秒単位で返すSDL_GetTicks
関数を提供している
次のフレームのために、メンバー変数mTicksCount
の時刻を更新する
想定よりも早くフレーム処理が完了すると問題が発生する場合(物理法則で動くゲームなど)において、フレーム制限を実装するためにSDL_TICKS_PASSED()
関数を利用する
デバッグ時のブレークポイントで発生する停止時間によって、巨大なデルタタイムが発生するのを防ぐために、デルタタイムの値に最大値を設定する
パドルの位置を更新する
Game.h
Game.cpp
Game::ProcessInput()
メンバ変数mPaddleDir
は、パドルが動かないなら0
、上に移動するなら-1
(負のy)、下に移動するなら1
(正のy)をセットする(こうすることで、両方のキーを押したら0
になる)
Game::UpdateGame()
デルタタイムに基づいてパドルのy座標位置を更新する(毎秒300.0fピクセル)
パドルを画面内におさめるために、パドルのy座標に境界条件の制約を加える(paddleH
はパドルの高さを示す定数)
ボールの位置を更新する
ボールの動きを示すには速度と衝突検知が必要
Game.h
Game.cpp
Game.Initialize()
Vector2変数のメンバ変数mBallVel
を追加し、初期化する
Game.UpdateGame()
ボールの位置を速度に応じて動かす(mBallPos.x
とmBallPos.y
を更新する)
ボールが壁に衝突する際は、速度成分を反転させる(反転の更新がその場で繰り返されてボールが壁にくっつく問題は、速度方向に状態の制約をかけることで解決する)
ボールがパドルに衝突するかは、x位置とy位置とx速度の条件によって判定する
ゲームプロジェクト
単純化したPong(ポン)ゲームが完成