三角関数を用いると、斜めベクトルを水平垂直のX成分とY成分に分解できることが分かりました。C# 等ではそのまま float と書けば小数を扱えるので簡単ですが、Z80 のような8ビット CPUでは、どのように扱えば良いのでしょうか。今回は小数を最も簡単に扱う固定小数と三角関数のお話です。


  • 固定小数の原理
2進数と16進数でもお話ししましたが、大事な概念は「桁です。DAA を用いて 10進数として扱っても良いのですが、8ビット CPUが最も簡単に扱える数は1バイトです。そのため、小数に1バイト、整数に1バイトの 2バイトをセットして扱うのが、最も簡単に小数を扱える事が出来ます。10進数では小数部1桁目は 0.0 から 0.9 まで 10種類ですが、小数1桁を 1バイトで置き換えますから、小数部1桁目が扱える数値は 0x00 ~ 0xFF の 256種類となります。無理矢理言葉として表現するのであれば 256進数です。ただ、この方法でも 0.005 程度の分解能はありますので、レトロPCのような低解像度では必要にして十分となります。
固定小数0
入れ物としてはこんなイメージでしょうか。それぞれ扱える数値は 0 から 255 までです。ここに 0.5 相当の数値を設定すると
固定小数1
となります。0.5 は 2倍すると 1.0 になりますよね。0x00, 0x80 の 0x80 に、さらに 0x80 を加算すると、 0x100 になります。桁上がりしていますから、結果として
固定小数2
このように整数部が 1 になり、小数部が 0 になっています。1 = 256 なので、0.5 = 128 = 0x80 というワケですね。ただ…、例えば 1/3 を固定小数で扱う場合は、256÷3≓85=0x55 となりますが、0x55 を3倍しても 0xFFと桁上がりしません。かといって、+1 して 0x56 としてしまうと 0x102 とさらに誤差が大きくなってしまいます。2のべき乗で割り切れない数値では、Z80の固定小数は誤差が大きいのは諦めポイントかもしれません。


  • 三角関数テーブルを作成する
8ビット CPUで小数を扱う方法が分かりました。三角関数は角度によって結果が返ってきます。XとYの成分を取得する COS と SIN は、それぞれ最小値が -1.0 で最大値が 1.0 となります。中心が 0.0 なので、そこを基点としてコンパスで 1.0 半径でグルリと周回するイメージで考えれば、なぜ 1.0 を超えないのかは理解出来るかと思います。

私たちが通常使用しているデグリー角は、1周360度となります。コンピュータでよく使用されるラジアン角は、1周2π(3.1415926×2.0)となります。Z80では1周を256とすると扱いやすいので、deg 360 = rad 2π = angle 256 とする事は多いです。デグリー角 deg からラジアン角 rad を求めるには、比率で計算するので 2π×deg÷360 で算出します。ということは、固定小数角度 angle からラジアン角を出すには 2π×angle÷256 とすれば良い事になります。

ところで、256角度は angle 1度で deg 1.4ぐらいです。Z80を搭載しているレトロPCでは、少なくともアクションゲームやシューティングゲームでは、ほぼこのような細かい角度は不要です。また、テーブルサイズが大きくなってしまいますので、今回は1周を64の分解能で、エクセルを使って、X成分とY成分を固定小数で取得してみます。
※ 参考までにエクセル(ods)ファイルもこちらからダウンロード出来ます。
        org     ($ + 255) / 256 * 256
;Y X Y X
TrgTbl: dw $0000, $0100, $0019, $00FE,
dw $0031, $00FB, $004A, $00F4,
dw $0061, $00EC, $0078, $00E1,
dw $008E, $00D4, $00A2, $00C5,
dw $00B5, $00B5, $00C5, $00A2,
dw $00D4, $008E, $00E1, $0078,
dw $00EC, $0061, $00F4, $004A,
dw $00FB, $0031, $00FE, $0019,
dw $0100, $0000, $00FE, $FFE6,
dw $00FB, $FFCE, $00F4, $FFB5,
dw $00EC, $FF9E, $00E1, $FF87,
dw $00D4, $FF71, $00C5, $FF5D,
dw $00B5, $FF4A, $00A2, $FF3A,
dw $008E, $FF2B, $0078, $FF1E,
dw $0061, $FF13, $004A, $FF0B,
dw $0031, $FF04, $0019, $FF01,
dw $0000, $FF00, $FFE6, $FF01,
dw $FFCE, $FF04, $FFB5, $FF0B,
dw $FF9E, $FF13, $FF87, $FF1E,
dw $FF71, $FF2B, $FF5D, $FF3A,
dw $FF4A, $FF4A, $FF3A, $FF5D,
dw $FF2B, $FF71, $FF1E, $FF87,
dw $FF13, $FF9E, $FF0B, $FFB5,
dw $FF04, $FFCE, $FF01, $FFE6,
dw $FF00, $FFFF, $FF01, $0019,
dw $FF04, $0031, $FF0B, $004A,
dw $FF13, $0061, $FF1E, $0078,
dw $FF2B, $008E, $FF3A, $00A2,
dw $FF4A, $00B5, $FF5D, $00C5,
dw $FF71, $00D4, $FF87, $00E1,
dw $FF9E, $00EC, $FFB5, $00F4,
dw $FFCE, $00FB, $FFE6, $00FE,
最小角度毎に4バイト使用しますので、それが64個となりますから、256バイトのテーブルとなります。これでもデカイです。ここまでの分解能が必要なければ、32方向でも問題ないと思います。実際、私が途中まで制作していた PC-6001mkⅡのプログラムでは 32方向でだいたい相手に命中していました。場合によっては16方向でも十分な場合もあります。実は正確に狙わない方が弾は避けづらいんで難易度は上がったりします。
次回、PC-8001 リスト構造と弾幕に続きます。いよいよ PC-8001 で弾幕が動くのか!?
やっぱり気になるのはやっぱり実行速度ですよね

※クレカ登録が抵抗あるときは、やっぱりプリペイドカードですよねぇ
;-----------------------------------------------------------------------
; 角度番号からXYの移動成分を取得する
; input: Acc = 角度: 0 - 63
; output: DE = X成分, BC = Y成分
;
TrigMtrc:
ex de, hl
add a, a
add a, a ; Acc = Acc×4
ld l, a
ld h, TrgTbl / 256 ; HL 三角関数テーブル
ld c, (hl)
inc l
ld b, (hl) ; BC Y成分
inc l
ld a, (hl)
inc l
ld h, (hl)
ld l, a ; HL X成分を
ex de, hl ; DE に移動して HL復帰
ret
テーブルが256バイトと、ぴったりアライメントにハマってるので、データの取得でポインタの移動は inc hl ではなく inc l で出来るのでちょっとだけ高速になっています。ここから得られた値は、同じく固定小数の X座標と Y座標に単純加算するだけで、値の更新が出来るようになります。


  • 弾のワークエリア
ここまで説明してきて、なんとなく悟ってますよね。以前、C# で三角関数を説明して、今回も三角関数を説明して、私が何を目指しているかって。そう、当然、漢なら弾幕です!その実現に必要なワークエリアの確保について、固定小数を扱うので先に説明しておきたいと思います。

プログラムが操作する場合の変数にあたるのがワークエリアです。このワークエリアに、座標等の情報を保存しておいて、この入れ物に対して数値操作するのが Z80では標準的なプログラミング手法となります。弾は最低でも位置情報と移動方向は情報として必要です。そのため、弾ひとつあたりの構造を equ で定義するのが便利だと思います。

さて、弾幕を作ろうとすると、弾数はかなり多くなりますので、このワークエリアのサイズは極力小さくしたいです。そのため、必要最小限で構成するようにします。また、まとめてワークを確保するため、一つ辺りの構造を定義した上で、トータルサイズをまとめて確保するようにします。

弾ひとつあたりの構造の定義と実際のワークエリアの確保を行います。
;-----------------------------------------------------------------------
; 弾構造体定義
;
BLT:
.NEXT equ 0 ; 2 次のワークアドレス
.ANGLE equ 2 ; 1 移動方向(0-63)
.POS_Y equ 3 ; 2 現在のY位置(固定小数)
.POS_X equ 5 ; 2 現在のX位置(固定小数)
.DRAW_ADRS equ 7 ; 2 描画アドレス(Base position)
.SIZE equ 9 ; ワーク一つ辺りのサイズ
.MAX equ 160 ; 最大弾数

WORK:
.Bullet ds BLT.SIZE * BLT.MAX
弾の最大数は 160としています。実はこの数、エミュで実行して使用される最大ワーク数を既に確認してて、その +1 のサイズを指定しています。高速化対応のため、アライメント調整したいところですが、ワークサイズが奇数であり、さらにワークが256バイトより遙かに大きいため、このワークはアライメントで高速化は厳しかったです。
※ 弾の威力とか状態とか種別とかのワーク追加でサイズが偶数になればアライメント調整できます。
実は実行速度を最優先しようと、最初は現在の位置に加えて、XY成分加算値もワークに確保していました。すると、ワークサイズが 1440バイト から 1920バイトまで増えてしまいまして、流石にこのサイズはヤバイと考え直しました。幸いな事に angle から XY加算値成分の取得はさほど重くなかったため幸いでしたが、このように消費メモリと実行速度に関しては、常に意識する事が Z80プログラミングでは肝要となります。

次回、PC-8001 リスト構造と弾幕に続きます。いよいよ PC-8001 で弾幕が動くのか!?
やっぱり気になるのはやっぱり実行速度ですよね
プレイステーション ストアチケット 5,000円|オンラインコード版
ソニー・インタラクティブエンタテインメント
2020-11-09
※ こちらはプレステストア用のプリペイドです。