概要
こんなの↓作った.…っていう日記.
ぼくのホームページからダウンロードできます.
これまでのあらすじ
Win32 +GDI でミニゲームを作ろう的な.
今回の日記
……というわけで「砦の攻防」のような物をつくる.
上記の1.で用意したゲーム実装用の箇所に,上記2.で用意した部品を用いる形で実装.
ヘッダ側 :
とりあえず必要そうな物を何でもかんでも GameImpl
クラスに詰め込んだ.
これぞ初心者!と言わんばかりの雰囲気である.
70行くらいある
class GameImpl : public Toyger::IContentPainter
{
public:
bool Initialize( Toyger::IWnd &GameWnd );
bool Update( const InputState_t &InputState, Toyger::IWnd &GameWnd );
public: // Toyger::IContentPainter 実装
virtual void Paint(HDC hdc, int W, int H) override;
private:
void Setup();
int iOffenceSide() const { return m_CurrOffenceSide; }
int iDeffenceSide() const { return (m_CurrOffenceSide==0 ? 1 : 0); }
void MakeHole( int x, int y, int r );
void SideChange();
bool Update_WaitFire( const InputState_t &InputState, Toyger::IWnd &GameWnd );
bool Update_Missile( const InputState_t &InputState, Toyger::IWnd &GameWnd );
bool Update_Exprode( const InputState_t &InputState, Toyger::IWnd &GameWnd );
private:
//処理ステート
enum class State
{
WaitFire, //ミサイル発射待ち
Missile, //ミサイル飛翔中
Exprode, //爆発中
GameOver //決着した
};
private:
State m_CurrState; //現在のステート
StlRnd m_RND; //乱数用
DIBSection24 m_FieldImg; //ゲームフィールド画像:この画像の画素値が当たり判定に用いられる.
Vec2i m_BasePos[2]; //基地の座標:m_FieldImg のこの座標の画素値が変化したとき,敗北と判定される.
COLORREF m_BkgndColor; //背景色
int m_Wind = 0; //風
RingBuff< Vec2d, 32 > m_MissilePos; //ミサイル軌跡データ:※ミサイル発射待ち状態では[0]をミサイル発射位置座標として使用
Vec2d m_MissileVelocity; //ミサイル速度
int m_MissileExpR; //ミサイルの爆発半径
Exprode m_Exprode; //爆発アニメーション用
int m_CurrOffenceSide; //現在の攻撃側がどちらなのか(0 ro 1)
int m_Winner = 0; //勝者がどちら側なのか(0 or 1):GameOverステートでの表示用
private: //定数群
static constexpr int GC_W = 640; //Size of Game Content
static constexpr int GC_H = 540; //Size of Game Content
static const COLORREF ms_BkgndColors[3]; //背景色候補
static const COLORREF ms_GndColor; //地面の色
static const COLORREF ms_BaseColor[2]; //基地の色
static const double ms_MaxMissileV0; //ミサイル初速の最大値
static const double ms_Gravity; //重力
static const int ms_BaseRx; //基地のサイズ
static const int ms_BaseRy; //基地のサイズ
};
.cpp側 :
- ステートは
enum
+switch
で扱う.単純明快. - ミサイル発射方向と初速は,簡便にマウスクリック位置で決めるようにした.
(大昔に遊んだやつだとタイミングよくキーを押す感じで,そっちの方が面白い気もするのだが,実装が面倒なので妥協)
300行くらいある
//-------------------------------------------
//定数群
//色系.当たり判定に用いられる
const COLORREF GameImpl::ms_BkgndColors[3] = { RGB(0,0,16), RGB(8,12,96), RGB(72,32,0) }; //背景色
const COLORREF GameImpl::ms_GndColor = RGB( 0, 160, 32 ); //地面の色
const COLORREF GameImpl::ms_BaseColor[2] = { RGB( 8, 64, 255 ), RGB( 255, 64, 8 ) }; //基地の色
//物性値系
const double GameImpl::ms_MaxMissileV0 = 20; //ミサイル初速の最大値
const double GameImpl::ms_Gravity = 0.5; //重力
//基地のサイズ(矩形描画サイズ.ms_BaseRy はミサイル発射位置座標の決定に使用)
const int GameImpl::ms_BaseRx = 4;
const int GameImpl::ms_BaseRy = 6;
//-------------------------------------------
//初期処理
bool GameImpl::Initialize( Toyger::IWnd &GameWnd )
{
if( !GameWnd.SetCaption( _T( "砦の攻防もどき Ver. 1.0" ) ) )return false;
if( !GameWnd.SetClientRegionSize( GC_W, GC_H ) )return false;
if( !m_FieldImg.Create( GC_W, GC_H ) )return false;
Setup();
return GameWnd.UpdateViewContent( *this );
}
//更新
bool GameImpl::Update( const InputState_t &InputState, Toyger::IWnd &GameWnd )
{
//Rキーでステージ再生成
if( Toyger::PosEdge( InputState, 'R') )
{
Setup();
return GameWnd.UpdateViewContent( *this );
}
switch( m_CurrState )
{
case State::WaitFire: return Update_WaitFire( InputState, GameWnd ); break;
case State::Missile: return Update_Missile( InputState, GameWnd ); break;
case State::Exprode: return Update_Exprode( InputState, GameWnd ); break;
default: break;
}
return true;
}
//更新処理:ミサイル発射待ち状態
bool GameImpl::Update_WaitFire( const InputState_t &InputState, Toyger::IWnd &GameWnd )
{
m_MissileVelocity = 0.1 * ( Vec2d( InputState.MouseX(), InputState.MouseY() ) - m_MissilePos[0] );
{
double v = m_MissileVelocity.L2Norm();
if( v > ms_MaxMissileV0 ){ m_MissileVelocity *= ( ms_MaxMissileV0 / v ); }
}
if( Toyger::PosEdge( InputState, VK_LBUTTON ) )
{ m_CurrState = State::Missile; }
return GameWnd.UpdateViewContent( *this );
}
//更新処理:ミサイル飛翔中状態
bool GameImpl::Update_Missile( const InputState_t &InputState, Toyger::IWnd &GameWnd )
{
auto PrevPos = m_MissilePos.Last();
//当たり判定:てきとーに CheckLength を超えない距離単位で実施
constexpr double CheckLength = 2.0;
int nCheckStep = (int)std::ceil( m_MissileVelocity.L2Norm() / CheckLength );
for( int i=1; i<=nCheckStep; ++i )
{
auto CheckPos = PrevPos + (double(i) * m_MissileVelocity) / (double)nCheckStep;
int x = int( std::round( CheckPos[0] ) );
int y = int( std::round( CheckPos[1] ) );
if( x<0 || GC_W<=x || GC_H<=y )
{//場外に出た
SideChange();
return GameWnd.UpdateViewContent( *this );
}
else if( 0<=y )
{
auto PixVal = m_FieldImg.At(x, GC_H-1-y);
if( PixVal != m_BkgndColor && PixVal != ms_BaseColor[iOffenceSide()] )
{//何かにHIT
m_MissilePos.Push( CheckPos );
m_Exprode.Setup( x,y, m_MissileExpR );
m_CurrState = State::Exprode;
return GameWnd.UpdateViewContent( *this );
}
}
}
//
m_MissilePos.Push( PrevPos + m_MissileVelocity );
m_MissileVelocity[1] += ms_Gravity; //重力
m_MissileVelocity[0] += m_Wind * 0.1; //風
return GameWnd.UpdateViewContent( *this );
}
//更新処理:爆発中状態
bool GameImpl::Update_Exprode( const InputState_t &InputState, Toyger::IWnd &GameWnd )
{
if( m_Exprode.Update() )
{
auto C = m_MissilePos.Last();
int x = (int)std::round( C[0] );
int y = (int)std::round( C[1] );
MakeHole( x,y, m_MissileExpR );
for( int iSide=0; iSide<2; ++iSide )
{
if( m_FieldImg.At( m_BasePos[iSide][0], GC_H-1-m_BasePos[iSide][1] ) != ms_BaseColor[iSide] )
{
m_Winner = ( iSide==0 ? 1 : 0 );
m_CurrState = State::GameOver;
return GameWnd.UpdateViewContent( *this );
}
}
++m_MissileExpR;
SideChange();
}
return GameWnd.UpdateViewContent( *this );
}
//フィールドに穴を開ける
void GameImpl::MakeHole( int x, int y, int r )
{
auto hmemdc = ::CreateCompatibleDC(NULL);
auto OldBitmap = SelectBitmap( hmemdc, m_FieldImg.GetHBITMAP() );
auto OldPen = SelectPen( hmemdc, GetStockPen(NULL_PEN) );
auto OldBrush = SelectBrush( hmemdc, GetStockBrush(DC_BRUSH) );
::SetDCBrushColor( hmemdc, m_BkgndColor );
::Ellipse( hmemdc, x-r, y-r, x+r, y+r );
SelectBrush( hmemdc, OldBrush );
SelectPen( hmemdc, OldPen);
SelectBitmap( hmemdc, OldBitmap );
DeleteDC( hmemdc );
}
//攻め手の切り替え
void GameImpl::SideChange()
{
m_CurrOffenceSide = iDeffenceSide();
m_CurrState = State::WaitFire;
m_MissilePos.clear();
m_MissilePos.Push( Vec2d( m_BasePos[m_CurrOffenceSide][0], m_BasePos[m_CurrOffenceSide][1] - ms_BaseRy ) );
m_MissileVelocity.Assign( 0,0 );
m_Wind = m_RND.GetInt( -5,5 );
}
//ステージセットアップ
void GameImpl::Setup()
{
//地形生成
int nGndPoints = m_RND.GetInt( 6, 24 );
std::vector< Vec2i > GndPoints( nGndPoints );
for( int i=0; i<nGndPoints; ++i )
{
GndPoints[i].Assign(
i*GC_W/ (nGndPoints-1),
GC_H - m_RND.GetInt( 5, GC_H*3/5 )
);
}
{//基地の位置を決める
m_BasePos[0][0] = m_RND.GetInt( GC_W/10, GC_W/3 );
m_BasePos[1][0] = GC_W - m_RND.GetInt( GC_W/10, GC_W/3 );
for( int iBase=0; iBase<2; ++iBase )
{
auto &BP = m_BasePos[iBase];
BP[1] = GC_H; //※以下の処理で定まらない場合は無いと思うけど一応何か入れとく
for( int i=0; i+1<nGndPoints; ++i )
{
const auto &L = GndPoints[i];
const auto &R = GndPoints[i+1];
if( L[0]<=BP[0] && BP[0]<=R[0] )
{
BP[1] = ( (BP[0]-L[0])*R[1] + (R[0]-BP[0])*L[1] ) / ( R[0]-L[0] ) - 3;
break;
}
}
}
}
//今回の背景色を決定
m_BkgndColor = ms_BkgndColors[ m_RND.GetInt(0,2) ];
//m_FieldImg にフィールドの様子を描画する
auto hmemdc = ::CreateCompatibleDC(NULL);
auto OldBitmap = SelectBitmap( hmemdc, m_FieldImg.GetHBITMAP() );
{
{//背景色で塗りつぶし
RECT Whole{ 0,0,GC_W,GC_H };
::SetDCBrushColor( hmemdc, m_BkgndColor );
::FillRect( hmemdc, &Whole, GetStockBrush(DC_BRUSH) );
}
{//地形の描画
auto OldPen = SelectPen( hmemdc, GetStockPen(DC_PEN) );
::SetDCPenColor( hmemdc, ms_GndColor );
for( int iLeft=0; iLeft<nGndPoints-1; ++iLeft )
{
const Vec2i &LP = GndPoints[iLeft];
const Vec2i &RP = GndPoints[ size_t(iLeft)+1 ];
for( int x=LP[0]; x<RP[0]; ++x )
{
int y = ( (RP[0]-x)*LP[1] + (x-LP[0])*RP[1] ) / (RP[0]-LP[0]);
::MoveToEx( hmemdc, x, y, NULL );
::LineTo( hmemdc, x, GC_H );
}
}
SelectPen( hmemdc, OldPen);
}
//基地を描画
for( int iSide=0; iSide<2; ++iSide )
{
RECT Whole{ m_BasePos[iSide][0]-ms_BaseRx, m_BasePos[iSide][1]-ms_BaseRy ,m_BasePos[iSide][0]+ms_BaseRx, m_BasePos[iSide][1]+ms_BaseRy };
::SetDCBrushColor( hmemdc, ms_BaseColor[iSide] );
::FillRect( hmemdc, &Whole, GetStockBrush(DC_BRUSH) );
}
}
SelectBitmap( hmemdc, OldBitmap );
DeleteDC( hmemdc );
//データ初期化
m_MissileExpR = 10;
m_CurrOffenceSide = 1;
SideChange();
}
//描画処理
void GameImpl::Paint(HDC hdc, int W, int H)
{
m_FieldImg.Blt( hdc, 0,0 );
{//風向き描画
::SetTextColor( hdc, RGB(255,255,255) );
::SetBkMode( hdc, TRANSPARENT );
std::wstring Text = ( m_Wind==0 ? _T("無風") : std::wstring( abs(m_Wind), m_Wind<0 ? L'<' : L'>' ) );
RECT TextLineRect = {0,0,GC_W,32};
DrawText( hdc, Text.c_str(), -1, &TextLineRect, DT_NOCLIP|DT_SINGLELINE|DT_VCENTER|DT_CENTER );
}
switch( m_CurrState )
{
case State::WaitFire:
{//ミサイル発射方向と強さのガイドを描画
auto OldPen = SelectPen( hdc, GetStockPen(DC_PEN) );
::SetDCPenColor( hdc, RGB(255,255,0) );
::MoveToEx( hdc, (int)std::round(m_MissilePos[0][0]), (int)std::round(m_MissilePos[0][1]), NULL );
Vec2d P = m_MissilePos[0] + m_MissileVelocity;
::LineTo( hdc, (int)std::round(P[0]), (int)std::round(P[1]) );
SelectPen( hdc, OldPen );
}
break;
case State::Missile:
{//ミサイル軌跡描画
auto OldPen = SelectPen( hdc, GetStockPen(DC_PEN) );
::SetDCPenColor( hdc, RGB(255,255,255) );
::MoveToEx( hdc, (int)std::round(m_MissilePos[0][0]), (int)std::round(m_MissilePos[0][1]), NULL );
for( size_t i=0; i<m_MissilePos.size(); ++i )
{ ::LineTo( hdc, (int)std::round(m_MissilePos[i][0]), (int)std::round(m_MissilePos[i][1]) ); }
SelectPen( hdc, OldPen );
}
break;
case State::Exprode: //爆発アニメーション描画
m_Exprode.Draw( hdc, m_BkgndColor );
break;
case State::GameOver:
{//どっちが勝ったかを描画
::SetTextColor( hdc, RGB(255,255,255) );
::SetBkMode( hdc, TRANSPARENT );
RECT TextLineRect = {0,0,GC_W,GC_H};
auto Text = std::to_wstring( m_Winner+1 ) + std::wstring( _T("P Win!") );
DrawText( hdc, Text.c_str(), -1, &TextLineRect, DT_NOCLIP|DT_SINGLELINE|DT_VCENTER|DT_CENTER );
}
break;
default:
break;
}
}
Exprode クラス :
爆発アニメーション用.↓の絵みたいな感じで円で簡素に描画するだけ.
(ところでコレ,綴りが間違っていることに今気付いた.これは恥ずかしい.)
60行くらいある
//爆発アニメーション用
class Exprode
{
private:
int m_Cx;
int m_Cy;
int m_MaxR;
int m_R1 = 0;
int m_R2 = 0;
public:
//中心と最大半径を指定してセットアップ
void Setup( int Cx, int Cy, int MaxR )
{
m_Cx = Cx;
m_Cy = Cy;
m_MaxR = MaxR;
m_R1 = m_R2 = 0;
}
//更新.戻り値はアニメーション完了したか否か.(true:完了)
bool Update();
//描画
void Draw( HDC hdc, COLORREF BkGndColor ) const;
};
bool Exprode::Update()
{
constexpr int dR = 2;
if( m_R1 < m_MaxR )
{
m_R1 = (std::min)( m_R1+dR, m_MaxR );
++m_R1;
return false;
}
else
{
m_R2 = (std::min)( m_R2+dR, m_MaxR );
return (m_R2 >= m_MaxR);
}
}
void Exprode::Draw( HDC hdc, COLORREF BkGndColor ) const
{
auto OldPen = SelectPen( hdc, GetStockPen(DC_PEN) );
auto OldBrush = SelectBrush( hdc, GetStockBrush(DC_BRUSH) );
::SetDCPenColor( hdc, RGB(255,255,0) );
::SetDCBrushColor( hdc, RGB(255,0,0) );
::Ellipse( hdc, m_Cx-m_R1, m_Cy-m_R1, m_Cx+m_R1, m_Cy+m_R1 );
if( m_R2 > 0 )
{
::SetDCPenColor( hdc, BkGndColor );
::SetDCBrushColor( hdc, BkGndColor );
::Ellipse( hdc, m_Cx-m_R2, m_Cy-m_R2, m_Cx+m_R2, m_Cy+m_R2 );
}
SelectBrush( hdc, OldBrush );
SelectPen( hdc, OldPen);
}
Conclusion
-
なんとなく「砦の攻防」っぽいのが作れた.よかった.
- ミニゲームを作るのは思ったよりも楽しい.
大昔に N88-BASIC で純粋に「動かしたい」を楽しんでいたような頃を思い出した.
コードの良し悪しだとかはそういう話は置いといて,「とりあえず動くぞヒャッハー!」みたいな勢いを最優先するのがこういう場合には大切なのではないかと.
(書き直したければ事後に好きなだけやればいい.最初から「満足いくコードを~」とか何とか言うと多分途中で精神力が尽きて終わることになりそう.)
- ミニゲームを作るのは思ったよりも楽しい.
-
残された課題 :
現状,一人二役で対戦するの楽しいです^^ という状態なので,ちょっとしたCPU操作が欲しいところか. -
所要時間 :
2日くらい? 「DIBがどうの~」のところに数時間ハマったのが痛い.
追記
さすがに延々と「自分vs自分」に興じている様子は人類の健全な姿とは思えないので,上記で「課題」として挙げていたCPU操作を設けた.
まずは,完全にランダムにミサイル発射方向と初速の大きさを決定するだけの簡素すぎる実装を設けたが,これは予想通り「目の前の壁にミサイルを撃ち込んで即自滅」とか「ミサイルが風でブーメランのように返ってきて自滅」みたいな事態が多発.
こちらの行動と無関係に高頻度で自滅されるのでは「対戦」として成り立たないから困る.
あまりにも乱数すぎるのが問題なのだろうか?
しかし例えば「発射方向(の乱数の幅)を適当に相手がいる方向に絞ってやる」みたいな安直なことを考えると,以下の理由でイマイチだと思う.
- (これはステージ生成方法のせいだが)目の前が壁という率が高いため,前に撃つと自滅し得る.
- 相手がいる方向に撃っても風によるブーメラン自滅が起きる.
風向き的にミサイルが相手に届く見込みがないような場合には後方に撃ち捨てるのが良手だったりする.
ではどうすればいいのか?
……
今回の答え:「完全ランダムのままでいいけど,それをN回試行して最善を選ぶことにする」
つまり「とりあえずランダムにミサイルを発射したら結果がどうなるのか?」を複数回やってみて(:シミュレートして)その中で最も評価結果の良いやつに決める.
評価については,言うまでもなく結果が自滅になる場合の評価結果は最悪とし,あとは「目標と着弾地点の距離が短いほど良い」ということにした(飛びすぎとかで場外に出た場合には場外に出た時点での位置を着弾地点として評価).
これなら
- 基本ランダムなので「CPU操作があまりも正確すぎてツマンネー」ともならないし
- 複数回の試行のうち1つでも自滅しないものがあればそれが選ばれるから,少なくとも自滅はそうそう起きないハズである.
- あと,とにかく実装がめっちゃ簡単!!
「複数回」の具体的な回数については様子を見て「強すぎず弱すぎず」な雰囲気になる回数とした.(15回くらいが良さそう)
以上の簡単な修正で,行動がそれとなく「知的」に見えるようになった.行動に「狙い」が存在しているように見えるというか.
ちゃんと風を利用した攻撃を仕掛けてくるし,下図のようにフィールド地形的に上空から狙うのが困難な場合にはせっせとトンネルを掘り始めたりするところが「人間くさい」というか,そんな印象が感じられる…気がする.
(CPU操作の赤砦が俺の青砦に向けてせっせとトンネルを掘り進めてくる様.かわいい.)
拡大がきれいにならなかったという愚痴
全体的に表示が小さいので StretchBlt()
を用いて拡大する機能を付けることを検討したのだが,何か変な挙動になる.
元々,描画は bitmap に対して行った後でそれをクライアント領域に BitBlt()
で表示しているのだが,ここを StretchBlt()
に変えると,地形(緑)と背景の境界が激しく点滅するのだ.
すなわち,その境界のところの画素が 地形側の色になるか背景側の色になるか というのが StretchBlt()
の実施毎に異なっている(?)という雰囲気.
コレ,全く同じ処理をしてるのに結果が微妙に変わるのか? どういう仕組み?
拡大具合(倍率)次第で起きたり起きなかったりするみたい.何だろう?(1.5倍だと起きるけど 1.6倍だと起きないとか,何かそんな)
あと,この点滅現象とは関係ないけど, SetStretchBltMode()
で「ちょっと綺麗に見えるようにならんかな?」というのを試してみたけど,なんつーか,結果が「きたない(気に入らない)」んだよなぁ.
「バイリニアですらない謎の汚らしい効果」みたいな…
しかも画像全体に一律に効果が出てるわけでもない(:妙に汚らしい効果が出る場所と,全く何の効果も出ない場所とがある)ように見えるし,何なんだコレ?
これならデフォルト(ニアレストネイバー)の方がまだマシだな.
うーん,元々は 単純に 2倍 拡大するだけ のつもりだったんだけど,2倍だと自環境で画面に入りきらないっていう…盲点すぎるw
(その結果として上記のように色々と残念な話に)
次回は自分の環境で2倍にできるようにサイズを決めよう!と固く心に誓うのであった.