前回の日記の続きです.
ゲームを作るとは? 実際何を作るのか? というのは大きな問題だ.
「コンソールでじゃんけんを作りました!」みたいなのをやりたいわけではないのだが,本格的な(?)何かを作れる技量は無いのである.(あと,「本格的な何か」というのは Win32 + GDI とかで作るべきものではないような気もする.)
とにかく素人なのだから,ここはミニゲーム的なやつが良かろう.
…と,いろいろ考えた結果,「砦の攻防」を題材に決めた.
30年くらい前にベーマガ掲載のコードを意味もわからず打ち込んで遊んだアレだ.
基地からミサイルを発射して相手の基地にぶつける対戦ゲーム.
そんなわけで,この題材に必要そうな以下の部品を用意した.(…というのがこの日記の内容だ)
- 乱数:ステージ生成に使う
- 2次元のベクトル:加減乗除くらいは楽に書きたい
- リングバッファ:ミサイルの軌跡をある程度の長さで描画する用
- 画像バッファ:当たり判定を「画素値」で見ることを考えているので
くらいが必要だ.
乱数
<random>
を使うが,呪文みたいな記述を何回も書きたくないので適当にラップする,というだけ.
今回欲しいのは「指定範囲の整数」だけなので,多分こんなのでOK.
#include <random>
//std::mt19937を用いた乱数生成
class StlRnd
{
public:
StlRnd() : m_MT( std::random_device().operator()() ){}
StlRnd( std::mt19937::result_type Seed ) : m_MT( Seed ){}
public:
template< class T_INT >
T_INT GetInt( T_INT Min, T_INT Max ){ return std::uniform_int_distribution<T_INT>( Min,Max )( m_MT ); }
private:
std::mt19937 m_MT;
};
2次元ベクトル
よくある operator オーバーロードをひたすら書くやつだ.とにかくひたすら書くだけの話.
//2D Vector
template< class VAL_T >
class Vec2D
{
public:
VAL_T v[2]; //data is public.
public: //こういうのを必要なだけ書く.無心で.
//Unary operators
Vec2D operator -() const { return Vec2D( -v[0], -v[1] ); }
Vec2D &operator +=( const Vec2D &rhs ){ v[0]+=rhs.v[0]; v[1]+=rhs.v[1]; return *this; }
Vec2D &operator -=( const Vec2D &rhs ){ v[0]-=rhs.v[0]; v[1]-=rhs.v[1]; return *this; }
Vec2D &operator *=( VAL_T s ){ v[0]*=s; v[1]*=s; return *this; }
Vec2D &operator /=( VAL_T s ){ v[0]/=s; v[1]/=s; return *this; }
//(※以下略)
};
//alias
using Vec2i = Vec2D<int>;
using Vec2f = Vec2D<float>;
using Vec2d = Vec2D<double>;
リングバッファ
std::array
のindexをぐるぐる使うだけのやつを用意した.
ガチの汎用みたいなのを実装するわけじゃなくて(←自分の力量的にやれないだろうし),あくまでも自分が今回必要とするやつを書くだけなので,こんな.
- iterator すら無いやさしい世界.
- 各メソッドも簡単に地獄が見れそうな実装になっている.
template< class T, size_t N >
class RingBuff
{
private:
std::array<T,N> m_Buff;
size_t m_iOldest=0;
size_t m_Size = 0;
public:
void clear(){ m_iOldest=0; m_Size=0; }
void Push( const T &t )
{
if( m_Size < N )
{ m_Buff[m_Size] = t; ++m_Size; return; }
m_Buff[m_iOldest] = t;
m_iOldest = (m_iOldest + 1) % N;
}
T &operator[]( size_t i ){ return m_Buff[ (m_iOldest+i)%N ]; }
const T &operator[]( size_t i ) const { return m_Buff[ (m_iOldest+i)%N ]; }
size_t size() const { return m_Size; }
T &Last(){ return this->operator[]( size()-1 ); }
const T &Last() const { return this->operator[]( size()-1 ); }
};
画像バッファ
お勉強が必要な箇所であった.
「DDB と DIB が DIBSection でどうのこうの…」いう深遠な世界が広がっており,大変に苦労した.ウボァー.
今でもイマイチ把握できていないが,使う側の視点からすると以下のような感じかと思われ:
-
DDB :
GDIの描画関数が使えるけど,ダイレクトに画素データには触れない感じ. -
DIB :
画素データがダイレクトだが,今度は GDI の描画関数を使えないので, 線とか丸とか描きたいなら自前で実装すれば?みたいな世界.
という感じで一長一短な感じなんだけど,DIBセクションなる物は
-
DIBセクション :
「俺ならどっちもいけるし!」という感じで,画素データを直接触れるしGDIで線とか丸とか描ける.すごい.便利.
みたいな感じであった.
DIBだけどDDBのように振る舞える(?)とか何とか,ちょっと何言ってるかわかんない感じだけども,何だか最強に思える.
CreateDIBSection()
で作ると画素データ用のメモリまで勝手に作ってくれて DeleteObject()
時に勝手に捨ててくれるとかいう至れり尽くせり感もあり,「とりあえずこれでいいじゃん」とか思うのでこれを使うことにした.
ハンドルとかを保持しなきゃならないので class として実装.(DIB なのか DIBセクション なのか… みたいな試行錯誤の跡が見られる実装になっているぞ)
class DIB
{
public:
DIB(){}
virtual ~DIB(){}
private:
DIB( const DIB & ) = delete;
DIB &operator=( const DIB & ) = delete;
public:
bool empty() const{ return !BitmapBytes(); }
int Width() const { return ( empty() ? 0 : BMI().bmiHeader.biWidth ); }
int Height() const { return ( empty() ? 0 : BMI().bmiHeader.biHeight ); }
public:
//部分描画.
//拡縮可.DstW, DstH が負の値の場合にはその方向に反転.
bool Blt(
HDC hdc, int DstLeft, int DstTop, int DstW, int DstH,
int SrcLeft, int SrcTop, int SrcW, int SrcH,
DWORD OP=SRCCOPY
) const
{
if( empty() )return false;
if( DstW<0 ){ DstLeft = DstLeft-DstW-1; }
if( DstH<0 ){ DstTop = DstTop-DstH-1; }
return ::StretchDIBits(
hdc, DstLeft, DstTop, DstW, DstH,
SrcLeft, Height()-(SrcTop+SrcH), SrcW, SrcH,
BitmapBytes(), &BMI(), DIB_RGB_COLORS, OP
);
}
//画像全体を描画.
//拡縮可.DstW, DstH が負の値の場合にはその方向に反転.
bool Blt( HDC hdc, int DstLeft, int DstTop, int DstW, int DstH, DWORD OP=SRCCOPY ) const
{ Blt( hdc,DstLeft,DstTop,DstW,DstH, 0,0,Width(),Height(), OP ); }
//画像全体を等倍 & 無反転 描画
bool Blt( HDC hdc, int DstLeft, int DstTop, DWORD OP=SRCCOPY ) const
{ return Blt( hdc,DstLeft,DstTop,Width(),Height(), 0,0,Width(),Height(), OP ); }
protected:
//(派生側の実装用ヘルパとして提供) パディングを考慮した1行のbyte数
static size_t LineBytes( size_t Width, int BitCount ){ return ( ((Width*BitCount/8)+3)/4 )*4; }
protected: //virtual
//画素データ先頭.
//データが無い時には nullptr を返す.
virtual const BYTE* const BitmapBytes() const = 0;
//描画に用いるための BITMAPINFO.
//データが無い場合は内容不定で良い.
virtual const BITMAPINFO &BMI() const = 0;
};
//24bit DIBセクション
class DIBSection24 : public DIB
{
public:
DIBSection24() : m_hBMP(NULL), m_pBits(nullptr), m_bmi{0} {}
virtual ~DIBSection24(){ Release(); }
public:
//DIBSectionの作成
bool Create( int W, int H )
{
if( W<=0 || H<=0 )return false;
Release(); //既に生成されていたら捨てる
m_bmi.bmiHeader.biSize = sizeof( BITMAPINFOHEADER );
m_bmi.bmiHeader.biBitCount = 24;
m_bmi.bmiHeader.biPlanes = 1;
m_bmi.bmiHeader.biWidth = W;
m_bmi.bmiHeader.biHeight = H;
m_bmi.bmiHeader.biCompression = BI_RGB;
m_hBMP = CreateDIBSection( NULL, &m_bmi, DIB_RGB_COLORS, (VOID**)(&m_pBits), NULL, 0 );
return ( m_hBMP != NULL );
}
//DIBSectionの破棄
void Release()
{
if( m_hBMP )
{
::DeleteObject( m_hBMP );
m_hBMP = NULL;
m_pBits = NULL;
}
}
//HBITMAPを得る
inline HBITMAP GetHBITMAP(){ return m_hBMP; }
//指定座標の画素値を得る
COLORREF At(int x, int y) const
{
const BYTE *P = m_pBits + ( LineBytes( Width(), 24 ) * y ) + x*3;
return RGB( P[2],P[1],P[0] );
}
protected: //virtual
virtual const BYTE* const BitmapBytes() const override { return m_pBits; }
virtual const BITMAPINFO &BMI() const override { return m_bmi; }
private:
BITMAPINFO m_bmi;
HBITMAP m_hBMP;
BYTE *m_pBits;
};
※1bit 版も作ったけど今回は没
※本当は 1bit 版のDIBセクションも作る予定だったのだけど,途中で振る舞いが「?」「???」となったので「だったらもう 24bit で富豪的にやるし!」と方針を切り替えて没にした.
(最終的には「パレットデータが2つ分必要」ということだと分かったのだが,大昔に触ったとき 1bitだと SetTextColor
とかの設定が描画に効いていた気がするんだよなぁ…… イミワカンネー)
……で,実際に使おうとすると今度は「メモリデバイスコンテキストが互換性どうこう…」いう話が出てきて,私は再度「ウボァー」と叫ぶのであった.
DIBセクションなる物にGDI関数で描画したい場合にはDDBと同様に「デバイスコンテキストに選択」せねばならぬ.
画像バッファをいじくる場合には CreateCompatibleDC()
で「メモリデバイスコンテキスト」なる物を作って使えばいいっぽいのだが,この場合,この関数の引数に渡す HDC は何なのか?
NULLを指定すると「アプリケーションの現在の画面と互換性のあるメモリDC」が作成されるとのことだが,これでいいのかダメなのか?
そもそもDIBは「デバイス非依存」なハズだが「何かと互換性のあるDC」とかいうのを使うべきなのか何なのか,これがさっぱりわからない.
とりあえず NULL 指定で動いてるみたいだけど,いいのかなぁ…?
あと,描画する際に StretchDIBits()
を使うのが良いのか,それとも「メモリデバイスコンテキストに選択して BitBlt()
とか使う」みたいな形とすべきなのか? というのもよくわかんない.(とりあえず前者で実装したけれども)
つづき :