C++ DXライブラリ ゲーム開発 プログラミング講座

シューティングゲームを作る part4.並行動作とオブジェクト生成

 

 

まとめ

  • オブジェクトを同時並行で動かす仕組みについて解説した
  • 自機のショットの発射を行った

はじめに

前回はこちら.
ゲームを作る上でまず重要な,
ゲームループの解説と,
キーボード入力,自機の移動を行った.

まだの人はこちらから読んでくれたほうが嬉しい.

オブジェクトを並行して動かす

ゲームはオブジェクトが同時並行で動作する.
それをどのように実現するか,
実は方法はいくつか*1あって,地味に奥深い分野でもあるのだが,
今回は一番シンプルな方法を紹介する.

単に,関数化して,
続けざまに関数を呼んでやる,という方法だ.

処理を関数化する

まずは,前回の自機の移動部分を関数化する.
これを

if( CheckHitKey( KEY_INPUT_RIGHT ) == 1 )
{
playerPosX += 5;
}
if( CheckHitKey( KEY_INPUT_LEFT ) == 1 )
{
playerPosX -= 5;
}
if( CheckHitKey( KEY_INPUT_UP ) == 1 )
{
playerPosY -= 5;
}
if( CheckHitKey( KEY_INPUT_DOWN ) == 1 )
{
playerPosY += 5;
}
// 出力
DrawPlayer( playerPosX, playerPosY );

こんな具合に.

void UpdatePlayer()
{
if( CheckHitKey( KEY_INPUT_RIGHT ) == 1 )
{
playerPosX += 5;
}
if( CheckHitKey( KEY_INPUT_LEFT ) == 1 )
{
playerPosX -= 5;
}
if( CheckHitKey( KEY_INPUT_UP ) == 1 )
{
playerPosY -= 5;
}
if( CheckHitKey( KEY_INPUT_DOWN ) == 1 )
{
playerPosY += 5;
}
DrawPlayer( playerPosX, playerPosY );
}

ゲームループの中でこの関数を呼んでやるのを忘れずに.

 // ゲームループ
while( ProcessMessage() == 0 )
{
ClearDrawScreen(); // 画面を消す
UpdatePlayer();
ScreenFlip(); //裏画面を表画面に反映
}

このゲームループの中で,
他のオブジェクトの更新処理を行えば良い.

例えば,今回追加する自機のショットの更新処理を行うのであればこんな感じだ.

 // ゲームループ
while( ProcessMessage() == 0 )
{
ClearDrawScreen(); // 画面を消す
UpdatePlayer();
UpdateShot();
ScreenFlip(); //裏画面を表画面に反映
}

簡単である.

自機のショットを発射する

それでは,実際に UpdateShot() の中身を記述していくのだが,
まずは使用するC言語の機能の説明から.

配列

自機は,画面内に 1 つ存在すれば良かった.
なので,

float playerPosX = 320.0f;
float playerPosY = 340.0f;

と,単一の変数を用意すれば十分だった.

しかし,ショットとどうだろうか?
画面内にいくつくらい存在すればよいだろうか?

ざっと見積もって 100 個くらいだとする.
それでは
100 個分の座標を表現する変数を用意したい.

float shotPosX_0;
float shotPosY_0;
float shotPosX_1;
float shotPosY_1;
float shotPosX_2;
float shotPosY_2; ...

と,変数を 200 個用意する必要があるだろうか?
あまりにもバカバカしい.

こんな悩みを解決してくれる機能がある.

そう,配列である.

配列を使うと,以下のように記述することで,
変数を実質 200 個用意することができる.

float shotPosX[ 100 ];
float shotPosY[ 100 ];

配列内の変数は,添字というもので使うことができる.
つまり,

shotPosX[0];

とすることで,実質

shotPosX_0;

という変数を扱うことになるのだ.

ショットを発射する

今回は,Zキーを押したらショットを発射する,
という仕様にしたい.

キー入力や関数化については前回やったので説明は省略する.

つまり,以下のようにしたい.

if( CheckHitKey( KEY_INPUT_Z ) == 1 )
{
StartShot();
}

また,自機の座標から ショットを撃ちたい.
加えて, ショットを一意に特定する id を割り振りたい.
(配列の要素にアクセスする添字に使うため)

どのようなショット生成関数にするのが良いだろうか.

正解はこれに限らないが,
以下のようにするのがシンプルだ.

void StartShot( float x, float y, int id )
{
shotPosX[id] = x;
shotPosY[id] = y;
}

引数で,座標と id を渡してやる,という形だ.

呼び出し箇所は以下のようになる.

StartShot( playerPosX, playerPosY, shotId );
shotId++;

ショットを更新する

上記でショットの生成ができた.

それでは, ショットを更新する.
今回はショットの速度を 40 とし,
全部のショットの座標を 更新すれば良い.

全ての弾のY座標は
shotPosY という配列が持っているのだから,
shotPosY の値を全て 40 足し続けてやれば良い.

配列の全ての要素にアクセスするには,
for 文を使うのが相性が良い.

つまり,以下のようになる.

void UpdateShot()
{
float speed = 40;
for( int i = 0; i < 100; i++ )
{
shotPosY[ i ] -= speed;
DrawShot( shotPosX[ i ], shotPosY[ i ] );
}
}

これで,ショットのプログラムができた.

実行してみて動くことを確認してほしい.

101発目のショット

しばらく弾を撃ち続けてみてほしい.

以下のようなエラーで,プログラムが停止してしまうはずだ.

f:id:gothlab:20210806222353j:plain

これは,
100 発分の弾の領域(配列)しか用意していないのに,
101発目の弾を撃とうとしたために発生するエラーだ.

つまり,現状自機は 100発の弾数制限がある状態となっている.

どのようにすれば回避できるだろうか.

100 発撃ったら 1発目をもう一回撃つという考え方

実は,この不具合は,
100 発ショットを撃ったら,
また 1 発目から撃つ

という考え方をすることで,解決することができる.

つまり,以下のような感じだ.

// 100 発目を超えたら 1 発目に戻す
if( shotId >= 100 )
{
shotId = 0;
}

定数

ショットの数 100 のように,
何度も出てくる上に,数が変わらない数字は,
定数化してしまうのが良い.

C言語では,

#define  

を使用することで,定数を定義できる.

#define ShotNum 100

といった具合だ.

これを,100 と直接数字を記述していた場所すべて置き換える.
それにより,定数定義をした箇所のみを書き換えるだけで,
全ての 数字が自動で置き換わり,大変便利だ.

つまり,完成コードは以下.

完成コード

#include "DxLib.h"
#define ShotNum 100
float playerPosX = 320.0f;
float playerPosY = 340.0f;
float shotPosX[ ShotNum ];
float shotPosY[ ShotNum ];
int counter = 0;
int shotId = 0;
void DrawPlayer( float x, float y )
{
// 本体の表示
DrawBox(x-16, y-16, x+16, y+16, 0xff0088ff, 0);
}
void DrawShot( float x, float y )
{
DrawBox(x-2, y-16, x+2, y+16, 0xff0088ff, 0);
}
void UpdateShot()
{
float speed = 40;
for( int i = 0; i < ShotNum; i++ )
{
shotPosY[ i ] -= speed;
DrawShot( shotPosX[ i ], shotPosY[ i ] );
}
}
void StartShot( float x, float y, int id )
{
shotPosX[id] = x;
shotPosY[id] = y;
}
void UpdatePlayer()
{
if( CheckHitKey( KEY_INPUT_RIGHT ) == 1 )
{
playerPosX += 5;
}
if( CheckHitKey( KEY_INPUT_LEFT ) == 1 )
{
playerPosX -= 5;
}
if( CheckHitKey( KEY_INPUT_UP ) == 1 )
{
playerPosY -= 5;
}
if( CheckHitKey( KEY_INPUT_DOWN ) == 1 )
{
playerPosY += 5;
}
if( CheckHitKey( KEY_INPUT_Z ) == 1 )
{
StartShot( playerPosX, playerPosY, shotId );
shotId++;
if( shotId >= ShotNum )
{
shotId = 0;
}
}
DrawPlayer( playerPosX, playerPosY );
}
// プログラムは WinMain から始まります
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow )
//int main()
{
ChangeWindowMode( true );
if( DxLib_Init() == -1 )     // DXライブラリ初期化処理
{
return -1 ;          // エラーが起きたら直ちに終了
}
// ゲームループ
while( ProcessMessage() == 0 )
{
ClearDrawScreen(); // 画面を消す
UpdatePlayer();
UpdateShot();
ScreenFlip(); //裏画面を表画面に反映
}
WaitKey();
DxLib_End() ;               // DXライブラリ使用の終了処理
return 0 ;               // ソフトの終了 
}

問題なく動くはずだ.

f:id:gothlab:20210806222543j:plain

おわりに

気づいたら 5000 字超えてた..

今回は自機の移動に加えて,ショットの発射まで行った.
次回は軽めで敵の出現を行おう.

次回はこちら

*1:古典的タスクシステムとかECSとかも含まれると思う

  • この記事を書いた人

GOTH

鹿児島県出身,吉祥寺在住の27歳.職業はゲーム会社でプログラマー.趣味はバイク,車,キャンプ,ガジェット,読書,そしてゲーム開発. サイトのテーマはプログラミングとガジェットでライフハック.たまに趣味に関する雑感や記録を残していく備忘録.ツイッターもやってます.
自己紹介
お問い合わせ

-C++, DXライブラリ, ゲーム開発, プログラミング講座