テーブルで GO!(後編)
(New:1997/12/1)
前編では、「テーブル」というものの基礎や簡単な応用例を書きました。
この後編では、前編の最後に予告した、自機に向かう敵弾の実現方法について書きます。
究極(?)のテーブル化テクニック、レーダー法とはいかに?
- レーダー法は市販のソフトでも使われていたと思われるテクニックですが、それらを解析したわけではないので、推測の部分がかなり含まれています。
ですから、現実の方法と異なる所があれば、ご指摘のメールなど下されば幸いです。
- このページのテキスト版とサンプルプログラムをセットにした圧縮ファイルを用意しました。
ぜひ、お手持ちの MSX でサンプルプログラムをお試し下さい。
ダウンロード
- このページに書かれているサンプルプログラムは、ブラウザの性能上、一部の文字(ひらがな、カタカナ、句読点、アスタリスクなど)が全角文字になっていますが、それらは実際には半角文字であるとして見て下さい。
自機に向かう敵弾の発射までのプロセス
自機に向かう敵弾の発射までのプロセスは以下の順になります。
1. 敵弾の発射角度、つまり自機の方向を求める
2. 1.の結果を元に、敵弾の速度の x、y 成分を求める
2.については、前編「三角関数のテーブル化」でやった通りです。
敵弾の速度の x、y 成分は、発射角度を入力とする sin テーブル、cos テーブルからすぐに求められます。
速度成分が得られたら、あとは敵弾の座標に速度成分を順次足していけば、その敵弾は自機に向かって飛んでいきますね。
問題は1. の自機の方向をどうやって求めるかです。
前編では発射角度はランダムに決めていましたが、今回はきちんと、自機への角度を求める方法を考えます。
というわけで、後編は1.の部分にスポットをあてた話となります。
要は逆正接を求めたいんだけど…
自機の方向を求めるには、逆正接(アークタンジェント)を求める方法が考えられます(下のイメージを見れば、なんとなくでも分かる方もおられると思います)。
自機の座標を px、py、敵の座標を ex、ey、自機の方向(敵弾の発射角度)をθ(「シータ」と読みます)、敵から見た自機の座標(自機と敵の座標の差分)をΔx、Δy とします(「Δ」は「デルタ」と読みます)。
自機の方向θをどう求めるか?
まずΔx、Δy は、自機と敵の座標の差分ですからそれぞれ、
Δx = px - ex
Δy = py - ey
ですね。
続いて、θを逆正接関数 ATN() を用いて表現すると以下のようになります。
Δx > 0 のとき
Δx = 0 のとき
Δy > 0 のとき
Δy < 0 のとき
θ = 3π / 2 [rad] (= 270°)
Δx < 0 のとき
θ= ATN(Δy / Δx) + π [rad]
※逆正接 ATN()とは、「正接 tanθ= A」という式において、A が既知でθが未知の場合に、A からθを求める関数です。
先の式は「θ= ATN(A)」と書き直すことができます。
「ATN()」(アークタンジェント)は本当は違う書き方をするのですが、テキストではうまく表現できないので、BASIC の ATN 関数にひっかけてこう表現することにします。
※π= 円周率(およそ 3.14)
さて、コンピュータ上で逆正接を求めるにはどうすればよいでしょうか。
BASIC や C 言語を用いるなら、逆正接関数(BASIC なら ATN())を用いて求めることも出来ます。
しかし、これらの逆正接関数もまた、他の三角関数と同様、計算が遅いのです。
また、Δx の値によって場合分けが必要になります。
機械語なら、まともな逆正接関数を作ろうとすると大変なことになるでしょう。
Math-Pack を用いるのも大変だし、処理速度もやっぱり遅くなってしまうでしょう。
※ DOS 用プログラムで Math-Pack を用いるには注意が必要です。
詳しくは「MSX テクニカルガイドブック」(アスキャット)あたりを見るのがいいでしょう。
もっとも、以後の説明では Math-Pack を使う必要はありませんが。
何とか、逆正接関数を使わずに逆正接(自機の方向)を求める方法はないでしょうか。
そこで登場するのがレーダー法なのです。
レーダー法の基礎
レーダー法とは、荒い2次元座標(碁盤目のようなもの)を用いて、自機の方向を求める方法です。
その2次元座標の各場所にはあらかじめ、中心から見た方向を記しておきます。
敵から見た自機の方向を求めたいとき、その2次元座標の中心を敵に合わせます。
すると、自機の方向は、その2次元座標の、自機のある所に記された方向を見るだけで分かるというわけです。
下のイメージを見て、イメージ(ややこしい(^^;))を膨らませて下さい。
レーダー法:
自機のところに記された方向
(イメージでは黄緑の矢印)を見れば、
敵から見た自機の方向が分かる。
BASIC によるレーダー法の実現
それでは、レーダー法を BASIC で実現する方法を考えてみましょう。
まず、荒い2次元座標として、適当な大きさの2次元配列を定義します。
ここでまず、1つのマスの大きさ(座標の荒さ)をどれくらいにするかが問題になります。
マスが小さいほど得られる方向は正確ですが、その分メモリを大量に使用してしまいます(マスの「数」が増えるため)。
サンプルプログラム4、5では、とりあえず1マス16×16ドットとしました。
発射方向が全部で32方向以下なら、これくらいの荒さでもどうにかなるでしょう。
画面モードを SCREEN 1〜3 とすると、画面の大きさは256×192ドットですから、配列の大きさは31×23で行けそうですね…え、数が多すぎる?
256(192)を16で割ったら、配列の大きさは16×12じゃないかって?
…自機が敵の上、あるいは左に来ることも考えれば、およそ4画面分の大きさが必要になるということです、はい。
これについては改良案を1つ、あとに挙げておきます。
ここでは、この配列を RT(x,y) と定義することにしましょう(x は0〜30の整数、y は0〜22の整数)。
続いて、RT(x,y)に、中心から見た各マスの方向を書き込んでいきます。
具体的には、逆正接関数を利用し、前編で書いたような sin テーブル、cos テーブルなどと併用するために、360°を一定値で畳んだものを書き込みます。
全方向数を32とすると、書き込まれる値は0〜31の整数となりますね(0:、…、8:、…、16:、…、24:、…)。
※ 実際の書き込み作業はサンプルプログラム4を参照して下さい。
これで2次元座標系 RT(x,y) の準備は出来ました。
あとはメインループ中で必要なとき(自機に向かう敵弾を発射する時など)に、この RT(x,y) を参照すればオーケイです。
敵から見た自機の方向はすなわち、
RT(((Δx + 8)/16) + 15, ((Δy +8)/16) + 11)
です。
ここで、15(11) を足しているのは、敵の位置をテーブルの中央すなわち RT(15,11) に合わせるためです。
Δx、Δy に8を足しているのは、敵をマス(大きさ16×16ドット)の中央に合わせるためです。
16で割っているのは、現実の座標(ドット)から、荒い2次元座標に対応させるためです(このため、1マスの大きさが小さいほど、精度が高くなる)。
※除算が整数除算(¥)でないのは、マイナスを含めた計算に対応させるためです(「整数除算の罠」参照)。
逆正接関数を用いて求める場合と比べると、レーダー法は、本来逆正接関数を用いる部分が配列(RT)を参照するという形になっています。
ここで、逆正接関数の処理にかかる時間より、配列の参照にかかる時間の方が短いです。
したがって後者、つまりレーダー法を用いた方が速いことが分かります。
Δx、Δy を入力とし、あらかじめ計算された角度を瞬時に返す…レーダー法は逆正接関数のテーブル化と言えなくもありませんね(この場合、配列 RT が逆正接テーブルになります)。
というわけで、以後は配列 RT のことを、逆正接テーブルと呼ぶことにします。
実際、配列 RT にあらかじめ入れておく角度は、逆正接関数で求められます。
さて、サンプルプログラム4です。
レーダー法を利用した敵弾発射プログラムです。
今まで書いたことを思い出しながら見てやって下さい。
サンプルプログラム4と解説
提案:テーブルをコンパクトにできないか
先の解説では、逆正接テーブルの大きさは、自機が敵の上または左に来ることも考えて、およそ4画面分必要であると言いました。
結果、逆正接テーブルの大きさは 31×23 = 713 となりました(1マスを16×16ドットとした)。
これって、結構大き過ぎると思いませんか?
バイト単位で考えれば、C 言語や機械語なら、配列(テーブル)の1マスは1バイトとして扱うのが自然でしょうから、そのまま 713×1 = 713 バイトになります(実際には掛算を簡単、高速にするために、もう少し大きくなるかも知れません)。
BASIC に至っては、1マスは少なくとも2バイトなので(配列を整数型として)、テーブルの大きさは 713 × 2 = 1426 バイトにもなります。
精度を上げるために、1マスの大きさを16×16ドットから8×8ドットに変えたら、さらに4倍のマス、容量が必要となってしまいます(BASIC なら 1426×4 = 5704バイトも!)。
何とかテーブルを小さくすることはできないでしょうか。
そこで、若干の計算量の増加を許して、テーブルをコンパクトにする方法を考えてみました。
それは、若干の計算、条件分岐を付加し、逆正接テーブルとしてはΔx、Δy がともに正のときの分(つまりはじめの90°まで)のみ用意するというものです。
三角関数の公式を思い出すと、わずかな加減算(と論理演算)を用いることにより、はじめの90°分のみの逆正接から、それ以上の角度の逆正接も求められることが分かるでしょう。
具体的には、以下の手順で逆正接(自機の方向)を求めます。
- Δx、Δy を求める。ここまでは今までと一緒
- Δx、Δy の「絶対値」を用いて、コンパクト化した逆正接テーブルから角度θ2(0°〜90°)を求める。
- Δx、Δy の正負、およびθ2から、真の逆正接θを求める。すなわち、
Δx > 0かつ Δy > 0のとき
Δx < 0かつ Δy > 0のとき
Δx < 0かつ Δy < 0のとき
Δx > 0かつ Δy < 0のとき
注:ここでは、θもθ2 も、0以上 2π未満という前提で書いていますが、実戦ではθがこの範囲を超えないよう、論理積(AND)などを併用した方がいいかも知れません。詳しくはサンプルプログラム5を参照。
この方法なら、逆正接テーブルの大きさは今までの約4分の1で済みますね(はじめの90°分、つまりΔx、Δy がともに0以上の場合だけだから)。
逆正接テーブルのコンパクト化:
逆正接テーブルははじめの90°分だけ用意。
テーブル外(灰色の部分)に自機がある場合は
簡単な計算(条件分岐)を併用。
サンプルプログラム5と解説
これで逆正接テーブル(配列)の縮小も実現可能なわけですが、しかし、若干ではありますが、今までのものより計算、プログラムが増えてしまいます。
C 言語やアセンブリ言語なら、このくらいの計算量の増加は問題ないと思うのですが(少なくとも Math-Pack を使うよりははるかに速いでしょう)、BASIC では深刻かもしれません。
しかも、BASIC の配列は整数型でも1つにつき2バイトの容量を使うので、配列のまだ半分以上が無駄なスペースになってしまっています(257方向以上にするのなら話は別ですが、通常そこまで細かくする必要があるのかどうか…)。
1つの配列に2つ分の方向データを入れるという手もありますが、そうするとまた計算が増えてしまう…。
…ここまで作っておいて何ですが、レーダー法ってやっぱり、BASIC 向きのテクニックじゃないのかもしれませんね(自爆)。
ま、少なくとも C 言語、アセンブリ言語などでは、レーダー法は有効なテクニックだと思います。
レーダー法の応用
レーダー法は逆正接関数のテーブル化とも言えるものであって、別に敵弾の発射方向を求めるだけのものではないと思います。
例えば、逆に自機から敵に向かう弾(あれば)の角度を求めるのにも使えるでしょう。
また、誘導弾等への応用も考えられるでしょう。
このほかにも、いろいろ応用は考えられると思います。
逆正接テーブルの1マスの大きさは?
最後に、逆正接テーブルの1マスの大きさについて、もう少し考えてみましょう。
当然ながら、マスの大きさは小さいほど、精度は上がります。
また、全方向数が多いほど、精度は高くなければなりません。
前述の通り、今回のサンプルでは、全32方向という前提で、1マスの大きさは16×16ドットとしました。
しかし、昨今のシューティングゲームでは、全発射方向が32を超えるものも少なくないようです。
某怒○○蜂とか某バ○ル○レッ○なんて、256方向でもあるんじゃないかと思うくらいです。
このように発射方向を増やすためには、1マスの大きさをかなり小さくしなければ意味がありません。
256方向なら、1マスは4×4ドットくらいでなければならないのではないでしょうか。
まとめ
- 敵から見た自機の方向(敵弾の発射方向)は、逆正接関数を用いて求められる。
- 逆正接関数は計算量が多いので、かわりにレーダー法を用いる。
- レーダー法とはつまるところ、逆正接関数のテーブル化である。
- 逆正接テーブルはコンパクトに出来る?
- レーダー法は誘導弾など他のことへも応用できる。
- 逆正接テーブルの1マスの大きさおよびマスの数は、全方向数やメモリなどを考慮して決める。
おまけ:サンプルプログラム6
今までやってきたことを総括して、某シューティングゲームの1シーンを再現してみました。
再現といっても、まぁ雰囲気程度のものですが。
サンプルプログラム6と解説
このサンプルプログラムについては、あまり詳しく解説しません。
変数表だけつけておきますので、興味ある人は各自解析してみましょう(無責任)。
MSXの適当手帳
MSX駅
西山駅