見出し画像

三角関数を使ったマシン語プログラミング

いままでの記事ではすべて整数値でなおかつ斜め方向をプログラムには
取り込んでいませんでした。
なぜなら「面倒くさいから」という理由です。
今回の記事ではその面倒くさいを実現させてみたいと思います。

思い立ったが吉日

いったんゲームプログラムを作り終わると次のプログラムのことを考えたりします。次のプログラムではキャラの動きに曲線的な動きを持たせないな。と考えたりしました。ギャラガとかゼビウスとかでもクルクルーっと回るような動きをするキャラがいました。
あんなのをどうやってやるんだろなーと思ったのが記事執筆の始まりです。
「曲線的」。
これが少年の頃、数学が万年赤点だった筆者にとってはすごい壁です。
今回の記事では内藤時浩さんのブログを参考に執筆して、MSX版に置き換えています。内藤さんはハイドライドのプログラマーという筆者にとっては神の存在なのですが、内藤さんの記事はとてもフレンドリーでわかりやすくて毎回参考になります。感謝感謝ですね。

三角関数をふりかえる

曲線、つまり角度が重要になってくるわけですが万年赤点時代の少年の頃に戻って学習しなおしてみます。
三角関数は、サイン(SIN)・コサイン(COS)・タンジェント(TAN)。です。
いきなり三角関数の学習に行く前に順序だてていきましょう。

今回の記事ではTANは対象にしません。そのうち書きます。

最初のつまづきポイント:小数点の表現について

いままでの記事ではすべて整数値でした。
「うへぇ、小数点ってどうやってマシン語で表現するの??」
というところからいきなりつまづきます。

さて、コンピュータで小数点を表現するためには固定小数点(fixed-point)と浮動小数点(float-point)という考え方があります。
どっちもFPと略します。当記事では固定小数点という考え方を説明します。
バイト値を並べてどこかのビットから下位を小数点に決めて表現してしまおうという考え方です。

例)
2バイト値で上位1バイトの下位4ビットから下を固定小数点にする場合

HL=1880H

0001 1000 1000 0000


こんな表現になります。最初の4ビットの0001が整数部、以降が小数部です。
整数部はいままでの記事のとおり、0001なので10進で1です。
小数部は少しややこしくて一番左のビットから

1/2, 1/4, 1/8, 1/16, 1/32 ... という値を表すことになります。
上記例だと 1000 1000 0000 なので 1/2(0.5) + 1/32(0.03125)で 0.53125 です。つまり、整数部と加算すると、1.53125 という小数値を表現していることになります。

Z80だと1バイトを扱うのがいちばん簡単なので2バイトの上位1バイトを整数値、下位1バイトを小数値として考えることにします。
HLレジスタのHレジスタを整数部に、Lレジスタを小数部にする。というような考え方です。
この固定小数点方式は8.8fixed-pointと呼ばれます。8ビット整数、8ビット小数の固定小数点という意味です。
上位1バイトの第7ビットはMSB(符号ビット)として扱うようにすれば
-128.99 から127.99 までの小数値を表現できるようになります。

計算するとわかるのですが、小数部全部のビットが1になった場合は、0.99609375 にしかなりません。

8ビット小数だと全部のビットが1でも0.996程度にしかならない

これがいわゆる誤差になります。コンピュータで小数を実現すると必ず誤差が生じます。誤差をなくすためにはたくさんのビットを連ねるしかありません。ですが、8ビットであれば仕方ないし、「MSXで月に行くわけでもない」のでこの程度の誤差は許容しておきましょう。

ちなみに小数部の第7ビットが繰り上がったら整数部が1加算されます。

0000 0000 1000 0000
↑上記8.8fixed-point 2バイトの状態で小数部に0.5(1000 0000)を加算すると整数部が1になります。0.5 + 0.5 で繰り上がって整数が1足される。これはわかりやすいですね。

円周

さて、ここからは三角関数です。
最初に円周はいくつでしょう?
円周率というのがありましたね。3.14159264...これがパイ(π、PI)ですね。
ラジアン(RADIAN)角ともいいます。
円周は2πです。少年の頃、「なんでなんで??」と思いながら授業に追いつくことができませんでしたが
いまは大人になりました。
「そういうものです」という理解で進めることができます。
「そういうものです」って言われてもなあ・・というのが私の少年の頃でした・・。

パイのパイのパイ!

角度

円の角度は360度です。この360度で1周するため、0-359が円の角度になります。また360度で一周する、この角度の単位はデグリー(DEGREE)角ともいいます。三角形の内角の合計は180度ですね。
コンピュータではラジアン角で基本的に考えるのがセオリーらしいです。

直角三角形だといろいろ使える三角関数

三辺のうち1角が90度になっている三角形のことを直角三角形と呼びます。
X辺とY辺、そして斜辺で三辺を表現しましょう。
この直角三角形のときにサイン・コサインを使うとそれぞれの辺の長さを算出することができます。角度のことをθ(シータ)記号でも表します。
ああ、このへんから落第した少年のころのワタクシ・・

サイン、コサイン

SINθは Y ÷ 斜辺です。逆を言えば SINθ * 斜辺でYが算出できることになります。
COSθは X ÷ 斜辺です。逆を言えば COSθ * 斜辺でXが算出できることになります。

三角形の内角の合計は180度ですから、半円(180度)はこの理屈で算出できます。この理屈をベースにしてあらかじめX値、Y値を計算しておきます。

0度であればX=1,Y=0
90度であればX=0,Y=1
180度であればX=-1,Y=0
270度であればX=0,Y=-1

になるようにそれぞれの角度でのX値、Y値の基準値をあらかじめ算出しておくということです。この値があれば例えば45度の角度で4ドット先に移動したい場合、45度のX値、Y値にそれぞれ4をかければ座標が算出できるようになります。

なお基準値の算出をSINθ、COSθを手計算でやるのはバカバカしいのでEXCELで計算しておきましょう。計算結果のEXCELは内藤さんのブログからも拾えます。

計算ではラジアン角を使います。

例えば
X値=COS*ラジアン角*256
という感じです。

なんで256をかけるの?という点ですが、小数点があるためです。
計算結果に256をかけるとあら不思議。
整数値が上位1バイトに、小数値が下位1バイトになります。
これは実際に自分でやってみると良いです。

8ビットで表現しやすい形で考える

円周は360ですが、これだと8ビットの値を超えてしまいます。
256で一周するように考えるとMSX(Z80)で扱いやすいので新たな単位として256angleという単位を作ります。デグリー1.4度くらいで1angleという感じ。
256角の算出値を8.8fixed-point方式でメモリ常に保持するとなるとX値=2バイトx256角=512バイト、Y値=2バイトx256角=512バイトで合計1024バイト(1KB)必要ですね。
MSXでの1KBバイトは無駄です。今回は円周を64分割した角度で考えます。
これでも256バイトあります。32分割でも良さそうですが内藤さんリスペクトなので64分割で考えます!

今回のサンプルコード

当記事恒例のサンプルコードです。まずはサンプルコードをゲットだぜ!
https://github.com/sailorman-msx/games/tree/main/src/sample020

今回の記事からは「俺ライブラリ」仕様に切り替えています。フレームワークというほどでもないので「俺ライブラリ」と自称してます。
これは次回ゲームを作るとき自分が作りやすいようにと構造化しているためです。他者からすると、とても読みづらいかと思いますが以下のような構造になっています。

「俺ライブラリ」だぜ!

今回のメインとなる部分はdata_trigono.asmとgame.asmです。
game.asmと言いながらゲームではないけれど、5/60秒ごとにgame.asmの処理が呼ばれ、X=192,Y=64の座標を中心としてスプライトを半径64で円を描くように表示しています。
data_trigono.asmに記載している値はすべて内藤さんのブログからの引用となっています。

まわる〜まわるぅよ、スプライトがまわる〜

ただそれだけのサンプルですが、前述した説明と見比べながらソースを読んでもらえればおおよそ理解できるかと思います。特筆すべきは小数点の掛け算ですかね??マイナス値の場合はいったんNEGで正の数に変換してから掛け算して最後に再度NEGする。っていうことをしています。
NEGって何?というかたは過去記事を参照ください。

さあ、今回のサンプルで角度が分かればX値、Y値の増分値が取得できるようになったのでこれからは角度を決めて弾を発射する。なんてことも可能になりますね!!

おまけ:マシン語はかくかたりき

ひさしぶりのZ80マシン語のコーナー。
(というかZ80ASMのコーナー・・)
Z80ASMではDEFVARSというディレクティブがあり、これを使うと変数宣言が楽になる。ということを先日知りました。
variable_define.asm を見ると以下のようになっています。

;--------------------------------
; ここから変数
;--------------------------------
DEFVARS $C000
{

H_TIMI_BACKUP     ds.b 5     ; 5バイト H.TIMIバックアップ用領域
VSYNC_WAIT_CNT    ds.b 1     ; 1バイト H.TIMI垂直同期待ちフラグ
WK_GAMESTATUS     ds.b 1     ; 1バイト ゲーム進行ステータス

; インターバルタイマ関連
WK_GAMESTATUS_INTTIME ds.b 1 ; 1バイト
WK_TIME05         ds.b 1     ; 1バイト 5/60秒タイマ

DEFVARS $C000という宣言を行うと、以降、{ } で囲まれたアドレスを勝手に解釈してくれます。DS.B n と書くと、そのアドレス以降が1バイト使う。というように解釈します。C言語のstructと同じような使い方ですね。
上記例だと、H_TIMI_BACKUPのアドレスは$C000として解釈され、5バイト使ったあとの、VSYNC_WAIT_CNTのアドレスは$C005として勝手に解釈してくれるということです。
これは便利!!!なぜ今まで知らなかったのか・・
読者のみなさん、ごめんなさい・・筆者はその程度なんです・・
DS.W n と書くとそのアドレスから n*2 バイト使う。という意味になります。

今回の記事はここまで!!
次回以降の記事でギャラガみたいなゲームできるといいなあーと思いつつ。
では、また!ノシ

セーラー服が似合うおじさんです。猫好き、酒好き、ガジェット好き、楽しいことならなんでも好き。そんな「好き」をつらつらと書き留めていきます。