第14章 複数のキャラクタを制御する(シューティング向け?)

14-4 敵の出現パターンを作る(グラディウス系?)

敵キャラをランダムに出現させるのではなく、自分が出現させたい順番・種類・間隔で敵キャラを出現させる方法を考える。

1. 考え方

いろいろな考え方があるが、今回は一番シンプル(だと思われる)方法を紹介する。

敵キャラを自分の意図した方法で出現させるには、次のデータを持てばよい。

  1. 前の敵キャラが出てから、次に表示するまでの時間
  2. 表示する敵キャラのタイプ

例)、1秒ごとに敵キャラを順番に出現させるためのデータ

出現させるまでの時間(ミリ秒)出現させるタイプ
10000
10001
10002
10003
10004
10000
10001
10002

このようにデータを持つことにより、ステージ別に出現させる敵キャラを自由に制御できる。このデータを配列に格納し、次のようにプログラムを作成すれば、配列のデータに従って敵キャラが出現する。

2. 敵キャラ出現データ格納変数の作成

敵キャラ出現データは「出現までの時間」と「出現させるキャラの種類」の2つである。このデータを出現させたい数だけ用意する必要がある。
今後、データの項目が増えることを考え、common.hに構造体を作成する。

//-----------------------------------------------------------------------------
// 構造体・列挙型
//-----------------------------------------------------------------------------
・
・
・
/* 敵キャラ出現データ */
typedef struct tarENEMYDATA {
    DWORD time;			// 出現までの時間
    int type;			// 出現させるキャラクタの種別
} ENEMYDATA;

次に、以下のマクロをcommon.hに宣言する。

#define ENEMY_DATAMAX 100 // 敵キャラデータ件数の最大
#define ENEMY_DATAEND 99999 // 敵キャラデータ終了フラグ

※ステージによって出現させる敵キャラの個数が違うため、データの終わりを示すマクロを宣言している

次に、出現データを格納するENEMYDATA構造体の形を持つ配列と、配列の指標に使う変数をGame.cppにグローバル変数として宣言する。

//=============================================================================
//	Game処理関係の自作関数群
//=============================================================================
#include "common.h"

//-----------------------------------------------------------------------------
// マクロ(ソース内でしか使わないもの)
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
// プロトタイプ宣言(ソース内でしか使わないもの)
//-----------------------------------------------------------------------------
・
・
・

//-----------------------------------------------------------------------------
// ゲーム処理用グローバル変数
//-----------------------------------------------------------------------------
・
・
・
static ENEMYSTATUS Enemy[ENEMY_MAX];			// 敵キャラのステータス
static ENEMYBLTSTATUS EnemyBlt[ENEMYTYPE_MAX];	// 敵キャラ画像データ
static ENEMYDATA EnemyData[ENEMY_DATAMAX];	// 敵キャラ出現データ
static int EnemyCnt;	// 敵キャラ出現データのカウント
・
・
・

※EnemyData配列に出現データを格納する
※EnemyData参照用にEnemyCntを利用する(例、EnemyData[EnemyCnt].time)

3. ゲーム開始時、ステージ開始時初期化処理

ゲーム開始時の初期化は次のように行う。

//-----------------------------------------------------------------------------
// 関数名 : GameInit()
// 機能概要: ゲーム開始時初期化処理
//-----------------------------------------------------------------------------
void GameInit(HWND hWnd)
{
    //------------------------------------------------------- キャラクタ用サーフェイスの生成
    CreateGameCharaSurface();

    //------------------------------------------------------- 各変数の初期化
    StargeNumber = 1;	// ステージ番号(1面から開始)
    MyCharaStatusInit(&MyChara, 0, 0, 40, 40, 1, 0); // 自キャラ
    EnemyBltStatusInit(&EnemyBlt[0], 0, 394, 32, 426, 10, 5, 22, 27, 100); // 敵キャラ0
    EnemyBltStatusInit(&EnemyBlt[1], 0, 218, 32, 250, 8, 2, 24, 30, 100); // 敵キャラ1
    EnemyBltStatusInit(&EnemyBlt[2], 0, 282, 32, 314, 8, 2, 24, 30, 100); // 敵キャラ2
    EnemyBltStatusInit(&EnemyBlt[3], 0, 346, 48, 394, 5, 5, 28, 44, 100); // 敵キャラ3
    EnemyBltStatusInit(&EnemyBlt[4], 0, 346, 48, 394, 5, 5, 28, 44, 100); // 敵キャラ4

    //------------------------------------------------------- フレームナンバーセット
    g_FrameNo = GAME_STARGE_INIT;

}

※敵キャラ出現データはステージ別に作成するため、ここでは何もしない

ステージ別初期化処理は次のようになる。

//-----------------------------------------------------------------------------
// 関数名 : GameStargeInit()
// 機能概要: ステージ毎初期化処理
//-----------------------------------------------------------------------------
void GameStargeInit(HWND hWnd)
{
    int i;

    /* ゲーム背景用サーフェイスの生成 */
    CreateGameBackImgSurface(StargeNumber);	// ステージ番号によって、ロードする背景を変える

    /* 各ステージ毎に初期化する */
    MyCharaStatusStargeInit(&MyChara, 300, 400, 2, 2); // 自キャラ
    for (i=0 ; i<ENEMY_MAX ; i++) // 敵キャラ
    {
        Enemy[i].life = DEAD;
    }
    EnemyTime = nowTickCount;	// 敵キャラ出現時間制御
    EnemyCnt = 0;				// 敵キャラ出現データのカウント

    /* ステージ別に初期化する */
    switch (StargeNumber)
    {
        case 1: // 1面での初期化
            EnemyData[0].time = 500; EnemyData[0].type = 0;
            EnemyData[1].time = 500; EnemyData[1].type = 0;
            EnemyData[2].time = 500; EnemyData[2].type = 0;
            EnemyData[3].time = 500; EnemyData[3].type = 0;
            EnemyData[4].time = 500; EnemyData[4].type = 0;
            EnemyData[5].time = 500; EnemyData[5].type = 0;
            EnemyData[6].time = 500; EnemyData[6].type = 0;
            EnemyData[7].time = 500; EnemyData[7].type = 0;
            EnemyData[8].time = 500; EnemyData[8].type = 0;
            EnemyData[9].time = 500; EnemyData[9].type = 0;
            EnemyData[10].time = 2000; EnemyData[10].type = 1;
            EnemyData[11].time = 500; EnemyData[11].type = 1;
            EnemyData[12].time = 500; EnemyData[12].type = 1;
            EnemyData[13].time = 500; EnemyData[13].type = 1;
            EnemyData[14].time = 500; EnemyData[14].type = 1;
            EnemyData[15].time = ENEMY_DATAEND;
            break;
        case 2: // 2面での初期化
            break;
        case 3: // 3面での初期化
            break;
    }

    //------------------------------------------------------- フレームナンバーセット
    g_FrameNo = GAME_FRAME;

}

※1面のみ、出現データをセットしている
※データの終わりを示すため、最後のデータのtimeにENEMY_DATAENDをセットしている
※配列の個数を超えたデータをセットするとバグが発生する

4. 敵キャラ出現処理

敵キャラは一定時間ごとにランダムに出現させるのではなく、設定したデータにしたがって出現させる。敵キャラ出現処理を次のように作成する。

//-----------------------------------------------------------------------------
// 関数名 : SetEnemy()
// 機能概要: 出現する敵キャラをセットする
//-----------------------------------------------------------------------------
static void SetEnemy(void)
{
    int i;

    /* EnemyDataに従って敵キャラを出現させる */
    if (EnemyData[EnemyCnt].time != ENEMY_DATAEND && nowTickCount - EnemyTime >= EnemyData[EnemyCnt].time) 
    {
        for (i=0 ; i<ENEMY_MAX ; i++)
        {
            if (Enemy[i].life == DEAD)
            {
                EnemyTime = nowTickCount;
                EnemyStatusInit(&Enemy[i], EnemyData[EnemyCnt].type);
                EnemyCnt++;
                break;
            }
        }
    }

}
  1. 配列の先頭から調べる
  2. データが残っていて、かつ、出現時間になったかどうかを調べる
  3. 出現時間になったら、出現させる敵キャラをセットする
  4. 出現させる敵キャラをセットしたらカウントを+1し、for文を抜ける

ここまでの修正を行い、プログラムを実行すると、セットしたデータの通りに敵キャラが出現する。
2面、3面用のデータをそれぞれセットすれば、ステージが切り替わるとそれに対応してデータにしたがって敵キャラが出現する。

第14章4確認問題1(必須問題)

14−3で作成したプログラムに上記修正を行い、1面のデータどおりに敵キャラが出現するかどうかを確認しなさい。

実行結果サンプル

第14章4確認問題2(自由問題)

2面、3面のデータを自分の好きなように作成し、ステージが切り替わってもステージ別のデータで敵キャラが出現するかどうかを確認しなさい。

5. 敵キャラ出現データをファイルで管理する

敵キャラ出現データをプログラム内に持つのははっきりいって美しくない。よって、データをファイルで管理し、ステージ別にデータファイルを読み込むようプログラムを改造する。

《File.cppの作成》

//=============================================================================
//	ファイル処理関係の自作関数群
//=============================================================================
#include "common.h"

//-----------------------------------------------------------------------------
// プロトタイプ宣言(ソース内でしか使わないもの)
//-----------------------------------------------------------------------------


//-----------------------------------------------------------------------------
// グローバル変数
//-----------------------------------------------------------------------------
static char FileName[][20] = { // データファイル名
    "", "EnemyData1.dat", "EnemyData2.dat", "EnemyData3.dat"
};

//-----------------------------------------------------------------------------
// 関数名 : EnemyDataRead()
// 機能概要: ステージ別にデータファイルを読み込み、配列にセットする
//-----------------------------------------------------------------------------
void EnemyDataRead(ENEMYDATA *data, int Number)
{
    FILE *fp;
    int i;
    DWORD time;
    int type;

    if ((fp = fopen(FileName[Number], "r")) == NULL)
    {
        OutputDebugString("敵キャラデータファイル読み込みエラー\n");
        return;
    }

    i = 0;
    while(fscanf(fp, "%d,%d", &time, &type) != EOF)
    {
        data[i].time = time;
        data[i].type = type;
        i++;
    }
    data[i].time = ENEMY_DATAEND;

    fclose(fp);

}

※ステージ番号は1から始まるため、ファイル名格納配列の0番目には空データを入れる
※格納する配列のアドレスとステージ番号をもらい、該当するデータファイルを読み込んで配列に格納する
※データをすべて読み込んだら、データ終了判定用の値をセットする

《File.hの作成》

//-----------------------------------------------------------------------------
// File.cppの関数のうち、他のソースで利用される関数のプロトタイプ宣言
//-----------------------------------------------------------------------------
void EnemyDataRead(ENEMYDATA *, int);

《ステージ別初期化処理の修正》

//-----------------------------------------------------------------------------
// 関数名 : GameStargeInit()
// 機能概要: ステージ毎初期化処理
//-----------------------------------------------------------------------------
void GameStargeInit(HWND hWnd)
{
    int i;

    /* ゲーム背景用サーフェイスの生成 */
    CreateGameBackImgSurface(StargeNumber);	// ステージ番号によって、ロードする背景を変える
	
    /* 各ステージ毎に初期化する */
    MyCharaStatusStargeInit(&MyChara, 300, 400, 2, 2); // 自キャラ
    for (i=0 ; i<ENEMY_MAX ; i++) // 敵キャラ
    {
        Enemy[i].life = DEAD;
    }
    EnemyTime = nowTickCount;	// 敵キャラ出現時間制御
    EnemyCnt = 0;				// 敵キャラ出現データのカウント
    EnemyDataRead(EnemyData, StargeNumber);	// 敵キャラ出現データを読み込む

    /* ステージ別に初期化する */
    switch (StargeNumber)
    {
        case 1: // 1面での初期化
            break;
        case 2: // 2面での初期化
            break;
        case 3: // 3面での初期化
            break;
    }

    //------------------------------------------------------- フレームナンバーセット
    g_FrameNo = GAME_FRAME;

}

※配列のアドレスを渡すことにより、呼び出す関数に配列データをセットしてもらう

後は、ステージ別のデータファイルを作成すれば、そのデータに従って敵キャラが出現する。

第14章4確認問題3(必須問題)

1〜4で作成したプログラムに上記修正を行い、作成したテキストデータどおりに敵キャラが出現するかどうかを確認しなさい。データは自分で用意すること。

余談

データファイルを読み込んで配列に格納する処理は、もっと簡単に書くことができる。

//-----------------------------------------------------------------------------
// 関数名 : EnemyDataRead()
// 機能概要: ステージ別にデータファイルを読み込み、配列にセットする
//-----------------------------------------------------------------------------
void EnemyDataRead(ENEMYDATA *data, int Number)
{
    FILE *fp;
    int i;

    if ((fp = fopen(FileName[Number], "r")) == NULL)
    {
        OutputDebugString("敵キャラデータファイル読み込みエラー\n");
        return;
    }

    for (i=0 ; fscanf(fp, "%d,%d", &data[i].time, &data[i].type) != EOF; i++)
        ;
    data[i].time = ENEMY_DATAEND;

    fclose(fp);

}

なぜこれでOKなのか、考えてみよう。


[ TOP ]