「C++でゲームを作ろう!」と呼びかけられた(?):
ので,「それじゃあ ちょっとやってみるか…」的な日記です.
ひたすらコードを整理する
↑の記事では「空のプロジェクト」からの開始が推奨されていたが,今回は「Windowsデスクトップアプリケーション」という種類のプロジェクトを作り,吐かれたひな形コードから作業を開始する形をとった.
まずは
- 要らない部分(バージョン情報ダイアログ等々)を削除する
- ウィンドウ生成とかウィンドウプロシージャ関係のコードは何となく「ウィンドウクラス」的な class に移動させる
- メインループを
GetMessage()
じゃなくてPeekMessage()
を使う形に変更する - ゲームロジックの更新処理をてきとーな時間間隔で呼ぶ感じにする
みたいなことをした.
最終的に,エントリポイントたる wWinMain()
が書かれている箇所は以下のようになり,元のひな形コードから残った部分はほとんど無い形になったが…… それでも一通りそろっているところから整理していけばよい,というのは助かる…かな?
(使うAPIとかが分かってるなら「空」から始めた方が楽かもしれない)
#include "framework.h" //ひな形コードのまま
#include "Win32GamePT.h" //ひな形コードのまま:※ "Win32GamePT" は今回作ったプロジェクトの名前
#include <chrono> //時間計測用
#include "Toyger_GameWnd.h" //ウィンドウ関連の処理
#include "Toyger_InputImpl.h" //入力関連の処理
#include "TestGame.h" //ゲームロジック
namespace
{
//Helper. Display Error Message and returns 1.
inline int Err( LPCTSTR Msg ){ ::MessageBox( NULL, Msg, _T("ERR"), MB_OK ); return 1; }
//StartからEndまでの時間をmsで計測
class CTimeMeasure
{
public:
CTimeMeasure(){ Start(); }
void Start(){ m_ST = std::chrono::system_clock::now(); }
double End()
{
auto Curr = std::chrono::system_clock::now();
return (double)std::chrono::duration_cast< std::chrono::milliseconds >( Curr - m_ST ).count();
}
private:
std::chrono::system_clock::time_point m_ST;
};
}
//
int APIENTRY wWinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow
)
{
UNREFERENCED_PARAMETER(hPrevInstance); //よくわからん.残しておく
UNREFERENCED_PARAMETER(lpCmdLine); //よくわからん.残しておく
//入力情報を扱うやつ
Toyger::ConcreteInputState<TestGame::InputHistType> InputSt{ false };
//ウィンドウ生成.ctorで↑をリスナとして結び付けておく
Toyger::GameWnd Wnd{ InputSt };
if( !Wnd.CreateWnd( _T("Toyger") ) ){ return Err( _T("Wnd.CrateWnd() failed") ); }
//ゲーム実装の初期処理
TestGame::Game TheGame;
if( !TheGame.Initialize( Wnd ) ){ return Err( _T("TheGame.Initialize() failed") ); }
//Main loop
constexpr int MaxSleep_ms = 33;
CTimeMeasure CTM;
while( true )
{
MSG msg;
if( ::PeekMessage( &msg, NULL, 0,0, PM_REMOVE ) != 0 )
{
if( msg.message == WM_QUIT )break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
if( !TheGame.Update( InputSt, Wnd ) )break; //ゲーム更新
InputSt.ToNextStep(); //入力情報用の処理
::Sleep( (std::max)( 0, int(MaxSleep_ms - CTM.End()) ) ); //いくらかウェイトを入れる
CTM.Start();
}
}
return 0;
}
ここで Toyger
というのは,ゲームロジックではない Win32 な部分(ウィンドウ関係とか)の実装を入れている名前空間である.
このように圧倒的にかわいい名前を付けることは自己のモチベーションを維持することに役立つ.(仕事じゃないんだから好き勝手にやろう!)
以下に Toyger
名前空間の仲間たちを紹介するぜ!(ごちゃごちゃした具体実装は省略するけど)
ウィンドウ
ゲームロジック側からウィンドウ側に対してやれることは,まずはこのくらいあれば十分かな? みたいなインタフェース.
(RegisterClassEx()
だの CreateWindowEx()
だのウィンドウプロシージャだのいうやつらはこの IWnd
を継承した具体クラスに全て突っ込んだ.)
一人でやってるんだし,今後このコードを再利用する予定も今のところ無いのだから,こういうのは本当に必要な処理だけを用意する.
必要なのが増えたらその時に追加すれば良いのであり,こういうところでいきなり何でもかんでも用意し始めることは避ける.
「ウィンドウハンドル」を外側には晒さない,という所にだけこだわってみた.
- キャプション文字列くらいはゲームロジック側から設定したい
- 話を簡単にするために,ウィンドウサイズは固定(初期に
SetClientRegionSize()
を1回用いて以降はそのまま)とする想定.
そのサイズはゲームロジック側の都合で決める物となるだろう - 描画手段は必要
- エラー時とか,終了確認のためにメッセージボックスを出す手段があると良いかな
/// <summary>ゲーム実装側からのウィンドウへのアクセスI/F</summary>
class IWnd
{
public:
virtual ~IWnd() = default;
public:
/// <summary>ウィンドウキャプション文字列の変更</summary>
/// <param name="pStr">キャプション文字列</param>
/// <returns>成否</returns>
virtual bool SetCaption( LPCTSTR pStr ) = 0;
/// <summary>ウィンドウのクライアント領域サイズ変更</summary>
/// <param name="W">所望の幅</param>
/// <param name="H">所望の高さ</param>
/// <returns>
/// 成否.
/// <remarks>※指定サイズが小さすぎる場合,trueを返しても,所望のサイズにはならないかもしれない…</remarks>
/// </returns>
virtual bool SetClientRegionSize( int W, int H ) = 0;
/// <summary>クライアント領域の表示内容を引数のオブジェクトを用いて更新する</summary>
/// <param name="rPainter">描画処理者</param>
/// <returns>成否</returns>
virtual bool UpdateViewContent( IContentPainter &rPainter ) = 0;
///<summary>メッセージボックスのモーダル表示</summary>
virtual int MsgBox( LPCTSTR Msg, LPCTSTR Caption, UINT MsgBoxType ) = 0;
};
いつのまにか,C++ でもメソッドの上で ///
と入力すると XMLコメント のひな形が挿入できるようになっていたので,せっかくだからその形式でコメントを書いてみた.
(でも C# の場合と比べると入力補助はそれっきりであり貧弱だなぁ)
描画処理者
ゲームの描画処理用インタフェース.
↑の IWnd::UpdateViewContent()
に引数として渡すと「そしたらこのデバイスコンテキストに描画してみせてみろよ,できればな」といった感じで Paint()
が呼び返されるという想定.
(実際に渡される hdc
がクライアント領域直結なのか,別の何か(バックバッファとか)なのかは呼ぶ側の都合で決める)
/// <summary> ゲーム側がウィンドウクライアント領域への表示内容を描画する手段.</summary>
class IContentPainter
{
public:
virtual ~IContentPainter() = default;
/// <summary>描画処理</summary>
/// <param name="hdc">描画先</param>
/// <param name="W">hdcに関連づけられているBITMAPのサイズ</param>
/// <param name="H">hdcに関連づけられているBITMAPのサイズ</param>
virtual void Paint( HDC hdc, int W, int H ) = 0;
};
入力情報
ゲームロジックから操作入力を知るためのインタフェース.
(メインループ内のゲームの更新処理 if( !TheGame.Update( InputSt, Wnd ) )break; //ゲーム更新
のところで第1引数に渡されてるやつの基底.)
ゲームロジック側は多分「キーが今回初めて押されたぞ」とかそういうのを知りたいので,こんな物があればよいかな? ということで,「整数型を用いてbitパターンとして各キーの押下状態の履歴を記録する」という方法を考えてこんな感じのtemplateにしてしまったが,「長押し」とかが不要ならば履歴は 2bit あれば済むハズなので,unsigned char
とかでも容量の無駄になるであろうと思われる.イマイチ.
/// <summary>ゲーム実装側が入力状態を得る手段</summary>
/// <typeparam name="UINT_T">キー押下状態履歴保持用の符号なし整数型(履歴データのbit数変更用)</typeparam>
template< typename UINT_T = unsigned char >
class IInputState
{
public:
virtual ~IInputState(){}
public:
using KeyHistValType = UINT_T;
public:
/// <summary>マウスカーソル X 位置(クライアント領域座標)</summary>
virtual int MouseX() const = 0;
/// <summary>マウスカーソル Y 位置(クライアント領域座標)</summary>
virtual int MouseY() const = 0;
/// <summary>キーあるいはマウスボタンの押下状態履歴</summary>
/// <param name="VKeyCode">対象のキーまたはマウスボタンの仮想キーコード</param>
/// <returns>
/// UINT_T 型のbit数分の状態履歴.
/// bitが立っていれば「押されている/いた」ことを示す.
/// 上位bit ほど古く,下位bitほど新しい.(時間と共に左シフトされていく)
/// <remarks>例えば,履歴値の下位2bitが b'01 であれば「新しく押された」と判断できる.</remarks>
/// </returns>
virtual UINT_T KeyStateHist( unsigned char VKeyCode ) const = 0;
/// <summary>ウィンドウを閉じようとする操作が成されたか否か</summary>
/// <returns>
/// ウィンドウを閉じる操作が成された(そしてその操作自体はその時点では握りつぶされた)場合にはtrueを返す.
/// <remarks>入力状態データが更新された際に,このメソッドの戻り値はfalseにリセットされ得る.</remarks>
///</returns>
virtual bool IsWndCloseOperationOccur() const = 0;
};
//---
//ヘルパ関数
//キーあるいはマウスボタンが押されているか
template< class UINT_T >
inline bool Pressed( const IInputState<UINT_T> &IS, unsigned char VKeyCode ){ return IS.KeyStateHist(VKeyCode) & UINT_T{0x01}; }
//キーあるいはマウスボタンが押されていない状態から押されている状態になったか
template< class UINT_T >
inline bool PosEdge( const IInputState<UINT_T> &IS, unsigned char VKeyCode ){ return ( (IS.KeyStateHist(VKeyCode) & UINT_T{0x03}) == UINT_T(0x01) ); }
//キーあるいはマウスボタンが押されている状態から押されていない状態になったか
template< class UINT_T >
inline bool NegEdge( const IInputState<UINT_T> &IS, unsigned char VKeyCode ){ return ( (IS.KeyStateHist(VKeyCode) & UINT_T{0x03}) == UINT_T(0x02) ); }
入力情報自体は,ウィンドウの具体クラスに以下のリスナを結び付けて収集している.
単にウィンドウプロシージャからイベントに応じたメソッドが呼ばれるというだけの話である.
/// <summary>Toyger::GameWnd のウィンドウプロシージャから入力情報を受ける人</summary>
class IWndInputListener
{
public:
virtual ~IWndInputListener() = default;
public:
/// <summary>キーあるいはマウスボタンが押されたことの通知</summary>
/// <param name="VKeyCode">対象のキーまたはマウスボタンの仮想キーコード</param>
virtual void OnPress( unsigned char VKeyCode ) = 0;
/// <summary>キーあるいはマウスボタンが離されたことの通知</summary>
/// <param name="VKeyCode">対象のキーまたはマウスボタンの仮想キーコード</param>
virtual void OnRelease( unsigned char VKeyCode ) = 0;
/// <summary>マウスカーソルが移動したことの通知</summary>
/// <param name="x">クライアント領域上での座標</param>
/// <param name="y">クライアント領域上での座標</param>
virtual void OnMouseMove( int x, int y ) = 0;
/// <summary>ウィンドウを閉じる操作が成された際の通知</summary>
/// <returns>ウィンドウを閉じて良いかどうか.falseを返すと閉じない(操作が棄却される)</returns>
virtual bool OnWndCloseOP() = 0;
};
で,この2つのインタフェースを継承して,入力情報をゲームロジックに伝達するやつを以下のように用意した.
ゲームの更新は一定間隔でやる,という話にしているから,入力イベント発生時に即処理を行うわけにはいかないと思うので,こいつが「入力イベントの情報を一旦保持して → ゲーム更新側が欲しい形として提供する」という形.
(実際には使いもしないであろうキー群の情報を大量に収集しているのが非常に馬鹿っぽい実装だが… そこはまずはこだわらない感じで.)
/// <summary>
/// IWndInputListener と IInputState の実装.
/// ウィンドウに対する入力イベントの情報を収集し,ゲーム実装側に情報を提供する.
/// </summary>
template< typename UINT_T = unsigned char >
class ConcreteInputState : public IWndInputListener, public IInputState<UINT_T>
{
private:
UINT_T m_History[256];
int m_MouseX, m_MouseY;
bool m_WndCloseOPFlag;
const bool m_OnWndCloseOP_RetVal;
public:
/// <summary>ctor</summary>
/// <param name="OnWndCloseOP_RetVal">IWndInputListener::OnWndCloseOP()メソッドの戻り値とする値. </param>
/// <remarks>※このctorが実行されるとき,いずれのキー/マウスボタンも押下中ではないことを前提としている</remarks>
ConcreteInputState( bool OnWndCloseOP_RetVal )
: m_MouseX(0), m_MouseY(0), m_WndCloseOPFlag(false), m_OnWndCloseOP_RetVal(OnWndCloseOP_RetVal)
{ for(int i=0; i<256; ++i ){ m_History[i]=UINT_T(0); } }
///<summary>用途:ゲームループ内でのゲーム処理後に呼ぶ</summary>
void ToNextStep()
{
m_WndCloseOPFlag = false;
for(int i=0; i<256; ++i ){ m_History[i] = (m_History[i]&UINT_T(0x01)) | (m_History[i]<<1); }
}
public: // IInputState 実装
virtual int MouseX() const override { return m_MouseX; }
virtual int MouseY() const override { return m_MouseY; }
virtual bool IsWndCloseOperationOccur() const override { return m_WndCloseOPFlag; }
/// <summary>キーあるいはマウスボタンの押下状態履歴</summary>
/// <remarks>この履歴値は <see cref="ToNextStep"/> をコールする毎に上位bit側にシフト(左シフト)されていく.</remarks>
virtual UINT_T KeyStateHist( unsigned char VKeyCode ) const override { return m_History[VKeyCode]; }
public: // IWndInputListener 実装
virtual void OnPress(unsigned char VKeyCode) override { m_History[VKeyCode] |= UINT_T(0x01); }
virtual void OnRelease(unsigned char VKeyCode) override { m_History[VKeyCode] &= ~(UINT_T(0x01)); }
virtual void OnMouseMove(int x, int y) override { m_MouseX=x; m_MouseY=y; }
virtual bool OnWndCloseOP() override { m_WndCloseOPFlag=true; return m_OnWndCloseOP_RetVal; }
};
ゲームロジック用class
以上で,とりあえずウィンドウが生成されてメインループが回ってそこから毎回ゲームロジックの更新関数が呼ばれるという外側ができた.
あとはゲームロジックを実装する class として単にこんな物があれば良いと思う.
namespace TestGame
{
class GameImpl;
//ゲームの実装(メインループからメソッドが呼ばれる)
class Game
{
public:
Game();
~Game();
private:
Game( const Game & ) = delete;
Game &operator=( const Game & ) = delete;
public:
//初期処理.
//メインループ開始前に1度だけ呼ばれる想定.
//ウィンドウのキャプションやサイズを変更したりとかする.
//成否を返す.
bool Initialize( Toyger::IWnd &GameWnd );
//更新処理.
//メインループ内で呼ばれる想定.
//APPを終了させるべき場合には false を返す.
bool Update(
const InputState_t &InputState,
Toyger::IWnd &GameWnd
);
private:
std::unique_ptr<GameImpl> m_upImpl;
};
}
- 名前空間やクラスの名称,コメント等々がかなり雑になっており,すでにやる気がかなりそがれているのが見て取れる
- その Pimpl は要るのか?
とかあるが,とりあえずこれでゲームを作るための最低限の準備ができたのではなかろうか.(音関係とかは完全無視:扱わない.)
あとはこいつにゲームの内容を実装してやれば
なんて楽しいゲームなんだー
と叫べるようになる予定.
ここまでで結構時間かかってしまった.
つづき :
追記
時間計測しているところのコードが
return (double)std::chrono::duration_cast< std::chrono::milliseconds >( Curr - m_ST ).count();
となっているが,ココって
return std::chrono::duration< double, std::milli >( Curr - m_ST ).count();
とするべきなんじゃなかろうか…?
(この std::chrono
ってやつ,使い方が分かり難すぎる……とか思うのは私だけなのだろうか?)