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

14-6 リスト構造によるキャラクタ管理

今までのプログラムでは、敵キャラのデータを配列によって管理してきた。配列に格納されるデータの件数が不特定である場合、次のような不都合が考えられる。

  1. データがどこまで入っているかを判断する処理が必要になる
  2. 配列の大きさを超えないようにする処理が必要になる。

ここでは、複数の敵キャラデータを配列を使わずに制御する手段を学ぶ。

1. 配列とは?(復習)

配列とは、同じデータ型を持つ変数を複数用意し、同じ変数名で扱うための技である。

《例、キーボードから入力した値を配列に格納し、表示するプログラム》

#include <stdio.h>

#define DATAMAX 10
#define DATAEND -1

void main(void)
{
    int data[DATAMAX];
    int inputdata, i;

    /* データ入力処理 */
    for (i=0 ; scanf("%d", &inputdata) != EOF ; i++)
    {
        data[i] = inputdata;
    }
    data[i] = DATAEND;  // データが終わったことを示す

    /* 入力されたデータを表示する */
    for (i=0 ; data[i] != DATAEND ; i++)
    {
        printf("data[%d] = %d\n", i, data[i]);
    }

}

※CTRL+Zが入力されるまで、入力処理が行われる
※入力件数が不特定なため、データの終わりを示す値を配列の最後にセットしている
(入力件数を数えてもよい)
※10件以上のデータを入力してもエラーにはならないが、値は保障されなくなる

2. リスト構造とは?

連続するデータを配列のように添え字で区別するのではなく、データが格納されているアドレスをたどっていく方法。
1件のデータに、前のデータが格納されているアドレスと、次のデータが格納されているアドレスを持たなければならないため、データ1件の容量は多少多くなるが、配列の大きさによる限界がなくなる。

配列は、配列宣言時にメモリ上にすべての領域を確保するが、リスト構造を用いると、データが発生するたびにメモリ領域を1つずつ確保すればよい。

3. リスト構造によるデータ格納処理の考え方

リスト構造を使って複数のデータを扱う場合、次の2通りの方法が考えられる。

FIFO(Fast In Fast Out:先入れ先出し)方式
存在しているデータを順番に検索するとき、先に作成した(古い)データから順にたどっていく
LIFO(Last In Fast Out:後入れ先出し)方式
存在しているデータを順番に検索するとき、最後に作成した(最新の)データから、逆順にたどっていく

※リスト構造を使う場合、実データのほかに、次のデータへのアドレスと前のデータへのアドレスを格納するポインタ変数を用意しなければならない

では、連続データをリスト構造を使って格納する方法を考えてみよう。今回はLIFO方式で考えてみる。

※次データのアドレスがNULLであるデータが最新のデータであり、前データのアドレスがNULLであるデータが最も古いデータである

4. LIFOリスト構造によるデータ検索処理の考え方

LIFOリスト構造でデータを管理している場合。スタートポインタには最後に格納したデータへのアドレスが格納されている。よって、スタートポインタが示す先のデータから逆順にアドレスを追うことで、すべてのデータを検索できる。

※LIFO方式の場合、検索には次データへのアドレスは使わない

5. LIFOリスト構造を用いたプログラム例

入力データを配列に格納し、表示するプログラムをリスト構造LIFO方式を使って記述すると次のようになる。

/* 入力した数値を配列に格納する */
#include <stdio.h>
#include <stdlib.h>

typedef struct tarDATATBL {
    int         data;   // データ
    tarDATATBL  *next;  // 次アドレス
    tarDATATBL  *prev;  // 前アドレス
} DATATBL;

void main(void)
{
    DATATBL *start_p = NULL;    // スタートポインタ
    DATATBL *wp;                // 作業用ポインタ
    int inputdata;

    /* データ入力 */
    while(scanf("%d", &inputdata) != EOF)
    {
        wp = (DATATBL *)malloc(sizeof(DATATBL));
        wp->data = inputdata;
        wp->next = NULL;
        wp->prev = start_p;
        if (wp->prev != NULL)
            wp->prev->next = wp;
        start_p = wp;
    }

    /* 登録データ確認(LIFO方式のため、逆順に表示される) */
    wp = start_p;
    while (wp != NULL)
    {
        printf("%d\n", wp->data);
        wp = wp->prev;
    }

}

※プログラムを入力、実行し、キーボードから入力したデータがちゃんと表示されるかどうかを確かめよう

《注意!!》

構造体の宣言時にその構造体名を使うというやりかたは、Cではサポートされていないため、拡張子をcppにする。

6. LIFOリスト構造を用いたデータの削除方法

では、LIFOリスト構造で管理されているデータのうち、特定のデータを消すにはどうしたらいいだろうか?
リスト構造で管理されているデータは、次のデータへのアドレスと、前のデータへのアドレスを持っている。であれば、削除したいデータへはたどれないように、前後のデータが持つアドレスを変更すればよい。

※使わなくなったメモリ領域は開放しないと、プログラム実行中はロックされたままになってしまうので、明示的に開放するべきである

プログラムにすると、次のようになる。

typedef struct tarDATATBL {
    int         data;   // データ
    tarDATATBL  *next;  // 次アドレス
    tarDATATBL  *prev;  // 前アドレス
} DATATBL;

DATATBL *start_p;   // スタートポインタ
DATATBL *wp;        // 作業用ポインタ


・・・wpに削除したいデータのアドレスが入っているとする・・・


/* 前データのnextを更新 */
if (wp->prev != NULL)
    wp->prev->next = wp->next;
/* 次データのprevを更新 */
if (wp->next != NULL)
    wp->next->prev = wp->prev;
else
    start_p = wp->prev;
/* 開放 */
free(wp);

※削除したいデータが一番先頭、または一番最後である場合のことも考えなければならない

では、登録済みのデータを1件ずつ調べ、削除対象であれば削除するようなプログラムはどのように作ればよいだろうか?
前のサンプルに、入力データが100より大きければ削除する処理を追加したものを紹介する。

/* 入力した数値を配列に格納する */
#include <stdio.h>
#include <stdlib.h>

typedef struct tarDATATBL {
    int         data;   // データ
    tarDATATBL  *next;  // 次アドレス
    tarDATATBL  *prev;  // 前アドレス
} DATATBL;

void main(void)
{
    DATATBL *start_p = NULL;    // スタートポインタ
    DATATBL *wp, *wpprev;       // 作業用ポインタ
    int inputdata;

    /* データ入力 */
    while(scanf("%d", &inputdata) != EOF)
    {
        wp = (DATATBL *)malloc(sizeof(DATATBL));
        wp->data = inputdata;
        wp->next = NULL;
        wp->prev = start_p;
        if (wp->prev != NULL)
            wp->prev->next = wp;
        start_p = wp;
    }

    /* 登録データ確認(LIFO方式のため、逆順に表示される) */
    puts("--- CHECK 1 ---");
    wp = start_p;
    while (wp != NULL)
    {
        printf("%d\n", wp->data);
        wp = wp->prev;
    }

    /* 登録データを調べ、100より大きなデータを削除する */
    wp = start_p;
    while (wp != NULL)
    {
        wpprev = wp->prev; // prev退避
        if (wp->data > 100)
        {
            if (wp->prev != NULL)
                wp->prev->next = wp->next;
            if (wp->next != NULL)
                wp->next->prev = wp->prev;
            else
                start_p = wp->prev;
            free(wp);
        }
        wp = wpprev; // 次のデータへ
    }

    /* 修正済み登録データ確認(LIFO方式のため、逆順に表示される) */
    puts("--- CHECK 2 ---");
    wp = start_p;
    while (wp != NULL)
    {
        printf("%d\n", wp->data);
        wp = wp->prev;
    }

}

※開放してしまうと、wp->prevで前のデータが分からなくなってしまうため、退避している

7. LIFOリスト構造を使って、敵キャラクタを制御する

では実際に、LIFOリスト構造を使って敵キャラデータを管理してみる。まずは敵キャラ情報構造体に、前後データのアドレスを格納するメンバを追加する。

/* 敵キャラ・ステータス情報 */
typedef struct tarENEMYSTATUS {
    int type;                       // キャラクタ種別
    void (*Move)(tarENEMYSTATUS *); // 関数ポインタの定義
    int x, y;                       // 表示座標
    int move_x, move_y;             // 移動量
    int life;                       // ライフ
    tarENEMYSTATUS *next;           // 次データへのアドレス
    tarENEMYSTATUS *prev;           // 前データへのアドレス
} ENEMYSTATUS;

次に、Game.cppを次のように修正する。

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

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

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

//-----------------------------------------------------------------------------
// ゲーム処理用グローバル変数
//-----------------------------------------------------------------------------
static int StargeNumber;                        // ステージ数制御用
static DWORD EnemyTime;                         // 敵キャラ出現制御用
static DWORD EndTime;                           // ゲームクリア・ゲームオーバー時の時間制御用
static MYCHARASTATUS MyChara;                   // 自キャラのステータス
static ENEMYSTATUS *Enemy_start, *wEnemy;       // 敵キャラの先頭アドレス、作業用
static ENEMYBLTSTATUS EnemyBlt[ENEMYTYPE_MAX];  // 敵キャラ画像データ
static ENEMYDATA EnemyData[ENEMY_DATAMAX];      // 敵キャラ出現データ
static int EnemyCnt;                            // 敵キャラ出現データのカウント

//-----------------------------------------------------------------------------
// 関数名 : GameInit()
// 機能概要: ゲーム開始時初期化処理
//-----------------------------------------------------------------------------
void GameInit(HWND hWnd)
{
・
・
・
}

//-----------------------------------------------------------------------------
// 関数名 : GameStargeInit()
// 機能概要: ステージ毎初期化処理
//-----------------------------------------------------------------------------
void GameStargeInit(HWND hWnd)
{
    /* ゲーム背景用サーフェイスの生成 */
    CreateGameBackImgSurface(StargeNumber);	// ステージ番号によって、ロードする背景を変える

    /* 各ステージ毎に初期化する */
    MyCharaStatusStargeInit(&MyChara, 300, 400, 2, 2); // 自キャラ
    Enemy_start = NULL;
    EnemyTime = nowTickCount;	// 敵キャラ出現時間制御
    EnemyCnt = 0;				// 敵キャラ出現データのカウント
    EnemyDataRead(EnemyData, StargeNumber);	// 敵キャラ出現データを読み込む
・
・
・
}

//-----------------------------------------------------------------------------
// 関数名 : GameFrame()
// 機能概要: ゲーム画面更新処理
//-----------------------------------------------------------------------------
void GameFrame(HWND hWnd)
{
    /* ゲーム処理 */
    ・
    ・
    ・

    /* ファンクションキーによってステージを切り替える(削除予定) */
    if (KeyTbl[VK_F1] & 0x80)
    {
        StargeNumber = 1;
        EnemyFree();
        g_FrameNo = GAME_STARGE_INIT;
    }
    else if (KeyTbl[VK_F2] & 0x80)
    {
        StargeNumber = 2;
        EnemyFree();
        g_FrameNo = GAME_STARGE_INIT;
    }
    else if (KeyTbl[VK_F3] & 0x80)
    {
        StargeNumber = 3;
        EnemyFree();
        g_FrameNo = GAME_STARGE_INIT;
    }
    else if (KeyTbl[VK_F10] & 0x80)
    {
        EnemyFree();
        g_FrameNo = START_INIT;
    }

}

//-----------------------------------------------------------------------------
// 関数名 : GameOverFrame()
// 機能概要: ゲームオーバー処理
//-----------------------------------------------------------------------------
void GameOverFrame(HWND hWnd)
{
・
・
・
}

//-----------------------------------------------------------------------------
// 関数名 : GameClearFrame()
// 機能概要: ゲームクリア処理
//-----------------------------------------------------------------------------
void GameClearFrame(HWND hWnd)
{
・
・
・
}

//-----------------------------------------------------------------------------
// 関数名 : SetEnemy()
// 機能概要: 出現する敵キャラをセットする
//-----------------------------------------------------------------------------
static void SetEnemy(void)
{
    /* 一定時間ごとに敵キャラを増やす */
    if (EnemyData[EnemyCnt].time != ENEMY_DATAEND && nowTickCount - EnemyTime >= EnemyData[EnemyCnt].time) 
    {
        /* 敵キャラ用領域を生成 */
        wEnemy = (ENEMYSTATUS *)malloc(sizeof(ENEMYSTATUS));
        wEnemy->next = NULL;
        wEnemy->prev = Enemy_start;
        if (wEnemy->prev != NULL)
            wEnemy->prev->next = wEnemy;
        Enemy_start = wEnemy;

        /* 初期化 */
        EnemyStatusInit(wEnemy, EnemyData[EnemyCnt].type);
        EnemyTime = nowTickCount;
        EnemyCnt++;
    }

}

//-----------------------------------------------------------------------------
// 関数名 : BackDraw()
// 機能概要: 背景描画処理
//-----------------------------------------------------------------------------
static void BackDraw(void)
{
・
・
・
}

//-----------------------------------------------------------------------------
// 関数名 : CharDraw()
// 機能概要: キャラクタ描画処理
//-----------------------------------------------------------------------------
static void CharDraw(void)
{
    HRESULT hRet;

    /* 自キャラ */
    hRet = g_pDDSBack->BltFast(MyChara.x, MyChara.y, g_pDDSChara, &MyChara.rect, DDBLTFAST_SRCCOLORKEY);
    if (hRet != DD_OK)
        return;

    /* 敵キャラ */
    for (wEnemy=Enemy_start ; wEnemy != NULL ; wEnemy=wEnemy->prev)
    {
        hRet = myBltFastClip(wEnemy->x, wEnemy->y, g_pDDSChara, EnemyBlt[wEnemy->type].rect, DDBLTFAST_SRCCOLORKEY, ScreenRect);
        if (hRet != DD_OK)
            return;
    }

}

//-----------------------------------------------------------------------------
// 関数名 : CharMove()
// 機能概要: キャラクタ移動処理
//-----------------------------------------------------------------------------
static void CharMove(void)
{
・
・
・
    /* 敵キャラの移動 */
    for (wEnemy=Enemy_start ; wEnemy != NULL ; wEnemy=wEnemy->prev)
    {
        wEnemy->Move(wEnemy);
    }

}

//-----------------------------------------------------------------------------
// 関数名 : ScreenCheck()
// 機能概要: キャラクタがゲーム画面をはみ出している時の処理
//-----------------------------------------------------------------------------
static void ScreenHitCheck(void)
{
    ENEMYSTATUS *prev;

    /* 自キャラとゲーム画面(画面全体)をチェック */
    if (MyChara.x < ScreenRect.left)
        MyChara.x = ScreenRect.left;
    else if (MyChara.x > ScreenRect.right - MyChara.width)
        MyChara.x = ScreenRect.right - MyChara.width;

    if (MyChara.y < ScreenRect.top)
        MyChara.y = ScreenRect.top;
    else if (MyChara.y > ScreenRect.bottom - MyChara.height)
        MyChara.y = ScreenRect.bottom - MyChara.height;

    /* 敵キャラとゲーム画面(画面全体)をチェック(クリッピングを考慮) */
    wEnemy = Enemy_start;
    while (wEnemy != NULL)
    {
        prev = wEnemy->prev;
        if (wEnemy->x <= ScreenRect.left - EnemyBlt[wEnemy->type].width ||
            wEnemy->x >= ScreenRect.right ||
            wEnemy->y <= ScreenRect.top - EnemyBlt[wEnemy->type].height ||
            wEnemy->y >= ScreenRect.bottom)
        {
            /* 衝突した敵キャラを削除する */
            if (wEnemy->prev != NULL)
                wEnemy->prev->next = wEnemy->next;
            if (wEnemy->next != NULL)
                wEnemy->next->prev = wEnemy->prev;
            else
                Enemy_start = wEnemy->prev;
            free(wEnemy);
        }
        wEnemy = prev;
    }

}

//-----------------------------------------------------------------------------
// 関数名 : CharHitCheck()
// 機能概要: キャラクタ同士の当たり判定と処理
//-----------------------------------------------------------------------------
static void CharHitCheck(void)
{
    RECT r1, r2;

    /* 自キャラの位置情報をセット */
    SetRect(&r1, MyChara.x, MyChara.y, MyChara.x + MyChara.width, MyChara.y + MyChara.height);

    /* 自キャラと敵キャラの衝突を調べ、当たっていたらライフをDEADにする */
    wEnemy = Enemy_start;
    while (wEnemy != NULL)
    {
        CreateHitRect(&r2, wEnemy->x, wEnemy->y, &EnemyBlt[wEnemy->type].hitrect);
        if (HitCheck(r1, r2))
        {
            MyChara.life = DEAD;
            break; // 以降の当たり判定を行わずにfor文を抜ける
        }
        wEnemy = wEnemy->prev;
    }

    /* 自キャラが死んだら、ゲームオーバー */
    if (MyChara.life == DEAD)
    {
        g_FrameNo = GAME_OVER;
        EndTime = nowTickCount;
    }

}

//-----------------------------------------------------------------------------
// 関数名 : MyCharaStatusInit()
// 機能概要: ゲーム開始時ステータス情報初期化(自キャラ用)
//-----------------------------------------------------------------------------
static void MyCharaStatusInit(
	MYCHARASTATUS *status,
	int left, int top, int right, int bottom, // キャラクタ画像の矩形
	int life, // ライフ
	int score // ゲーム開始時間
)
{
・
・
・
}

//-----------------------------------------------------------------------------
// 関数名 : MyCharaStatusStargeInit()
// 機能概要: ステージ切り替え時ステータス情報初期化
//-----------------------------------------------------------------------------
static void MyCharaStatusStargeInit(
	MYCHARASTATUS *status,
	int x, int y, // 表示座標
	int move_x, int move_y // 移動量
)
{
・
・
・
}

//-----------------------------------------------------------------------------
// 関数名 : HitCheck()
// 機能概要: あたり判定関数(2つの矩形が重なっているかを調べる)
//-----------------------------------------------------------------------------
static bool HitCheck(RECT Rect1, RECT Rect2)
{
・
・
・
}

//-----------------------------------------------------------------------------
// 関数名 : CreateHitRect()
// 機能概要: キャラクタの座標とあたり判定用矩形から、真の矩形を求める
//-----------------------------------------------------------------------------
static void CreateHitRect(RECT *rect, int x, int y, RECT *hitrect)
{
・
・
・
}

//-----------------------------------------------------------------------------
// 関数名 : EnemyFree()
// 機能概要: 敵キャラデータをすべて解放する
//-----------------------------------------------------------------------------
static void EnemyFree(void)
{
    ENEMYSTATUS *prev;

    wEnemy = Enemy_start;
    while (wEnemy != NULL)
    {
        prev = wEnemy->prev;
        free(wEnemy);
        wEnemy = prev;
    }

    Enemy_start = NULL;

}

※作業効率を考え、スタートポインタ、作業用ポインタをグローバル変数として宣言している
※敵キャラ操作関係の各関数(Enemy.cpp内の関数)は変更しなくてもよいように作成した
※敵キャラデータはステージ毎に使うため、使い終わったときの開放処理を追加した
(この処理がないと、ゲームを繰り返すたびにゴミデータがメモリに溜まってしまう)
※メモリ領域生成処理をSetEnemy関数内で行っているが、敵キャラスタートポインタと作業用ポインタを外部変数として宣言すれば、EnemyStargeInit関数内でメモリ領域を生成できるようになる

リスト構造を使うメリットとデメリット

リスト構造を使うと、どのようなメリットとデメリットがあるかを考えてみる。

メリット
・配列の大きさを意識する必要がなくなる
・メモリ領域を効率よく使える
・初期化が楽(ほとんど必要ない)
デメリット
・プログラムが複雑になる(デバッグが大変)

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

14−5で作成したプログラムに上記修正を行い、配列を使わなくても敵キャラを制御できることを確認しなさい。

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

敵キャラをもう1種類増やし、増やした敵キャラが表示されるかを確認しなさい。

第14章6確認問題3(自由問題)

敵キャラデータを格納する配列を削除し、リスト構造でデータを持つようプログラムを改造しなさい。
(LIFO方式のため、最後のデータから読み込まれる)

補足:FIFO方式を使わない理由

データの格納方法と検索方法を見ると、LIFO方式よりもFIFO方式のほうが直感的に分かりやすい(と思う)。では、何故FIFO方式を使わなかったのだろうか?
LIFO方式では前のデータはスタートポインタを見るだけで分かるが、FIFO方式では、スタートポインタから最新のデータまで探してこなければならない。つまり、FIFO方式ではデータ生成時、前データのアドレスを追うのが大変だからである。
実際にFIFO方式でのプログラムを考えてみよう。


[ TOP ]