« 弾日記 - 速度調整 | メイン | 弾とか »

2005年03月07日

割と真剣なメモ

Sample

main.c

#include "dfpfx.h"

struct TESTS {
	int num;
	void (*func2)(int);
};

int main(void)
{
	struct TESTS test;
	
	test.num = 10;
	test.func2 = func();
	
	test.func2(test.num);

	return 0;
}

func.c

#include "dfpfx.h"

void funcc(int num);

void (*func(void))(int num)
{
	return funcc;
}

void funcc(int num)
{
	printf("%d",num);
}

dfpfx.h

#include <stdio.h>

void (*func(void))(int num);

このコードについて

関数へのポインタを返す関数の作り方。
注目すべきは、funcc関数についての宣言はmain関数からは見えない事。
現在私が採用している方法では、全部の関数の宣言をヘッダファイルに書き込む必要があり、拡張作業が煩雑な原因の一つとなっています。
そこで、一つ前の日記に書いたコード+αを利用してよりスマートになる方法を思いついたので忘れないうちにメモっておきます。
時間をあけて見直すと改良点が浮かび上がってくると思うので、そのためにも。

現状分析

現在、弾関連の処理は以下のようになっています。


BJL.cとBullet.cでは共通のヘッダファイルBulletData.hをインクルードし、そこに弾別ファイル内関数を全て記述。
BulletDeleteやBulletMoveではBUL構造体配列からフラグ記述用メンバのfが1(存在)になっている要素を探し、
その番号のBul[i].fを0にし、BJL_Delete(Bul[i].num)を呼び出してBulD[Bul[i].num].fも0にします。
しかし、ここで問題になるのはBul[x].bnumで番号を管理し、その番号により呼び出すHOGE_Delete関数を変えなければいけなくなるのです。
よって、新しい弾パターンを追加するたびに

switch(Bul[i].bnum){
   case 1: BJL_Delete(Bul[i].num);
              break;
   case 2: HOGE_Delte(Bul[i].num);
              break;
}

といった感じで追加しなければなりません。
これが削除だけならまあ、まだそこまで面倒でもないんですが、
Moveでも、更にはここでは取り上げていませんがEnemy関数におけるDeleteやMoveでも(Enemy情報に対して)同じ事をしなければならないのです。
これでは手間は増えるし間違いも多くなるし、何よりスマートではありません。

改善案

そこで、BUL構造体に関数へのポインタを格納するメンバを追加します。

struct BUL {
	BOOL	f;		//フラグ
	int		num;	//弾タイプ内の管理番号(配列の添え字)
	float	x;		//x座標
	float	y;		//y座標
	void (*Draw)(void);
	void (*Delete)(int);
	void (*Move)(int);
}Bul[MAX];

後はBJL_Create内でBul[i].DeleteにBJL_Delete(int num)へのポインタを格納し、
Bullet.c内のBulletDelete関数をこのように変更します

BulletDelete(int num)
{
Bul[num].f = 0;

- switch(Bul[num].type){
-	case BUAT_BJL:
-		BJL_Delete(Bul[num].num);
-		break;

(略)

+ Bul[num].Delete(Bul[num]);
}

これと同じ事をDrawやMoveでもやればOK。
こうしておけば、他の弾種を追加する時もそのファイル以外を触る必要がありません。

問題点

しかし、この方法はこのままでは致命的な欠点があります。
それは、複数のファイル間にまたがるグローバル変数としてのBUL構造体を前提にしている事です。
このままでは、せっかく片方からの依存関係を整理したのに、もう片方からは強烈な依存関係が残ってしまいます。
そこで、原則として全ての変数を1つのファイル内だけでしか使わないように改善するために以下の方法を考えます。
もっとも、これは上記のような似非オブジェクト指向を全てのファイルで徹底し、
更に拡張性と保守性を高めたいと考えているmSTG2(仮)で一気に実装したいので、今回のプログラムには適用しません。

現状分析

今度はBJLファイルに着目します。
BJL_Createでは、おおまかに書くと

1. Bul[i].fを調べ、空きをチェック。
  空いている所を発見すると引数の開始座標やらスピードやらを入れる。
2. BulD[j].fを調べ、空いている所を発見したら自機狙い弾固有の情報を入れる。
  更に、BulとBuldを関連付けるためにBul[i].numにjの値を入れる。

という事をやっています。
しかし、BulはBullet.c内で管理している変数であり、BJL.c内では初期入力以外ではほとんど呼び出しません。
(この「ほとんど」、つまり少しは呼び出している点が問題なんですが、これは解決できます)
更に、1の作業は他の弾定義ファイルでも全く同じの共通部分です。
ではなぜ、Bulletファイル内で前処理をしてからBJL_Create関数を呼び出す事により共通化をはからないのかと言いますと、
例えばBulletCreate関数とBJL_Create関数を使って以下のように処理するとします。

//開始x座標,開始y座標,当たり判定半径,威力,速度
BulletCreate(float x,float y,float r,int pow,float spd,float gx,float gy)
{
	BJL_Create(gx,gy);
}
//目標x座標,目標y座標(この関数は本当は単なる直線移動弾発生関数で、呼び出しにその時点での自機座標を使う事で自機狙いとしている)
BJL_Create(float gx,float gy)
{
	弾作成;
}

しかし、このままでは拡張できません。
では、BulletCreateに"int bnum"引数を追加し、switch/caseをすればいいかというと、それは困ります。
なぜなら、それでは弾種を追加する度に新たにこの部分に処理を追加しなければならなくなるからです。
更に、このままでは別の弾種が更なる情報、引数を欲していた場合に対応できません。
よって、この方法をこのまま使う事はできません。

解決案

可変個の引数を取る事のできる関数と、関数へのポインタを併用します。
上に、ヘッダファイルへの記入をできるだけ減らしたいと書きましたが、そのままでは外部から呼び出しができませんから、
HOGE_Create関数だけはヘッダファイルに記入し、Bullet.cから呼び出せるようにします。
そして、BulletCreateとBJL_Create関数の引数を以下のように変更します。

補足:
可変引数関数は引数を(int num, ...)と記述し、numの部分に自身を含めた引数の数を入れます。
後は関数内で引数を呼び出す処理を行えば全ての引数が抽出できます。
ただし、stdarg.hをインクルードする事。

BulletCreate(int num, ...);
BJL_Create(int num, ...);

これで準備はOKです。
では、個別の処理を考えてみましょう。

BulletCreate(8,x,y,r,pow,spd,gx,gy)

#define UNI_MAX 共通処理に使う引数の数
BulletCreate(int num, ...)
{
	int x = num - UNI_MAX;
	float input[num];	//情報が失われる事を防ぐために一番幅を取る型で宣言する
	float output[UNI_MAX-x];
	
	input[i]に対する抽出処理;
	
	Bul[i].hogeへの入力処理;
	
	for(i=0;i<num-UNI_MAX;i++){
		output[i] = input[UNI_MAX+i];
	}
	
	BJL_Create(x,output);
}

BJL_Create(int num,float input[])
{
	抽出処理;
	
	BulD[i].hogeへの入力処理;
}

これで、汎用的な値渡し処理が可能になったと思います。
実際には引数は全て配列を使って処理します。
添え字の最大数と共通処理に使う添え字の数はわかっている事ですし、その部分に不安はありません。
また、渡す値の数がいくつになるかわからないのでBJL_Createには配列を渡します。

しかし、これでもまだ足りません。
なぜなら、これではどのHOGE_Create関数を呼び出せばいいかわからないからです。
ですが、管理番号を使う事は止めたいのは今まで書いた通り。
ここで、関数へのポインタを使います。

呼び出し例
BulletCreate(9,BJL_Create,x,y,r,pow,spd,gx,gy)

引き出し例
void (*func_bcreate)(int, ...);

//嘘が入ってます。ここら辺は実験しないと何ともいえませんがどうやってもできないって事は無いはず
func_bcreate = va_arg(ptr, void*);
input[1] = va_arg(ptr, float);

(一つ前のコード例に書いた処理)

func_bcreate(x,output);

これで、管理番号を使わずに弾をセットできるようになりました。
しかしこれだけではまだ不十分です。

問題点

今回のテーマであった関数へのポインタを使ったmoveやdelete、draw処理に関する記述の一括化は、
BUL構造体がグローバルな事を利用していましたので、ここでは使えません。

解決案

ここではポインタを渡して向こうでその内容を書き換えてもらう事にします。
すなわち、bcreate関数を呼び出す際にBul.deleteやdraw,moveのポインタを渡しておいて、
そこにHOGE_Create内でHOGE.c内においてのdeleteやdraw,moveのポインタを書き込めばいいのです。

moveに関しては、Bul[i].xやBul[i].yの値をHOGE.c内の処理をもとに書き換えなければなりません。
ですので、Bullet.c内のmove関数からHOGE.c内のmove関数を呼び出す時にも、
Bul[i].xやBul[i].yのアドレスを渡して向こうで書き換える事を忘れてはいけません。

もっとも、ポインタを渡しての直接書き換えはグローバル変数を使う事よりはマシな物の、
あまり好ましい事では無いようにも思います。
ただ、現段階ではこれが精一杯の案です。

総括

以上の方法をBulletとEnemyのコードに適用する事で、弾及び敵パターンの拡張性は広がる事と思います。
BULD構造体はローカルである為、共通部分を他のファイルからコピー&ペーストし、
必要な部分(主にCreateとMove)のみ編集し、関数名を置換した後にCreate関数のみヘッダファイルにて宣言すれば良いわけですから。
使いまわしの処理、基本的に編集しないであろうDeleteやら初期化やらは継承できてしまえば楽なんですけどね……。
いや、オブジェクト指向プログラミングをやった事が無いので本当に楽かはわかりませんが。

半分は後の自分へ残したい遺産として書いたので読みやすいかはわかりません、むしろ関数名やらがごっちゃになって読みにくいと思いますが、
これが現段階での拡張性に対する精一杯の考えです。

後は、ステージ構成データ、スクリプトの問題が大きなハードルとして残っています。
また、現段階では背景について何も考えていないので、これも考えなくてはなりません。
これは、mSTGで1ステージ作成し、BOSSも実装した上で背景も実装してみた上で考えてみたいと思います。
もっとも、気まぐれなので先にそちらを実装してしまうかもしれませんが。

蛇足:
ステージスクリプトに関しては、敵の動きと弾の動きをまとめた関数を別ファイルに作り、
フレームに応じてそれを呼び出す形はどうかなと考えています。
もちろんハードコードになるのでスクリプトよりは柔軟性が低いですが、
高度な処理を行えて、思い通りのパターンが組みやすいのではないかと思います。
より良いスクリプトリーダーを実装して細かい制御をできるようにできるなら、もちろんそちらの方が断然良いわけですが現段階ではスクリプト解析については全く想像もできません。
いや、資料はあるんですが……。
ただ、これに関しては後で関数を取り替えれば済む話だと思っているのであまり危惧してはいません。
mSTG2が完成してから考えて、どうにも関数の取替えだけでもすまないと思ったら……mSTG3ですか?
ちょうどC++を学習したいと思う頃に重なると思うので、それも良いですね。

投稿者 miff : 2005年03月07日 06:11

コメント

眠れなかったので布団の中で考えていた内容をメモっておきました。
これを書くだけで二時間くらいかかっていたり……(汗

投稿者 miff : 2005年03月07日 06:26



XREAAD