敵キャラの出現パターンを作る

前項で、敵キャラをタイプ別に移動させることはできるようになった。ここでは、自分が出現させたい順番・種類・間隔で敵キャラを出現させる方法を紹介する。

考え方

いろいろな考え方があるが、今回は、敵キャラの出現パターンをデータで管理する方法を紹介する。出現パターンをデータで管理すれば、プログラムの修正を行うことなく、敵キャラを思い通りに出現させることができる

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

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

例えば、次のようなデータになる。

出現させるまでの時間
(ミリ秒)
出現させる
タイプ
備考
10000ゲーム開始から1000ミリ秒経過したら、タイプ0の敵キャラを出現させる
5000前の敵キャラを出現させてから500ミリ秒経過したら、タイプ0の敵キャラを出現させる
5000
5000
5000
10001
3001
3002
3002
3001
3002
15003
1004
1003
1004
1003
1004

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

プログラムへの組み込み

では実際に、プログラムに組み込む方法を解説する。

1.敵キャラ出現データ用構造体の作成

敵キャラ出現データは「出現までの時間」と「出現させるキャラの種類」の2つである。このデータを出現させたい敵キャラ数だけ用意する必要がある。
他のソースから利用する(かもしれない)ことを考え、NKC_Common.hに構造体を作成する。

/* 敵キャラ出現データ */
typedef struct _ENEMYOUTDATA {
    DWORD time;         // 出現までの時間
    int type;           // 出現させるキャラクタの種別
} ENEMYOUTDATA, *LPENEMYOUTDATA;

2.敵キャラ出現データを格納する構造体変数を宣言

敵キャラの出現はゲーム処理で行うため、それらの処理で必要なグローバル変数はGame.cppに宣言するのがいいだろう。次のようにプログラムを修正する。

//=============================================================================
//  ゲーム処理関係の自作関数群
//  Copyright NKC Game Staff(←自分の名前) 
//-----------------------------------------------------------------------------
#include "NKC_Common.h"

// マクロ
#define STAGEMAX    3       // ステージ数
#define ENEMYMAX    100     // 敵キャラの最大数
//#define ENEMYTIME 1000	// 敵キャラの出現タイミング
#define ENEMYDATAMAX 100    // 敵キャラデータ件数の最大
#define ENEMYDATAEND 99999  // 敵キャラデータ終了フラグ

// グローバル変数(自ソースでのみ使用するもの)
/* 自ソースでのみ利用するもの */
//---- ステージ別情報
static int gl_StageNo;              // ステージ番号管理
//---- 背景
static TLVERTX BackVertex[4];       // 頂点情報配列
//---- 自機
static STATUS MyChara;              // 自キャラステータス情報
//---- 敵
static STATUS EnemyInitData;        // 敵キャラステータス情報初期化データ
static STATUS Enemy[ENEMYMAX];      // 敵キャラステータス情報
static DWORD gl_EnemyTime;          // 敵キャラ出現時間
static ENEMYOUTDATA EnemyOutData[ENEMYDATAMAX];// 敵キャラ出現データ
static int gl_EnemyCnt;             // 敵キャラ出現データのカウント
・
・
・

《POINT》

3.ステージ開始時初期化処理の修正

作成した出現データ用配列に、出現データをセットする処理を追加する。本来ならファイルから読み込むべきだが、とりあえず、プログラム内にデータを記述する。

//-----------------------------------------------------------------------------
// 関数名 : GameStageInit()
// 機能概要: ステージ開始時初期化処理
//-----------------------------------------------------------------------------
void GameStageInit(void)
{
    int i;

    //--------------------------------------------------- 各変数の初期化
    // ステージ別に使用するテクスチャの作成
    CreateGameStageTexture(gl_StageNo);
    // ポリゴンの初期化
    /* 自キャラ */
    InitVertex(MyChara.Vertex, 272.0f, 380.0f, 368.0f, 476.0f, 255); // 頂点データ
    SetRect(&MyChara.HitRect, 10, 10, 10, 10);                       // 当たり判定矩形
    MyChara.MoveX = 2.0f;
    MyChara.MoveY = 2.0f;
    /* 敵キャラ出現データのカウント */
    gl_EnemyCnt = 0;
    /* 敵キャラ出現データをセット(ステージ別に) */
    switch ( gl_StageNo ) {
        case 1: // 1面での初期化
            EnemyOutData[0].time = 1000; EnemyOutData[0].type = 0;
            EnemyOutData[1].time = 500; EnemyOutData[1].type = 0;
            EnemyOutData[2].time = 500; EnemyOutData[2].type = 0;
            EnemyOutData[3].time = 500; EnemyOutData[3].type = 0;
            EnemyOutData[4].time = 500; EnemyOutData[4].type = 0;
            EnemyOutData[5].time = 1000; EnemyOutData[5].type = 1;
            EnemyOutData[6].time = 300; EnemyOutData[6].type = 1;
            EnemyOutData[7].time = 300; EnemyOutData[7].type = 2;
            EnemyOutData[8].time = 300; EnemyOutData[8].type = 2;
            EnemyOutData[9].time = 300; EnemyOutData[9].type = 1;
            EnemyOutData[10].time = 300; EnemyOutData[10].type = 2;
            EnemyOutData[11].time = 1500; EnemyOutData[11].type = 3;
            EnemyOutData[12].time = 100; EnemyOutData[12].type = 4;
            EnemyOutData[13].time = 100; EnemyOutData[13].type = 3;
            EnemyOutData[14].time = 100; EnemyOutData[14].type = 4;
            EnemyOutData[15].time = 100; EnemyOutData[15].type = 3;
            EnemyOutData[16].time = 100; EnemyOutData[16].type = 4;
            EnemyOutData[17].time = ENEMYDATAEND;
            break;
        case 2: // 2面での初期化
            break;
        case 3: // 3面での初期化
            break;
    }
    /* 敵キャラ初期化データ */
    SetEnemyInit(&EnemyInitData,
        288.0f, -64.0f, 352.0f, 0.0f,	// 表示位置
        5, 5, 5, 5,						// 当たり判定矩形
        2.0f, 2.0f,						// 移動量
        0, 1							// タイプ・ライフ
    );
    /* 敵キャラ */
    for ( i=0 ; i<ENEMYMAX ; i++ ) Enemy[i].life = DEAD;
    /* 敵キャラ出現時間のクリア */
    gl_EnemyTime = 0;

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

}

《POINT》

4.敵キャラ出現処理

敵キャラは、設定したデータに従って出現させる。敵キャラ出現処理を次のように修正する。

//-----------------------------------------------------------------------------
// 関数名 : SetEnemy()
// 機能概要: 敵キャラ発生処理
//-----------------------------------------------------------------------------
static void SetEnemy(void)
{
    int i;
#ifdef DEBUG
    char buff[80];
#endif

    /* EnemyOutDataに従って敵キャラを出現させる */
    if ( EnemyOutData[gl_EnemyCnt].time != ENEMYDATAEND && gl_nowTime - gl_EnemyTime >= EnemyOutData[gl_EnemyCnt].time ) {
        gl_EnemyTime = gl_nowTime;
        /* flgが0のデータを検索し、初期情報をセット(敵キャラが死ぬことを考えてある) */
        for ( i=0 ; i<ENEMYMAX ; i++ ) {
            if ( Enemy[i].life == DEAD ) {
                Enemy[i] = EnemyInitData; // 一括代入
                Enemy[i].type = EnemyOutData[gl_EnemyCnt].type; // タイプをセット
                gl_EnemyCnt++; // 敵キャラ出現データのカウントアップ
#ifdef DEBUG
                wsprintf(buff, "配列の %d 番目にセット\n", i);
                OutputDebugString(buff);
#endif
                return; // 見つかれば、関数を抜ける
            }
        }
    }
}

《POINT》

ここまでの修正を行い、初期化データどおりに敵キャラが出現するかを確かめる。また、2面用、3面用のデータも作成し、ステージを切り替えたら、それぞれの初期化データどおりに敵キャラが出現するかを確かめる。

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

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

※C言語のファイル操作関数を利用する。分からない人はC言語教科書の第13章ファイル操作、またはWeb教材のC言語入門の第15章を参照すること。

1.敵キャラ出現データファイルの作成

敵キャラ出現データファイルを、ステージ別に作成する。ファイル名は「Enemy1.dat」「Enemy2.dat」「Enemy3.dat」とし、ソースファイルと同じフォルダに配置する。例えばステージ1用のデータファイルの内容は次のようになる。

1000,0
500,0
500,0
500,0
500,0
1000,1
300,1
300,2
300,2
300,1
300,2
1500,3
100,4
100,3
100,4
100,3
100,4

《POINT》

同様に、ステージ2、ステージ3用のデータファイルを作成しておくこと。

2.敵キャラ出現データファイル名の宣言

Game.cppのグローバル変数の宣言に、敵キャラ出現データのファイル名を宣言する。

// グローバル変数
/* 自ソースでのみ利用するもの */
//---- ステージ別情報
static int gl_StageNo;              // ステージ番号管理
//---- 背景
static TLVERTX BackVertex[4];       // 頂点情報配列
//---- 自機
static STATUS MyChara;              // 自キャラステータス情報
//---- 敵
static STATUS EnemyInitData;        // 敵キャラステータス情報初期化データ
static STATUS Enemy[ENEMYMAX];      // 敵キャラステータス情報
static DWORD gl_EnemyTime;          // 敵キャラ出現時間
static ENEMYOUTDATA EnemyOutData[ENEMYDATAMAX];// 敵キャラ出現データ
static int gl_EnemyCnt;             // 敵キャラ出現データのカウント
static char* szEnemyOutData[] = {"", "Enemy1.dat", "Enemy2.dat", "Enemy3.dat"}; // 敵キャラ出現データファイル名

3.ステージ別初期化処理の修正

ステージ別にデータファイルを読み込み、配列に格納するように、ステージ別初期化処理を次のように修正する。

//-----------------------------------------------------------------------------
// 関数名 : GameStageInit()
// 機能概要: ステージ開始時初期化処理
//-----------------------------------------------------------------------------
void GameStageInit(void)
{
    FILE *fp;
	char buff[80];
    DWORD time;
    int type;
    int i;

    //--------------------------------------------------- 各変数の初期化
    // ステージ別に使用するテクスチャの作成
    CreateGameStageTexture(gl_StageNo);
    // ポリゴンの初期化
    /* 自キャラ */
    InitVertex(MyChara.Vertex, 272.0f, 380.0f, 368.0f, 476.0f, 255); // 頂点データ
    SetRect(&MyChara.HitRect, 10, 10, 10, 10);                       // 当たり判定矩形
    MyChara.MoveX = 2.0f;
    MyChara.MoveY = 2.0f;
    /* 敵キャラ出現データのカウント */
    gl_EnemyCnt = 0;
    /* 敵キャラ出現データをセット(ステージ別に) */
    if ( (fp = fopen(szEnemyOutData[gl_StageNo], "r")) == NULL ) {
        wsprintf(buff, "敵キャラ出現データファイル「%s」読み込みエラー\n", szEnemyOutData[gl_StageNo]);
	    MessageBox(hWnd, buff, "ERROR", MB_OK);
        return;
    }
    for ( i=0 ; fscanf(fp, "%d,%d", &time, &type) != EOF ; i++ ) {
        EnemyOutData[i].time = time;
        EnemyOutData[i].type = type;
    }
    EnemyOutData[i].time = ENEMYDATAEND;
    fclose(fp);
    /* 敵キャラ初期化データ */
    SetEnemyInit(&EnemyInitData,
        288.0f, -64.0f, 352.0f, 0.0f,	// 表示位置
        5, 5, 5, 5,						// 当たり判定矩形
        2.0f, 2.0f,						// 移動量
        0, 1							// タイプ・ライフ
    );
    /* 敵キャラ */
    for ( i=0 ; i<ENEMYMAX ; i++ ) Enemy[i].life = DEAD;
    /* 敵キャラ出現時間のクリア */
    gl_EnemyTime = 0;

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

}

《POINT》

※ここまでの修正を行い、出現データファイルどおりに敵キャラが出現するかを確かめる。ステージを切り替えたら、それぞれの出現データどおりに敵キャラが出現するかを確かめる。

《参考》

上の例では、ファイルからデータを1行読み込み、配列に格納するプログラムを次のように作成した。

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

よーく考えると、次のように簡略化できる。

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

for文の中で行う処理が無いため、for文の終わりにセミコロンを付ける(または、中身の無い括弧「{}」を付ける)必要がある。この場合、GameStageInit関数内のtimeやtype変数の宣言が不要になる。


BACK(敵キャラの出現パターンを作る) NEXT(タイプ別に初期化データを変える)