テーブルで GO!(後編)

(New:1997/12/1)
前編では、「テーブル」というものの基礎や簡単な応用例を書きました。 この後編では、前編の最後に予告した、
自機に向かう敵弾の実現方法
について書きます。 究極(?)のテーブル化テクニック、レーダー法とはいかに?

自機に向かう敵弾の発射までのプロセス

自機に向かう敵弾の発射までのプロセスは以下の順になります。 2.については、前編「三角関数のテーブル化」でやった通りです。 敵弾の速度の x、y 成分は、発射角度を入力とする sin テーブル、cos テーブルからすぐに求められます。 速度成分が得られたら、あとは敵弾の座標に速度成分を順次足していけば、その敵弾は自機に向かって飛んでいきますね。
問題は1. の
自機の方向をどうやって求めるか
です。 前編では発射角度はランダムに決めていましたが、今回はきちんと、自機への角度を求める方法を考えます。
というわけで、後編は1.の部分にスポットをあてた話となります。

要は逆正接を求めたいんだけど…

自機の方向を求めるには、逆正接(アークタンジェント)を求める方法が考えられます(下のイメージを見れば、なんとなくでも分かる方もおられると思います)。
自機の座標を px、py、敵の座標を ex、ey、自機の方向(敵弾の発射角度)をθ(「シータ」と読みます)、敵から見た自機の座標(自機と敵の座標の差分)をΔx、Δy とします(「Δ」は「デルタ」と読みます)。


自機の方向θをどう求めるか?

まずΔx、Δy は、自機と敵の座標の差分ですからそれぞれ、

ですね。
続いて、θを逆正接関数 ATN() を用いて表現すると以下のようになります。

さて、コンピュータ上で逆正接を求めるにはどうすればよいでしょうか。 BASIC や C 言語を用いるなら、逆正接関数(BASIC なら ATN())を用いて求めることも出来ます。 しかし、これらの逆正接関数もまた、他の三角関数と同様、計算が遅いのです。 また、Δx の値によって場合分けが必要になります。 機械語なら、まともな逆正接関数を作ろうとすると大変なことになるでしょう。 Math-Pack を用いるのも大変だし、処理速度もやっぱり遅くなってしまうでしょう。

何とか、逆正接関数を使わずに逆正接(自機の方向)を求める方法はないでしょうか。
そこで登場するのがレーダー法なのです。

レーダー法の基礎

レーダー法とは、荒い2次元座標(碁盤目のようなもの)を用いて、自機の方向を求める方法です。
その2次元座標の各場所にはあらかじめ、中心から見た方向を記しておきます。
敵から見た自機の方向を求めたいとき、その2次元座標の中心を敵に合わせます。 すると、自機の方向は、その2次元座標の、自機のある所に記された方向を見るだけで分かるというわけです。
下のイメージを見て、イメージ(ややこしい(^^;))を膨らませて下さい。

レーダー法解説用イメージ1
レーダー法:
自機のところに記された方向
(イメージでは黄緑の矢印)を見れば、
敵から見た自機の方向が分かる。

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:↑、…)。

これで2次元座標系 RT(x,y) の準備は出来ました。 あとはメインループ中で必要なとき(自機に向かう敵弾を発射する時など)に、この RT(x,y) を参照すればオーケイです。 敵から見た自機の方向はすなわち、

です。 ここで、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°分のみの逆正接から、それ以上の角度の逆正接も求められることが分かるでしょう。
具体的には、以下の手順で逆正接(自機の方向)を求めます。

  1. Δx、Δy を求める。ここまでは今までと一緒
  2. Δx、Δy の「絶対値」を用いて、コンパクト化した逆正接テーブルから角度θ2(0°〜90°)を求める。
  3. Δx、Δy の正負、およびθ2から、真の逆正接θを求める。すなわち、
この方法なら、逆正接テーブルの大きさは今までの約4分の1で済みますね(はじめの90°分、つまりΔx、Δy がともに0以上の場合だけだから)。

レーダー法解説用イメージ2→レーダー法解説用イメージ3
逆正接テーブルのコンパクト化:
逆正接テーブルははじめの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ドットくらいでなければならないのではないでしょうか。

まとめ

おまけ:サンプルプログラム6

今までやってきたことを総括して、某シューティングゲームの1シーンを再現してみました。 再現といっても、まぁ雰囲気程度のものですが。
サンプルプログラム6と解説
このサンプルプログラムについては、あまり詳しく解説しません。 変数表だけつけておきますので、興味ある人は各自解析してみましょう(無責任)。
MSXの適当手帳 MSX駅 西山駅