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

14-5 キャラ別移動処理に関数ポインタを使う

1. ポインタとは?(復習)

ポインタは、変数のアドレスを格納するための変数であることはC言語で学んだ。

#include <stdio.h>

void main(void)
{
    int a;  /* int型変数aを宣言 */
    int *pa;  /* int型変数のアドレスを格納する変数paを宣言 */

    pa = &a;  /* 変数aのアドレスをpaに代入 */
    a = 256;  /* 整数256を変数aに代入 */

    printf("a = %d, *pa = %d, pa = %x\n", a, *pa, pa);

}

《実行結果》

a = 256, *pa = 256, pa = ????

※ポインタ変数paには、変数aのアドレスが入っている
※*paとすると、アドレスが示す先に入っているデータという意味になる

2. 関数ポインタとは?

 プログラム実行時にメモリ上に配置されるのは、プログラム中に宣言した変数だけではない。プログラム自身も読み込まれ、メモリ上に配置される。
 関数ポインタとは、メモリ上に配置された関数の先頭アドレスを格納する変数のことである。

《サンプルプログラム》

#include <stdio.h>

void func(void);

void main(void)
{
    void (*pfunc)(void);  /* 1 */
    pfunc = func;  /* 2 */

    func();  /* 3 */
    pfunc();    /* 4 */

}

void func(void)
{
    puts("関数からPUT");
}

《解説》

  1. 引数がなく(void)、戻り値もない関数のアドレスを格納する関数ポインタ「pfunc」を宣言
  2. 関数ポインタ「pfunc」に、func関数の先頭アドレスをセット
  3. func関数を実行(従来のやり方)
  4. 関数ポインタ「pfunc」に格納されているアドレスから始まる関数を実行

《実行結果》

関数からPUT
関数からPUT

※結果的に、同じ関数が2度実行される

3. 現在の敵キャラ移動処理について

敵キャラの移動処理は、キャラのタイプによって実行する関数を変えることにより実現している。

  1. 敵キャラ情報配列を先頭から調べ、ライフがDEADでないものを見つけたらEnemyMove関数を実行する
  2. 与えられた敵キャラデータのタイプを調べ、移動処理用関数を振り分ける

今までのプログラムの場合、2の処理は次の関数により行っている。

//-----------------------------------------------------------------------------
// 関数名 : EnemyMove()
// 機能概要: 敵キャラ移動処理
//-----------------------------------------------------------------------------
void EnemyMove(
    ENEMYSTATUS *status
)
{
    switch (status->type)
    {
        case 0:
            EnemyMove0(status);
            break;
        case 1:
            EnemyMove1(status);
            break;
        case 2:
            EnemyMove2(status);
            break;
        case 3:
            EnemyMove3(status);
            break;
        case 4:
            EnemyMove4(status);
            break;
        default:
            break;
    }

}

敵キャラの種類を増やすたびにこの関数をメンテナンスしなければならない(種類が増えれば移動処理用関数が増える)が、関数ポインタをうまく利用すれば、この処理自身が要らなくなる

4. 関数ポインタを用いた移動処理

まず、敵キャラ移動用関数のアドレスを格納する関数ポインタを宣言する。敵キャラ用関数はEnemy.cppにまとめてあるので、そのソース内で宣言するのがいいだろう。

//=============================================================================
//	敵キャラ処理関係の自作関数群
//=============================================================================
#include "common.h"

//-----------------------------------------------------------------------------
// プロトタイプ宣言(ソース内でしか使わないもの)
//-----------------------------------------------------------------------------
static void EnemyInit(ENEMYSTATUS *, int, int, int, int, int);
static void EnemyMove0(ENEMYSTATUS *);
static void EnemyMove1(ENEMYSTATUS *);
static void EnemyMove2(ENEMYSTATUS *);
static void EnemyMove3(ENEMYSTATUS *);
static void EnemyMove4(ENEMYSTATUS *);

//-----------------------------------------------------------------------------
// グローバル変数
//-----------------------------------------------------------------------------
static void (*MoveFunc[ENEMYTYPE_MAX])(ENEMYSTATUS *) = {
    EnemyMove0, EnemyMove1, EnemyMove2, EnemyMove3, EnemyMove4
};	// 移動処理関数のアドレスを持つ関数ポインタ変数を宣言
・
・
・

※敵キャラ移動処理用関数は複数あるため、関数ポインタはENEMYTYPE_MAX分の数を持つ配列として宣言
※戻り値はない為、void型で宣言
※引数はキャラクタ情報構造体のポインタ
※関数ポインタ配列の0番目にはEnemyMove0関数のアドレスが、1番目にはEnemyMove1関数のアドレスが格納される

次に、敵キャラステータス構造体を次のように修正する。

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

※引数が敵キャラステータス構造体のアドレスで、戻り値がない関数ポインタ「Move」を宣言

次に、Enemy.cppの敵キャラ発生処理初期化処理を次のように修正する。

//-----------------------------------------------------------------------------
// 関数名 : EnemyStatusInit()
// 機能概要: 敵キャラステータス情報初期化
//-----------------------------------------------------------------------------
void EnemyStatusInit(
    ENEMYSTATUS *status,
    int type  // 敵キャラのタイプ
)
{
    int x;

    status->type = type;	/* タイプをセット */
    status->Move = MoveFunc[type];	/* タイプ別の移動処理関数ポインタをセット */

    switch (type)
    {
・
・
・

※typeの値により、呼び出す関数ポインタを変える
(MoveFunc関数ポインタはEnemy.cppでグローバル変数として宣言済み)

次に、Game.cppのキャラクタ移動処理を次のように修正する。

//-----------------------------------------------------------------------------
// 関数名 : CharMove()
// 機能概要: キャラクタ移動処理
//-----------------------------------------------------------------------------
static void CharMove(void)
{
    int i;

    /* 自キャラの移動 */
    ・
    ・
    ・

    /* 敵キャラの移動 */
    for (i=0 ; i<ENEMY_MAX ; i++)
    {
        if (Enemy[i].life > DEAD)
            Enemy[i].Move(&Enemy[i]);
    }

}

※敵キャラ情報のメンバであるMove関数ポインタのアドレスから始まる関数に、敵キャラ情報のアドレスを引数として渡して実行している
※EnemyMove関数を使っていない

関数ポインタを使うメリット

関数ポインタを使うと、どのようなメリットがあるかを考えてみる。

  1. EnemyMove関数のように、種類によって呼び出す関数を振り分ける処理がいらなくなる
  2. 敵キャラ移動関数の作成を、人にお願いできる
  3. ...etc

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

14−4で作成したプログラムに上記修正を行い、EnemyMove関数を使うことなく敵キャラがタイプ別に移動するかどうかを確認しなさい。ただし、利用しなくなったEnemyMove関数をEnemy.cppから削除すること。

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

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


[ TOP ]