もう一度前稿で示した動画を見てもらいたい。
プレイヤーが画面奥へ向かって射撃している……そういう風に見えるよな?……のだが、本稿はこれをどうやっているか、という話を、自身の失念防止のために書き留める趣向である。
本作では照準を動かすと逆方向にすべてのオブジェクトが流れる……つまり、実装はどうあれ、すべてのキャラクタは自機、正しくは照準器座標に対する相対位置に表示されているということだが……のでわかりにくいかもしれないが、照準を動かさないで射撃すると、照準器の中央に正しく着弾していることが動画からわかると思う。
また、よく見ると弾の飛翔速度は一定ではなく、射撃直後は速く、照準着弾に向けて遅くなっている。これは奥行きを表現するためにこうしているのだが……余談だが、
前作Shoulder Blade OVERDRIVEは勢い優先の弾幕張りゲームであることを念頭において、敢えてそのような実装をおこなっていない……移動量が一定でないのに目的座標に正しく到達する演算は、厳密に言えば微分を要することになるが、もちろんそんなことを真面目にリアルタイムでおこなうパワーはZ80にはない……まぁ、それもやり方次第だが、Z80がゲーム実現のために1フレーム時間中こなさねばならない仕事はそれだけではなく、むしろここで触れているのはそのほんの一部に過ぎない……ので、ある種の誤魔化しでもってまるでそうしているかのように錯覚する状況を作っているのである。
先に結論を書いておこう。
(1) 弾は発射後、7フレーム時間(約0.5秒弱)で照準に到達する。
(2) 自機と照準の座標差=弾の飛翔距離を16分割(符号付右シフト4回)する。
(3) (2)の距離を1単位として、着弾まで1フレーム毎に 4、4、3、2、1、1、1単位ずつ弾は移動する。
(4) (2)の演算時の余り=シフト後の下位4ビットを(4)の単位毎に分散加算して、誤差を補正する。
端的に言えばやっていることは以上になる。
これでわかる人にはこれ以上の説明は不要で、以下は読むに及ばない。が、原理まで言及しておかないと、他ならぬボク自身が後日「コレって何でこんなことしてたんだっけ?」と首を傾げるのは必定なので、ただただそれを避けるべく下記の通り備忘する。
話を単純化するために、縦横軸の一方だけについて考えることにする。一方の原理がわかれば、もう一方に対しては独立して同じことをすればよいだけなので。
具体的な数値があった方がわかりやすいので、今仮に自機の座標が30、相対する照準の座標が100だとする。
この時点で上表のような値が得られる。続いて(3)の処理、すなわち、徐々に減速しつつ照準に近づいていく様を表現すべく、フレーム毎に異なる回数16分割した差を加算してみよう。
黄色で示した部分が、実際にゲーム画面にそれぞれのフレームにおいて反映される弾の座標になる。ここだけ注目すると、下方へ向かうに従って増加の勢いが下がっており、これが見た目上の弾の減速(奥行き表現)になっている。
が、見てわかるように、最終的な到達値 94 は、本来の目標値 100 に対して不足していて、もちろんこれは最初の演算で求めた「余り 6」がこれに対応している。うまいことこの 6 を分散補正してやりたい。
そこでこうする。
ポイントはピンク色に着色した「補正判定値」である。
16 で割って余りが 6 ある、というのは、言い換えれば、この商を 16 回加算する(上表)うちに、6 回だけ余計に 1 加えてやれば、元の割られた値に等しくなる、ということである。問題は、この 6 回を16回中のどこに配するか、になるが、これを判断するために補正判定値を用いる。
この値は、0 から始まってこの表の行(単位演算)毎に 7 を加えて 4 ビット(0〜15)でマスクしている値で、原理は
こちらの拙稿を参照いただくとして、一見ランダムだが 16 回演算すると正しく 0〜15 の値が重複も漏れもなくすべて登場する、そういう値になる。
で、この値を最初に求めた余り 6 と比較して、補正判定値がこの余りよりも小さいときだけ 1 を補正することにする。0〜15 で循環する系内に 6 より小さい値は 0〜5 の 6 つだから、補正値の合計は当然のことながら 6 になる。そして、この補正は補正判定値の一見ランダムな特性により、16 回の演算中にきれいに分散される。結果、見た目の挙動には(少なくとも人間の肉眼で見る限りでは)偏りのない、自然な移動として見えるに至る。
原理は以上だ。
そして、最も重要なのは、以上の演算はすべて加減算とビットシフト/マスクのみで実現されているので、Z80のような非力な8ビットCPUでも簡単に実装できる、という点である。
以下、さらなる余話2つ。
気になる人は気になるかも知れない「自機と照準の座標の大小が逆=加算値がマイナスの場合」がどうなるか、について。これについては、特別な考慮は必要がない。8ビット演算の範囲に収める限りにおいて、マイナスの加算値は10進数で 128 以上に対応する=加算すると桁溢れして和が結果的に元の値よりも小さくなる値に過ぎないのであり、まったく同じ処理系で遇することができる。
もう1つ。この方法はシフト演算を前提としているので、最初におこなう単位分割が 2
n でさえあれば(無論、8ビット演算に納まる範囲において、という大前提があるが)ここで例示したパラメータ以外でも適用できる。
また、任意の値による割り算サブルーチンを自前で用意すれば、2
n 以外でも実装はできる。が、この場合、割り算毎に要する処理時間が異なるという特性を必然的に抱え込んでしまうため、リアルタイムゲームへの組み込みについては注意を要する。具体的には、割り算に要する演算回数がすべての対象オブジェクトについて最大となるケースでも、全演算が1フレーム内に収まることを別途評価する必要が生じる。
そういうテストをある瞬間におこなうこと自体はさほど手間ではないが、開発が進んで全体の処理密度が上がっていく毎のこの動作保証をおこない続けるのはとても面倒で、結果的に潜在バグの原因になるのは必定である。シフト演算で分割している分には処理時間はいかなる値に対しても一定なので、よほど特殊なニーズがない限りは、上位の設計を 2
n 分割に合わせた方が合理的である。