TOP > プログラミング関係解説&調査

プログラミング関係解説&調査

本攻略のトップページでも紹介しているが、本作のデータや各種計算式(ダメージ量の算出方法など)は、以下ウェブサイト様に掲載されており、本攻略でも引用させていただいている。

見ればわかるとおり、上のふたつのウェブサイト様は、「ライブ・ア・ライブ」関連の知識の教科書兼百科事典と言っても過言ではない。
当コンテンツは、教科書兼百科事典の副読本程度として、「どうしてそういう計算式になるのか?」「ここに載っている以外の計算方法は?」といった話を紹介することが目的である。

内容の性質上、本作のネタバレを含むことをご了承いただきたい。

計算式を理解する上で必要な数学・プログラミングの基礎知識については、プログラミング関係の表記及び解説で紹介したが、ここでは更に一歩踏み込んで、実際のプログラムではどうなっているのか、という話をする。
ただし筆者はプログラミングについて大した知識はないため、プログラム内容を見てみてもわからないこともあるし、間違った読み方をしていることもあるかもしれないことをお断りさせていただく。

このページでは、以降の説明を読む上で必要となる基本事項について述べている。

スーパーファミコンのプログラムについて

「ライブ・ア・ライブ」はスーパーファミコンのゲームソフトなので、まず、スーパーファミコンのプログラムの方式についてざっと説明する。

スーパーファミコンのCPU(演算などを行う装置)は、65C816互換16ビットマイクロプロセッサ(厳密には「Ricoh 5A22」というカスタム版CPU)が使われている。
このCPU向けのプログラム言語はアセンブリ言語という言語が使われており、アセンブリ言語はCPUにより仕様が変わるため、スーパーファミコンのプログラムは65C816向けのアセンブリ言語(以下単に「65C816」とする)で書かれている。
アセンブリ言語 - Wikipedia及び、65816 (コンピュータ) - Wikipediaも参照のこと。
当コンテンツでは、65C816のプログラムの説明をしているため、ある程度のプログラミングの知識がないとわかりにくいと思われる。

リメイク版発売の2020年代に主流となっているプログラミング言語(C系言語、Python、JavaScriptなど)は、人間が読み書きする時に比較的わかりやすい、高水準言語(高水準言語 - Wikipedia)と呼ばれるものなのだが、アセンブリ言語は人間側にもコンピュータ側にもわかりやすいことを優先した低水準言語(低水準言語 - Wikipedia)であるので、高水準言語の知識がある程度あるとしても、アセンブリ言語の読み書きはややハードルが高い。
また、1980年~90年代のコンピュータの技術であるから、現代から見るとプログラミングにかなり制限があるように見えることも(扱える数字の幅が狭い、乗除算の手間がかかるなど)、ハードルの高さに繋がっている。
ただし、プログラムの基本構造である、「ある条件で分岐する」や「ある条件を満たすまで繰り返す」という理屈は、どんな言語でもだいたい同じである。
ここでは、細かいことは抜きにして、「そういう理屈でプログラムが書かれている」という説明を中心にしたつもりである。
わかりにくいところは飛ばして、結論だけ見る程度でも構わない。

65C816はスーパーファミコンのプログラミング言語として使われているため、検索してみると、それなりに解説や資料が見つかる。
英語の資料が多いが、日本語での説明もあるため、参考にしていただきたい(筆者も参考にさせていただいた)。

「ライブ・ア・ライブ」のデータについて

「ライブ・ア・ライブ」は、容量が「16メガビットロムカセット」のスーパーファミコンソフトであり、内部データはすべてのデータが16進法、0~9, A, B, C, D, E, Fの16文字で記されている。
1ビットとは2進法での1文字(0~1)分のデータのこと。
1ビットだと1桁、01しか記録できないが、2ビットなら2桁なので、00, 01, 10, 11の4種類(22)の数が記録できる。
という理屈で、4ビット、つまり4桁まで増やすと24=16種類まで記録できる。
一方16進法は、0~9, A, B, C, D, E, Fの16文字を使えるので、1桁だけで16種類記録できる。16進法1文字で4バイト分のデータを記録可能、ということである。
16メガビット=16,000,000ビットで、16進法の文字では2,000,000文字分ということになる。
内部データをパッと見ただけでは、数字とA~Fの文字が2,000,000文字分、ずらずら並んでいるだけで意味不明である(この状態でもプログラミングに慣れている人はデータを読み取れるそうだが)。
スーパーファミコンのカセット内に記録された内部データを、スーパーファミコン本体のメモリ領域に読み込み(ロード)して、プログラムを実行しゲームがプレイできるのである。
バーチャルコンソールの場合は、Wii Uやニンテンドー3DSの本体に、カセット内データを保存してあり、それを読み込んでいるということになる(はずだが、筆者はバーチャルコンソールの仕組みまではわからない)。

16進法で内部データが記されているが、16進法の2ケタ分(2文字分)を1バイト(Byte)として、2文字ずつ区切り、1バイトごとにナンバー(16進法)を振って、場所(アドレス)を決めている。
たとえば、データが

01 23 45 67 89 AB CD EF 98 76 54 32 10 FE DC BA

こんな感じだったら、10進法で16バイト分のデータとなる。
01のアドレスは「00」(1番目)、ABのアドレスは「05」(6番目)、DCのアドレスは「0E」(15番目)である。

スーパーファミコンのカセットのデータは、スーパーファミコン本体のメモリ領域に読み込み(ロード)するが、本体のメモリ領域の場所にも番号を振ってアドレスが決められている。
番号は16進数で6桁であり、上2桁を「バンク」、下4桁を「アドレス」と呼ぶ。
形式としては、バンクとアドレスで区切り、更に16進数であることを示すために頭に「$」をつけ、
$00:0000
や、
$00/0000
としたり、区切りなしで記すこともある。
また、同一バンク内で、アドレスのみを記す時は下4桁だけ「$0000」とする場合もある。
最大値は$FF:FFFFになる。
とりあえず「こんな風に表記していたら、メモリの位置を示したアドレスのこと」だと思ってくだされば問題ない。

ゲームデータの16メガビット内には、キャラクターや敵のデータ、技やアイテムのデータ、グラフィック関連のデータ、効果音やBGMなど音関連のデータ、セリフなどテキストデータ、そしてそれらを実際にゲームとして動かすためのプログラムのデータが詰め込まれている。
当コンテンツで紹介するのは主に、色々なデータをゲームとして動かすためのプログラム部分である。

ビット(bit)やバイト(byte)などの単位について

コンピュータでのデータ容量の単位はビットやバイトなど、似たようなネーミングの単位があって少しややこしい。
8ビット = 1バイトであり、2進数なら8文字(0000 00001111 1111)、16進法であれば2文字(00FF)で表現できる。
スーパーファミコンのゲームソフトのデータは、16進法の文字で書かれているので、「ビット」より「バイト」を単位にするとわかりやすいことも多いし、「バイト」単位で解説しているウェブサイトも多い。

2進法・16進法

プログラミングの話をする上では、中学~高校数学の基本的な知識と共に、コンピュータで扱われる2進法・16進法の知識があることが望ましい。最低限、仕組みについては知っておいた方が良い。
65C816では、数値はすべて16進数で表記し、2進法や16進法ならではのテクニックで計算を行うことが多いからである。
また、2進数においては、数値操作やフラグ操作などで頻繁に用いられる、ビット演算における論理演算(論理積や論理和など)、シフト演算の知識も必要である。
何もわからないまま、いきなり下の説明などを読んでも面食らうだけだと思うので、ゲームのプログラムでは実際にどのように使われているのかから学んでみるのも良いかもしれない。
例えば「ライブ・ア・ライブ」であれば、8種類の状態異常を2進数8桁のON・OFFで表し、技に設定された状態異常はどれか? や、現在の敵の状態異常の状況の読み出しなどをビット演算で行っている。

数学関連だと、基本的な四則演算(累乗など含む)、中学~高校数学レベルの確率計算と、高校数学で習う、数列・漸化式(漸化式 - Wikipedia)の知識があるのが望ましい。

また、65C816では、頭に$をつけることで16進数の数値であることを示す。
つまり「$12」は16進数表記だから、10進数では「18」である。
$FF」は2桁の16進数の最大値で、10進数だと「255」である。
2進数の場合は頭に%をつけて「%0101」(10進数の5)とするが、65C816の中ではこの表記をすることはまずない(たぶん)。
ただ、2進数の考え方が必要な時、説明には使っている。

正負表現

10進数だと、数字の前に+-の記号をつけることで、正の数か負の数かを区別し表記できる。
だが、2進数や16進数は、正負の記号を使わずに正負を表現する。
2進数の一番最初の位に「1」を立てると負の数、「0」なら正の数、というのが2進数での負の数の表現である。
正負の値(符号)がついているので、8ビットなら「符号付き8ビット整数」という。
2進数の頭の1桁は正負を表現し、残りの7桁が通常の数値(絶対値)である。
「符号付き8ビット整数」だと、10進数の-127から+127までを8ビットで表現できることになる。
16ビットなら「符号付き16ビット整数」という。頭の1桁は正負を表現し、残りの15桁が通常の数値(絶対値)なので、10進数の-32767から+32767までを16ビットで表現できる。

ちょっとややこしいのは負の数の表現方法で、10進法なら0を原点として、負の方向へ-1, -2, -3, ……と数値(絶対値)は増えていくのだが、符号付き8ビット整数だと、0を原点とした時に、%1111 1111, %1111 1110, %1111 1101, %1111 1100, ……と、数値(絶対値)が最大から最小へと減少していくのである。
よって、10進法「-1」は「%1111 1111」=「$FF」、10進法「-2」は「%1111 1110」=「$FE」……と、通常の16進数で見ると最大の$FFから減少していくように見える。
少しわかりにくいので、$100から符号付き8ビット整数を引くと、10進法っぽくなる。
たとえば$FBだったら、$100 - $FB = $5であり、10進法「-5」ということになる。

詳しくは以下も参照のこと。

スーパーファミコン&65C816の基礎知識

この先、プログラムに関してはその都度説明を付けているが、頻出となる、重要度の高い65C816の仕様についてまとめておく。

「レジスタ」について

「レジスタ」は、プログラムの実行中、一時的に様々な値(16進数)を記録しておく場所のことである。
メインで使うことになるのが、「アキュムレータ」という名前のレジスタで、当コンテンツでは「Aレジスタ」としたり、頭文字をとってAと省略表記していることが多い。
省略表記するくらい頻出という意味でもある。

他に「インデックスレジスタX」「インデックスレジスタY」というレジスタも頻出であり、当コンテンツでは「Xレジスタ」「Yレジスタ」としたり、頭文字をとってX, Yと表記することが多い。

プログラミングの知識がある方なら、レジスタは変数のことだと言えばわかるだろう。
スーパーファミコンにおいては、スーパーファミコン内のメモリ領域も変数として扱うのだが、それらの値は一度Aという変数にロードし、数値計算などの処理をする。
スーパーファミコン内のメモリ領域の値同士を直接計算することはできず、どちらかをAにロードして処理することになる。
そしてメモリの出し入れの際、アドレス指定のための計算にXYを使うことがほとんどである。
ループ処理で、アドレスを+1しながら順に計算をする場合などは大抵、XYに値を1~2ずつ足して次のアドレスを指定するなどしている。

レジスタには値を一時的に記録しておく、と記したが、65C816で記録できる値(つまり計算などで使える値)には制限がある。
2バイト(16進数で4文字)分が限度なので、0から16進数FFFF(10進数65535)まで、ということになる。

レジスタには値を「記録する(書き込む)」、記録された値を「読み込む」ことができるが、値の桁数が大きいほど読み書きに時間がかかることになる。
よって、レジスタの「下2桁だけ読み書きする」「4桁すべて読み書きする」と、読み書きの桁数を切り替えることで、読み書きの時間を変え、プログラム実行時間を変更することができる。要するに基本的な時短テクニックである。
2桁のみで読み書きするのが「8bitモード」で、4桁すべて使うのが「16bitモード」である。
「8bitモード」の方が扱う桁が少ないから、読み書きの時間が少なくて済む。
よって、基本的には、「必要がない限り8bitモードでプログラムを実行する」ことになる。

現在どのモードで計算をしているのかは、次で説明する「プロセッサステータスレジスタ」で判別できる。

「プロセッサステータスレジスタ」とフラグ

「レジスタ」には何種類かあり、重要なのは上で紹介したA, X, Yの3種類である。
それに加えてもうひとつ、「プロセッサステータスレジスタ」というものについても覚えておく方が良い。
ここには8ビット分、つまり2進法で8文字分のデータの記録しかできないのだが、1文字ずつが重要な意味を持つ。
8文字がそれぞれ別々の名前を持っており、「ネガティブフラグ」や「ゼロフラグ」、「キャリーフラグ」など、「○○フラグ」という名前がついていて、2進法1文字分の「0か1」しかデータが入っていない。
言い換えると「そのフラグがONかOFFか」というデータが入っているだけである。
1ならON、0ならOFFで、ONの時は「フラグが立つ」、OFFにする時は「フラグをクリアする」などという言い方をする。
また、各フラグは頭文字だけ取ってNフラグやZフラグ、Cフラグと省略表記することもある。

プログラミング用語を使うなら、ONとOFFを記録することから、特定の真偽値(boolean)を記録しておく変数、といえる。

少し前に、2桁のみで読み書きするのが「8bitモード」で、4桁すべて使うのが「16bitモード」である、と説明したが、このモード切り替えも、「プロセッサステータスレジスタ」の中の「メモリモードフラグ(Mフラグ)」「インデクスモードフラグ(Xフラグ)」である。
「メモリモードフラグ」がONなら、アキュムレータAは「8bitモード」で、OFFならアキュムレータAは「16bitモード」である。
「インデクスモードフラグ」がONなら、インデックスレジスタXYは「8bitモード」で、OFFならアキュムレータXYは「16bitモード」である。
このふたつのフラグを見れば、どのモードで計算をしているのかがすぐわかるということになる。

他に、フラグの中で覚えておくと良いのは、上にも記した「ゼロフラグ」「キャリーフラグ」である。
数値の判定のみならず、条件分岐で頻繁に登場するフラグだからである。
「ゼロフラグ」は文字通りに、「何かを計算した結果、0になった」や、「何かの値をロードしたら、その値は0だった」など、結果が0の時にONになる。
これがどのような時に使われるかというと、例えば、「攻撃した相手がオディオモールだったら、ダメージ量は1に変更する」という条件分岐の時、

  1. 「現在攻撃した敵のID」-「オディオモールのID」を計算する
  2. ゼロフラグが立ったら(つまり引き算の結果が0なら)、ダメージ量を1にする

こんな風に使えるし、実際にこんな風に内部で計算されているのである。

「キャリーフラグ」は、繰り上がりに関係したフラグである。ちなみに数学における「繰り上がり」は、英語では「carry」である。
65C816では、上で記した通り、8bitモード(16進数だと2桁)と16bitモード(16進数だと4桁)で計算ができるが、計算によっては、2桁や4桁では足らなくなったり、桁からあふれることがあるだろう。
例えば、8bitモードで、16進数で$40(10進数64)に$C0(10進数192)を足すと、$100(10進数256)となり、3桁目に繰り上がる。
この時、65C816では、8bitモードだから2桁までしか答えを表示できない。そこで$100の下2桁の「00」を答えとして表示し、更に「キャリーフラグ」をONにすることで、3桁目に1が溢れたということを示すのである。
これは足し算だけではなく、掛け算でも同様である。

また、割り算で余りが出た時も「キャリーフラグ」で表現する。
$40(10進数64)を2で割ると、答えは$20(10進数32)で余りはないためキャリーフラグは立たない。
だが、$41(10進数65)を2で割ると、商は$20、余りが$1、と計算され、「20」を答えとして表示し、更に「キャリーフラグ」をONにすることで、余りの「1」を示す。

問題は引き算で、引き算では「繰り下がりが起きなかった」時にキャリーフラグがONになる。
どうして逆なのかをここで説明すると長くなるので省く。
キャリーフラグとオーバーフロー
に理由が詳しく書かれているが、65C816だと、引き算を「足し算で処理」するために、逆になってしまう。
ややこしいため、理屈は抜きにして、とりあえずそういうものと思っていただいても問題ない。

さて、この「キャリーフラグ」も、ゼロフラグに近い形で条件分岐に使える。
たとえば、0~255の乱数を、疑似乱数計算で作ったとする。
作った乱数が、0~127だったら「テレポートコマンドが成功して通常通り逃げられる」、128~255だったら「テレポートコマンドが失敗しチビッコハウスに飛ばされる」というように分岐させたい場合は、

  1. 乱数 - 128」を計算する
    (実際には2進数の計算なので、乱数は「00000000~11111111」であり、128は2進数で「10000000」である)
  2. キャリーフラグによって条件分岐させる
    • キャリーフラグが立つ場合(減算の結果が0か正の数の時)、テレポート成功で逃げられる
    • キャリーフラグが立たない場合(減算の結果が負の数の時)、テレポートコマンド失敗時の飛び先計算に進む

このように実行可能なプログラムを書けば良い。
2進数「10000000」を引く時、一番上の位(27の位)にしか「1」がないから、引き算で繰り下がりが生じるのは、27の位が「0」の数だけである。
この条件を満たすのは、2進数「10000000」より小さい、「0???????」の時のみ、つまり10進数でいえば127以下の数だけである。
よって、乱数が128~255の時、「乱数 - 128」で繰り下がりが生じず、キャリーフラグが立つのでテレポート成功で逃げられる。
逆に、乱数が0~127の時、「乱数 - 128」で繰り下がりが生じ、キャリーフラグが立たないのでテレポートコマンド失敗判定である。

という判定が、実際にテレポート実行時に行われている。

以上の、ゼロフラグによる分岐と、キャリーフラグによる分岐は、「ライブ・ア・ライブ」のプログラム内で頻繁に行われている。
上のとおり、ゼロフラグによる分岐は「ある値とある値が同一かどうか」の判断に、キャリーフラグによる分岐は「乱数によって条件分岐する場合」に使用することが多い。

なお上で名前だけ出した「ネガティブフラグ」もそこそこ登場するが、これは「2進数で負の数」の時に立つフラグである。
上で述べたが、2進数の負の数の表現は「符号付き8ビット整数」や「符号付き16ビット整数」の考え方で、一番上の位を1とすることで負の数を表現するため、「ネガティブフラグ」が立つのは、2進数で一番上の位が1になった時である。
上のキャリーフラグでの分岐は、2進数「10000000」より小さいかどうかであったが、2進数「10000000」は一番上の位が1なので、2進数「10000000」以上の数はネガティブフラグがONになり、やはり分岐条件で使うことが可能である。

オーバーフロー

8bitモードの時、計算結果が16進数2桁の最大値$FFを越えて3桁目の$100になると、キャリーフラグが立ち、$100の下2桁の「00」を答えとして表示する、と上に記した。
この話から、レトロゲームのバグの原因のひとつ、「最大値255または65535を越えると0に戻ってループする」という現象を思い浮かべた方も多いだろう。
いわゆる「オーバーフロー」、桁溢れと呼ばれている現象になる。
もちろん、レトロゲームであっても、計算結果がオーバーフローした時の対策はなされていることが大半である。ただし「ライブ・ア・ライブ」と同時期のスクウェアのゲームは、特殊な状況下でのオーバーフローが割と見逃されていることがあり、ファイナルファンタジーシリーズなどはファンによる研究も盛んであるため、有名なバグがそこそこ発見されている。詳しくは適当に検索していただきたい。

本作ではオーバーフロー関係のバグはほとんどないようで、オーバーフロー対策されている処理があちこちで見られる。
例えば「足し算の結果、オーバーフローして$FFを越え、キャリーフラグが立ったら、この足し算の結果を$FFに変更する」といった分岐で処理している。
ただ、どうやらその処理をしていない箇所もあるらしく、それが意図したものかどうなのかがよくわからない。
一例が「武器の追加効果の状態異常」の発生時の処理で、状態異常の継続時間の計算時に、オーバーフローを考慮していないように見受けられる。
ただ、オーバーフローにより状態異常継続時間の数値がやたらと小さくなることがある程度で、ゲームに致命的な何かが発生している様子はなく(バグってデータが破壊されるなどは起こらない)、意図したものかどうかは不明である。

65C816の命令(ニーモニック)

65C816のプログラムは、何をするかをアルファベット大文字3文字で命令していくことで成り立っている。
このアルファベット大文字3文字の命令のことを「ニーモニック」といって、色々な命令があるが、プログラマーでない限り、それらをまるごと暗記する必要はない。
わからなければ、命令一覧から調べれば良いだけである。
命令(ニーモニック)は、
65C816 プログラミング リファレンス
ここなどで調べれば良い。

たとえば「LDA」という命令があるが、これは上で説明したアキュムレータ(A)に、指定した値をロード(読み込み)しろ、という意味である。
値の指定方法が色々とあるのが少しややこしいが、これも命令一覧で何の意味か調べれば良いだけなので、無理に覚える必要はない。
命令の書き方としては、

LDA #$01

こんな感じで、命令の後に読み込みたい数値や、内部データのアドレスを記す。
頭に「#」がついている時は数値であり、上の命令は「Aに16進数の01をロードしろ」の意味になる。
アドレスは先に説明した通り、

LDA $7EFEF0

このようにアドレスの数値を指定し、頭には「#」を付けない。
上の命令は、「Aにアドレス$7E:FEF0の中に入っている数値をロードしろ」の意味になる。
($7EFEF0という数値のロードではない)
他にも色々な指定の方法があるが、どれが何の意味かは、その都度解説しているので参照していただきたい。

また、この先では、

$C1/85D9 LDA $7EFEF0 ;Aに[$7EFEF0]をロード

このように命令&解説を記しているが、最初の「$C1/85D9」は、命令「LDA $7EFEF0」が書かれているアドレスのことである(よって、実際にはプログラムには書かれていない)。
「;」以降は解説で、「;」を付けた後から改行までの文章は、プログラムで読み込まないようになっている。
(ついでに、色分けはわかりやすさ重視でしてあるだけである)

なお、先に、「内部データはすべてのデータが16進法で書かれている」と述べたのに、「LDA」の「L」のように、16進法では使わない文字が使われているのはどうしてなのか、と思われたかもしれない。
実は、内部データではLDAなどのニーモニックが、「オペコード」という識別番号(16進数2文字)で表記されている。
先に紹介した、65C816 プログラミング リファレンスにも、ニーモニックに対応したオペコードが掲載されている。
例えばLDAは、オペコードだと「AF」である。
LDA $7EFEF0の部分は、内部データだと「AF F0 FE 7E」という16進数8文字で表記してある。
$7EFEF0部分はLDAを意味するAFの後に、2桁ずつ逆の順序で記す方式である。
とはいえ、さすがに「AF F0 FE 7E」だけではわかりにくいので、当コンテンツではオペコード表記は省いてニーモニックを記している。

基本の計算方法

65C816における基本的な計算方法をまとめておく。
足し算や引き算は、該当のニーモニックを使えば良いのだが(足し算はADC、引き算はSBC、他に「+1」や「-1」するための専用のニーモニックもある)、他の計算には一工夫必要である。
また、16進数や2進数の計算であることを利用したテクニックもいくつかある。

なお、プログラミング関係の表記及び解説で記したとおり、65C816での計算では、掛け算や割り算の結果で小数点以下の端数が出たら(基本的に)切り捨ての処理を行う、ということも覚えておくこと。

ニーモニックを使った記述

足し算はADC、引き算はSBCでできる、と書いたが、例えばADCでできることは、「アキュムレータAと、指定したメモリの値を足す」ことである。
何かと何かを足し算したい場合、まずAに数値をロードし、次にADCでその数値に足したい値を指定する、という方法を使う。
例えば2+5(16進数)を計算するなら、

LDA #$02   ;Aに$02をロード
ADC #$05   ;A + $05を計算

上を実行すると、Aに足し算の答えの7(16進数)が書き込まれる。
こんな風に2つのニーモニックが必要となる。
また、最初にAにロードしていた「$02」を上書きするため、最初の値を残しておきたい場合は、別途どこかのメモリアドレスに書き込んでから足し算を行う必要もある。
Aに何かをロードして何かの計算、というのが、65C816の基本中の基本である。
Aに入る数値は次々更新されていくことになるので、今何の値が入っていて何の目的の計算をしているのかがわからないと、65C816のプログラムを解読できない。
と偉そうに言う筆者も、わからないことは多いので、わかる範囲でのみ当コンテンツで解説をしている。

掛け算と割り算

65C816には、乗除算、つまり掛け算と割り算の機能はない。
(実は引き算もできず、足し算しかできないという話は、キャリーフラグとオーバーフローにある通りである)
乗除算をする方法は、

ここなどに記されているとおりである。
簡単にまとめると、

  • 2倍する(×2):ASL(算術左シフト)を使う
  • 2で割る(÷2):LSR(論理右シフト)を使う
  • それ以外:コプロセッサというCPUの拡張機能で上サイト様のようなコードを使って行う
    ※上サイト様にある「符号付8bit x 8bit」というのは、正負の値を考慮した「符号付き8ビット整数×符号付き8ビット整数」の掛け算のことである。
    「ライブ・ア・ライブ」でも、「符号付き8ビット整数×符号付き8ビット整数」「符号付き16ビット整数×符号付き8ビット整数」「符号付き16ビット整数÷符号付き8ビット整数」などは上サイト様の方法で計算を行っている。

となっている。
※単に割り算のやり方としてこういうものがあると覚えていていただければ良いが、「算術左シフト」や「論理右シフト」について詳しくは、 ビット演算 > ビットシフト - Wikipediaなどを参照のこと。

また、特定の数で掛け算や割り算をする時に使える方法もある。
本作のプログラムでよく出てくるのが、「256で割る」、16進数でいえば「$100で割る」方法である。

わかりやすくするため、我々が普段使う10進法の話にする。
÷100÷102)を計算する場合、小数点を2つ左に移動させる。
例えば、
1234.56÷100 = 12.3456
である。この時、動いたのは小数点だけで、数字の並び自体はまったく変化していない。

では16進法だとどうなるかというと、小数点を2つ左にずらすということは162 = 256で割り算をすることと同義である。
つまり16進法での÷256の計算とは、16進法において小数点を2つ左にずらすことである。
例えば16進法で「$1234」という数に対し÷256(10進法)を行うと、小数点が2つ左に移るから、上の2桁だけ取り「$12」が答えなのである(これまでも述べたとおり、65C816での計算で小数点以下の数値が出たら切り捨て処理なので、下2桁の34は切り捨てられる)。

割り算の時だけではなく、16進法で4桁の数から、上2桁だけを取り出して2桁の数にしたい(上で言えば「$1234」から上2桁の「$12」だけ取り出す)という処理も、結局は「256で割る」計算をすれば可能ということである。

8bitモードと16bitモードの応用

8bitモードと16bitモードの切り替えで、実質的な割り算をする方法もある。
8bitモードでは16進数で2桁(1バイト)、16bitモードで4桁(2バイト)の数値をレジスタ(AXY)に出し入れできる。
だが、出し入れする先のアドレスのメモリには、1バイトしか数値が収まらない。
16bitモードで2バイトの数値をメモリに入れろ、という命令の時、どうやって1バイト分の容量しかないメモリに値を入れるのだろうか?

こういう時には、「下2桁分の1バイトは、指定先のアドレスに値を入れる」「上2桁分の1バイトは、指定先のアドレスに+1した場所に値を入れる」という仕様になっている。
入れる時だけではなく、出す(ロードする)時でも同じである。
たとえば、16bitモードで、$1234という値を、メモリのアドレス[$00:0030]に入れろ、という命令があったとしよう。
この時は、下2桁の$34[$00:0030]に入り、上2桁の$12[$00:0031]に入る。
ではこの後に8bitモードに切り替えて、[$00:0031]Aに読み込め、という命令を実行するとどうなるか。
Aには、[$00:0031]から$12が読み込まれる。
最初の数値$1234から、上2桁の$12だけ読み込んだので、実質上と同じように「256で割る」計算をしたのと同じである。

ということからもわかる通り、現在8bitモードなのか、それとも16bitモードなのかは、数値の読み書きをする上で重要である。
8bitモードの時は、レジスタに4桁の値が入っていても、下2桁だけで計算をする、ということも覚えておくこと。

論理積と「数値の取り出し」

プログラミング関係の表記及び解説の、ビット演算で説明したことの繰り返しになるが、これも頻出な計算方法なので紹介しておく。

ダメージ量計算には、敵や味方のステータスが必要になる。
それら各種データは、スーパーファミコンのカセット内部からスーパーファミコン本体のメモリに読み込まれているのだが、丁寧に「ローキックの属性は足技で、最大2ヒット」などと細かい説明が書かれている訳ではない。
ひとつの技につき、16進数で50文字分、つまり25バイト分のデータしか書かれていないのである。
各技には番号が振られていて、その番号の順に、25バイトずつデータがずらずらと並んでいる。
このコンテンツでは、便宜上、1バイト(16進数2桁)ずつ区切って、「技データ0」から「技データ24」と名前を付けている。
例えば技データ17は、1桁目の数値が「技が発動するまでの待機時間」で、2桁目の数値が「技LV」となっている。
ダメージ量計算に必要な、「技LV」だけを取り出したい場合、

  1. 技データ17をアキュムレータ(A)にロードする
  2. ロードした値の下1桁だけ取り出す

という、2段階の作業が必要になる。
この時に役立つのが「論理積」である。
ビット演算における「論理積」とは、2個の数値を2進数にし、各桁の数値を比較して、共通して「1」なら「1」に、それ以外だったら「0」にする、という計算のことである。

実際に、「忍法矢車草」の技データ17「$2A」から、「技LV」を取り出すことを考えてみる。
$2Aは2進数で「0010 1010」である。
(プログラマー電卓があれば変換が楽。Windowsパソコン付属の電卓でも可能)
2進数の上4桁(4ビット)が、16進数での上1桁の部分であり、2進数の下4桁(4ビット)が、16進数での下1桁の部分である。
技LVのみ、つまり16進数の下1桁を取り出したいのであれば、2進数の下4桁がすべて「1」である、「0000 1111」との論理積を取れば良い。
0010 1010」と「0000 1111」の論理積を取ると、上4ビット分がすべて0、下4ビット分はそのままの、「0000 1010」となる。
これを16進数に戻すと、「$2A」と「$0F」の論理積をとって「$0A」になった、という計算になる。

なお、「$0A」は10進数の10だから、「忍法矢車草」の技LVは10である。
また、上の桁の「$20」は「技が発動するまでの待機時間」なので、「忍法矢車草」の待機時間は10進数で32となる。

というのが「論理積」の使い方である。
結局のところ、特定の桁を取り出したい時、16進数なら、取り出したい桁はF、それ以外は0の2進数で論理積を取れば良い、というのが結論になる。
アキュムレータと指定の値で論理積を取るニーモニックは「AND」なので、「AND #$0F」という命令が出てきたら、「Aに入っている数値の下1桁だけ取り出したいのだな」とわかる。

なお、技データの中には、2進数にした時に上2ビット分だけ取り出して計算する値なども存在する。
そういう時は「1100 0000」との論理積を取れば良いことになる。
とりあえず、「論理積を取っている時は、何かの数値を取り出したいのだろう」と見当がつくということである(他の場合ももちろんある)。

論理積で取り出した値で、何かのフラグが立つかどうか(ゼロフラグなど)で分岐判定をすることもある。
BIT」というニーモニックも、アキュムレータと指定の値で論理積を取る命令なのだが、こちらの命令は計算結果をアキュムレータに書き込まず、フラグ変化だけ書き込む。例えば論理積を取ってゼロフラグが立つか確認したいが、アキュムレータの値はそのままにしておきたい、という場合はBITを使う。

用語集

その他、当コンテンツでよく使う用語について記しておく。

サブルーチン

プログラミング用語における「サブルーチン」について説明する。
この用語は、65C816やアセンブリ言語だけではなく、他のプログラミング言語でも使われる一般的な用語なので、知っている方は飛ばして構わない。

プログラム内におけるひとまとまりの計算手順のことを、プログラミング用語で「Subroutine/サブルーチン」という。
先程の、テレポートの話であれば、

  1. 乱数 - 128」を計算する
    (実際には2進数の計算なので、乱数は「00000000~11111111」であり、128は2進数で「10000000」である)
  2. キャリーフラグによって条件分岐させる
    • キャリーフラグが立ったらテレポート成功で逃げられる
    • キャリーフラグが立たない場合はテレポートコマンド失敗時の飛び先計算に進む

この一連の手順(プログラム)は、「テレポート」コマンドを選ぶ度に毎回実行される。
「テレポートの成功・失敗判断用サブルーチン」という言い方ができる。
また、このテレポート判断用サブルーチンの前には、乱数を生成する計算をしている。
乱数生成用の一連の手順もまたサブルーチンであり、いわば「乱数生成用サブルーチン」である。
本作ではテレポートだけではなく、籐兵衛のアイテム改造が成功するか失敗するか、また、2~3種類から改造先が選ばれる時に何が選ばれるか、など、色々な場面で乱数が必要とされ、その都度「乱数生成用サブルーチン」を実行している。

というような形で「サブルーチン」という言葉をよく使って説明をするので、覚えておいていただきたい。

フレーム

フレームレート - Wikipedia

「フレーム」(frame)という英単語自体には色々な意味があるが、当コンテンツにおいては「フレームレート」の意味で使っている。
ゲームや映像において、「フレームレート」とは、1秒あたり画面に何枚の絵を表示できるか、その枚数を示すと考えていただいて良い。
単位は「Frames Per Second」、頭文字を取って「FPS」である。
ゲーム好きなら、「ゲーム画面の滑らかさの単位」という意味で「FPS」を捉えている人が多いだろうが、概ね、それで正解である。
スーパーファミコンは、60FPSでプログラムを処理し、画面の表示ができる。
つまり、1秒あたり60枚の絵を表示可能である。
このため、1/60秒を単位として「1フレーム」という言い方をする。

人間からすれば、1フレームというのはほんの一瞬である。
だが、本作においては(というか、他のスーパーファミコンのゲームでもできるが)、実はその1フレームの間に乱数を生成するサブルーチンを1周回して、1フレームに1個、乱数を作っている。
幕末編や西部編だったら、同時に「1分間隔で鐘を鳴らす」というタイマー処理もやっているし、背景やキャラクターといった画像の処理もして、BGMを流し、場面に合ったサウンドエフェクトも鳴らしている。中世編や最終編はランダムエンカウント関係の計算もしている。
それくらいの処理能力を持っているのがスーパーファミコンというゲームハードであり、当時のプログラマーが工夫した様々なプログラムが動いていたのである。

※厳密にはスーパーファミコンのフレームレートは59.94FPSだそうだが、当コンテンツではとりあえず60FPSということでこの先も話を進める。
詳細は上のWikipediaの説明、及び専門書などでご確認いただきたい。

なお、リメイク版の「ライブアライブ」は、Nintendo Switch版が30FPS、PlayStation4版が60FPS、Steam版はプレイするパソコンなどのスペックにより60FPS以上に上げることも可能、となっている。
ただしリメイク版の時代においては、スーパーファミコンの時代とは異なり、グラフィック処理において3D描画など桁違いの性能を必要とするし、そもそも表示する画面のサイズ(解像度)や色数などの基本スペックがまったく異なるので、単純にFPSだけで処理能力の比較はできない。

この「フレーム」という単位も、本作のプログラムを説明する上で時折登場するので、用語として説明した次第である。
例えば、本作には西部編の罠仕掛けなど、時間制限イベントが存在するが、この時にどのように「鐘が鳴るまでの1分間」を測定しているのかというと、「1フレームでカウントが1減るカウントダウンタイマー」を使っている。
幕末編の1分間隔に鳴る鐘や、原始編で荒野に追い出された時に時間経過でフィールドが暗くなっていくのも、この「1フレームでカウントが1減るカウントダウンタイマー」を利用したイベントである。

余談だが、1/60秒、1フレームの間にスーパーファミコンはどれくらいの処理ができるのか。
上で、

LDA #$01

というプログラム(1回分のニーモニック)を紹介した。
実際には、

LDA #$01
STA $22
STZ $23
LDA #$08
STA $08
STZ $20
LDA $7ECE00,x
JSR $20CD
……

というように、ニーモニックがひたすら続いていく。
1回のニーモニックの処理時間は内容により少しずつ違うので厳密に何回できるのかは明言できないが、「ライブ・ア・ライブ」のプログラムでいえば、1フレームの間にニーモニックを10,000回くらい処理してしまう。
1秒だと60倍だから、600,000回くらい
コンピュータに慣れ親しんでいないと、とんでもない回数の計算をこなしているように見える。
だがこれでも1980~90年代の技術であり、リメイク版が発売された2020年代のゲーム機は当然、計算回数が桁違いに増加している(らしい)。
扱える数値の幅も、スーパーファミコンでは最大16ビットだったが、Nintendo Switchでは64ビットである。
詳しいことを知りたいのなら「クロック周波数」などで調べてみると良い。



このページをシェアする

上へ