勝手移植 ドルアーガの塔

アーケード版ドルアーガの塔を 80SR に勝手移植してみました。 まずは動画をご覧ください。


いかがでしょうか?もう少し処理落ちを軽減したかったですが、 高速化ネタがだいぶ尽きてきたので、これで完成としました。

以下、長々と解説しますが、 どちらかと言うと私自身のための備忘録で、 あまり参考にならないかもしれません。




制作するにあたって、いくつか決めていたことがあります。



アーケード版エミュレータは大昔に作ったのですが、 解析はサウンドプログラムしかしていなかったので、 今回はメインプログラムも解析しました(ドルアーガの塔 解析情報を参照)。

解析は 2024 年 4 月から 2 ヶ月ほどで完了しましたが、それから 2 ヶ月ほどは手付かずでした。ちなみに何をしていたかというと、 マッピーの解析とかしていました(こちらは移植の予定はありません)。

初めは X680x0 に移植するつもりだったのですが、ふと 80SR 版を作りたくなり、エミュレータをいじって 8 色表示や 12fps にしてプレイしながら、方針を固めていました。

実は大昔にも一度(目コピで)移植しようとしていて、 各種データを作成していたのですが、いつのまにか放置していました。 理由はすっかり忘れましたが、何か他のことに興味が移ったのかと思います。 わりと飽きっぽかったので(笑)。




このゲームは特に曲が大好きなこともあり、 まずサウンドの移植から始めました。

80SR への移植の前に X680x0 用として YM2151(FM 音源 8ch)で再現していたので、音色はそのまま利用しています。

参考動画:ドルアーガの塔 YM2151版比較

音色の作り方については、手作業で似せるのは非常に難しいので、FM 音源エミュレータを利用して、

  1. FB/ALG、MULx4、TLx4 を乱数で決定、他は固定
    ※ALG=Algorithm、Connection とも
  2. FM 音源エミュレータで生成した波形を FFT にかけて周波数成分に変換し、オリジナル(波形メモリ)のものと比較して、 良ければ更新&保存
  3. どれか 1 つのパラメータを動かし、2. を繰り返して最善を探す
  4. 他のパラメータに対しても同様に動かしてみる
  5. 改善されなくなったら 1. に戻る

といった手順で作成しました。実際には第 10 候補まで保存し、低音から高音まで聴き比べたり、 曲を演奏してみて選択しました。

参考までに音色データソースを載せておきます。 ちなみに ALG は 0~3 しか使用していません。4~7 はボリューム調整で複数の TL を操作する必要があるので。

	; TL[4],FB/ALG,DT/MUL[1,3,2,4],TL[1,3,2]
SND_FM_PARAMS:
	DB $04,$30,$02,$01,$01,$01,$21,$1E,$1E
	DB $09,$03,$01,$08,$01,$07,$1C,$21,$21
	DB $07,$0B,$04,$01,$03,$01,$28,$38,$2A
	DB $07,$32,$01,$04,$07,$01,$10,$2C,$34
	DB $01,$33,$01,$01,$03,$01,$11,$27,$25
	DB $08,$18,$04,$02,$01,$01,$3A,$22,$1A
	DB $06,$08,$0A,$0E,$0C,$01,$3D,$3A,$00
	DB $06,$3A,$0C,$01,$01,$04,$2C,$2A,$14

音色データが一通りあれば、YM2151 なら(アーケード版と同様に)8ch あるのでそのまま演奏できたのですが、80SR の YM2203 は FM 音源 3ch+SSG 音源 3ch で、発音数が足りない上に音色を変えられるのは 3ch だけです。

それぞれの曲に対して、 各チャンネルを矩形波に割り当てたり削ったりして試行錯誤しました。 これには以前に作ったプレイヤーを流用しました。

NSounds

次に、アーケード版エミュレータでサウンド CPU をエミュレートする代わりに、サウンドルーチンを C++ で書き直し、少しずつ改造していきました。直接 Z80 アセンブリで書き直すのに比べて二度手間にはなりますが、 試行錯誤やデバッグが非常に楽になります。

シーケンスデータは、原作のままだと多少メモリ効率が悪いので、 データフォーマットを変えました。


データ 内容
$00~$7E 音符(bit6-4=オクターブ、bit3-0=音階C~B)
$7F 休符
$80~$CF 音長 1~80
$D0~$EF エンベロープ(兼音量)番号
$F0~$F7 FM:波形番号、SSG:減衰量
$FB,ofstm 後方ジャンプ
$FC,n,ofstp n 回目に前方分岐
$FD,n,ofstm n 回後方リピート
$FE,len 音長(81 以上)
$FF 全チャンネル終了

圧縮後で合計約 3KB あり、必要に応じて展開しています。

なお、バイナリデータだと調整しにくいので、独自の MML 形式で記述したものを変換しています。たとえばラウンド・スタート曲の 1 パート目は、

ch 0 0 0
	@4@e11o2a+16@e13o3f8@e11a+24g+24@e13g6f6d+6g6@e11f18@e13d+6@e11f48d+16
	@e13f8@e11o4c24o3a+24g+12g12f12d+12f72r24
	[end]
endch

こうなっています。

サウンドルーチンでは、FM 音源への出力パラメータをバッファに溜めておいて、 次の割り込み開始時にまとめて出力しています。 これは処理上の都合もありますが、 微妙な発音タイミングのバラつきを抑えることにもなります。




ここで 80SR のグラフィックアクセス機能について解説しておきます。

80SR の GVRAM は 3 プレーンをそれぞれ独立アクセスする以外に、ALU(Arithmetic Logic Unit)という機能があります(88SR 以降も同じ)。今回はこのうち「3 プレーン同時アクセス」と「論理演算」のモードを使っています。

3 プレーン同時アクセスモードでは、3 プレーンそれぞれに 1 バイトのバッファがあり、GVRAM の読み書きはバッファとの転送になります。

GVRAM を読むと 3 プレーン全ての値がバッファに蓄えられ、GVRAM へ書き込む(値は任意)とバッファの値が 3 プレーン全てに書き込まれます。

これを利用すると、スクロールは 1 プレーン分の転送作業で済みます。

ALU exsample 1

論理演算モードは、GVRAM に書き込んだ際に '1' のビットが 3 プレーンそれぞれに設定した演算で処理されます。'0' のビットは変化しません。

演算にはリセット/セット/反転/NOP(何もしない)の 4 種類があります。たとえば B プレーンをリセット、R プレーンと G プレーンをセットにすると、'1' を書き込んだ部分が黄色になります。

ALU exsample 2

キャラ描画は、これら 2 つのモードを組み合わせて処理しています。 詳しい解説は後述します。

ところで、論理演算モードを使って文字描画をする場合、 すでに何かが描かれている場合は消す必要があり('0' のビットには何もしないため)、2 回描画することになります。ALU のちょっと不便なところです。

しかしパレット 1・2・4 に限っては、ALU を使わずに 1 つのプレーンに描くだけで済むので、 点数や残りタイム表示を高速化するために、 パレットを以下のように変更しています(1・2・4 以外の割り当ては気にしないでください)。


0 1 2 3 4 5 6 7
               

これで白・赤・黄の文字描画は高速に行えます。 ただし他の色で何か描かれている場合は、 やはり消す必要もあります(残りタイムが白から赤に変わるときなど)。




画像データをいかに圧縮するかは、常に悩みの種でした。

特にタイトルロゴは工夫次第で縮められそうなので、 いろいろ試行錯誤していました。

title logo

全部で 6 色使われているのですが、4 色なら 1 ドットあたり 2 ビットで表せるので、「DRUAGA」の文字のダークグレーを黒に置き換えて 5 色に減らし、さらに「THE TOWER OF」と「DRUAGA」の間で上下に 2 分割して 4 色ずつに収めました。圧縮して約 1.3KB になっています。

そしてタイトル画面に移行時、352x144 ドット 8 色の画像データに変換しています。変換後は約 19KB もメモリを食っていますが、 タイトル画面ではキャラ描画などは無いこともあって足りています。

なお、右上の小さいロゴはディジタル 8 色データを別途作成したのですが、 ずっと消さないので最初に描画した後データを捨てています。

さて、タイトル画面では、 タイトルロゴ→ランキング→タイトルロゴ→ストーリー、 と画面が左右にスクロールします。

スクロール処理はプレイ中にもありますが、 このとき「範囲内に入ってくる部分」を、全体をずらした後で描画すると、 描き換えが遅れて見苦しくなります。

そこで、GVRAM の各プレーン 16KB(16384 バイト)の最後の方に余っている 384 バイトを利用します。このうち 200 バイトに、各ラインに入ってくる画像を一通り描画してから、 スクロール処理を開始しています。 これでラインごとにスクロール処理が終わります。

scroll exsample 1

プレイ中以外の画面としては、オプション、エンディング、GAME OVER、ネーム入力がありますが、これらは文字しか描画しなくて簡単なので、 タイトル画面の次に一通り作りました。




フロア開始時に迷路を作成しますが、この処理にアーケード版では 20 フレームほどかかっています。さすがに遅すぎるので改良したところ、3 フレーム程度で済むようになりました。

補足すると、解析情報の迷路作成の JavaScript ソースでいうところの m_pos_list を、ループのたびに全初期化(「通過した柱リスト」のところ) しているのが無駄で、必要最低限にすることで劇的に速くなります。

迷路の画像データは、GVRAM 最後の空きの(各プレーン)384 バイトを前半 128 バイトと後半 256 バイトに分け、起動時に前半に詰め込んでいます。 ちなみに後半は前述のスクロール処理(200 バイト)とキャラ描画で兼用しています。

128 バイト中 39 バイトは、下の画像になっています。

maze image 1

この組み合わせだけで迷路は全て描画できますが、残りの 89 バイトはよく使うパターンを並べて高速化に利用しています。

迷路の描画ルーチンは、縦長の「8x200 ドット」を描画するルーチンと、キャラ描画用の「32x16~64x32 ドット」を描画するルーチンがあり、3 プレーン同時転送モードでコツコツ描画しています。 壁の有無などで場合分けが多く、だいぶ複雑な処理になっているので、 まだ高速化や省メモリ化の余地はありそうです。




アーケード版のスプライトは 16x16 ドット 16 色で 128 パターン定義されていて 16KB あります。あらかじめディジタル 8 色データに変換しておきたいところですが、 パレット切り替えがあるのでデータ量が膨大になってしまいます。

仕方ないので基本的に元データのままで持っていますが、 多少の工夫はしています。

sprite exsample 1 sprite exsample 2

宝箱の上下の余白は削っています。 また、ナイトはアニメーションしても上部は変化が無いので、 上下に分けて共通部分はまとめています。

これらの画像データに付加情報をいくつか付けて、全部で約 11KB、圧縮して約 6.5KB になっています。

画像データとパレットデータを元に、フロア開始時にデータ変換しています。3 プレーン以外に重ね合わせのためのマスクパターンも必要なので、計 4 プレーンになります。

render exsample 1

これはメイジの描画例で、迷路→マスク→プレーン 0~2 の順に、ALU を利用して描画しています。 スライムや呪文のように色数の少ないものでは ALU の設定を変えたりしていますが、基本は同じです。

画像の変換は各階で使用するキャラのみに限定しています。 今のところ一番使用メモリが多いのは、ナイトが総出演する 45 階です。ナイトは複数のパーツ(本体+剣、キャラによっては盾も) で構成されている上に、アニメパターンが多いためです。

次に多いのが 26 階で、こちらは主にクオックスとハイパーナイトによるものです。 ドラゴンにはブレスもあるので、かなり圧迫します。

敵キャラだけでなく、ギルもかなりのメモリを使用します。 宝箱を取る前と取った後のパーツを用意しておくためです。

59 階ではドルアーガが出るので 26 階よりも敵キャラの種類は多いですが、ギルの装備は変化しないので、 ハイパーガントレットが出る 26 階よりも使用メモリは少なくなっています。

なお、アーケード版で画面下部に表示している宝物(と鍵) はスプライトではなく BG で、8x8 ドット 4 色が 2x2 個並べられています。こちらは似た画像が多いので、ディジタル 8 色の色別パーツを組み合わせるようなデータ形式にしました。

詳しい説明は省略しますが、全部で約 1.6KB、圧縮して約 1.4KB のデータになっています。 階数に関係なくいろいろな宝物を表示しなければならないので、 キャラ画像と違って常に全画像を用意しています。

treasure




まずグラフィック/サウンドのデータ領域を初期化し、 ラウンド・スタート曲を登録&再生します。

次に画面の大部分を消すのですが、 少しだけ時間がかかって描き換えが見えるので、 テキスト画面に黒四角を並べておいて ON することで隠しています。 ここでは黒四角を白い「×」に置き換えてみました。

floor start 1

グラフィック画面を中央付近だけ消去し、「GET READY~」と残りギル数を表示したところまでです。

この後、隠した部分を消去してから、テキスト画面を OFF にします。80SR ではテキスト画面を ON にすると 3 割ほど速度低下するので、なるべく ON にしたくないのです。ちなみに 88SR ではテキスト画面専用 RAM が追加されて速度低下しなくなっています。

次に迷路を作成後、今の階で使用するキャラデータを変換します。 この変換処理はかなり重く、長い時で 2 秒ほどかかっています。 プログラムも非常に複雑で、もう二度と見直したくない処理です(笑)。

続いて、使用するサウンドの登録などをしてから、再びテキスト画面を ON にして、隠している部分の「TIME 20000」、宝物、迷路を描画します。

floor start 2

この状態で少し待った後、「GET READY~」の部分もテキスト画面の黒い四角で隠し、 そこにも迷路を描画後、テキスト画面を OFF にします。

さらに少し待った後、 ギルを点滅→表示してプレイ開始となります。




基本的な流れとしては以下の通りです。

  1. キー入力チェック
  2. ギル操作関連
  3. 鍵/宝物(画面右)の表示更新
  4. 宝箱出現チェック
  5. 赤タイム時のウィスプ出現処理
  6. ギル以外のキャラ処理
  7. スクロールが必要な場合は、スクロールして 1.~6. をもう一度
  8. キャラ描画
  9. 速度調整後 1. に戻る

面クリアやミスなどで別処理に移ります。

プログラムやワークエリアに関しては、 アーケード版を理解しつつ作り直してあるので、 かなり違うものになっています。

基本方針に書いた通り、基本 30fps 動作なので、1 フレームあたりの移動量や与ダメージなどは 2 倍にしています。また、スクロール時は描画が 15fps になるので、点滅などのアニメーションをそれぞれで調整しています。

敵キャラの行動処理は、ドラゴンが圧倒的に複雑でした。 特にブレスを吐く条件が微妙で、 正直なところ合っているか自信がありません。

他のキャラは見た目移植でもほぼ同じものが作れると思いますが、 ドラゴンだけは無理ですね。

キャラ描画では、GVRAM 最後の空きの(各プレーン)256 バイトを、64x32 ドットの仮想画面として利用しています。 いったんここで重ね合わせ処理をしてから転送することで、 チラツキが発生しないようにしています。

このとき、たとえばブルーウィルオーウィスプが右へ移動したとして、

render exsample 2


このように描画範囲を移動前まで含めることで、 消去を兼ねています。

また、キャラが多数重なって、

render exsample 3


この例だと 80x16 ドットあるのですが、こういう場合は 64x32 ドット以下の範囲で複数回に分けて描画しています。 同じキャラを何度も描画することになるので、 こういうケースではかなり重くなります。

高速化については、再描画の必要がない場合 (扉や鍵、停止しているスライムなど)は描画しないとか、 重なり判定の工夫などをしていますが、 まだ処理落ちの激しい階があるのが心残りです。

ちなみにスクロール処理では、ギルの周辺だけ転送していません。 スクロールは時間がかかるので、 ギルがスクロールに流されてガクガクして見えるのを防いでいます。

ここで一つ反省点になりますが、 スクロールは上から下の順に処理しており、 ギルが上の方にいるほど描き直すまでのタイムラグが大きくなるので、 ギルが上半分にいる場合は、 スクロールを下から上の順で処理するべきかもしれません。




ここではハードウェアの違いによるものについては触れませんが、 まず仕様変更した点を挙げます。

次に、バグ関連で変更したものを挙げます。

他の(既知の)バグは残してあります。




Z80 あれこれ

Z80 プログラミングの技について書こうかと思いましたが、 特筆するようなことはありませんでした(笑)。

というのもなんなので、 わりとありがちなことになりますが、いくつか書いておきます。



メモリマップ

まず 80SR と 88SR のメモリ配置の違いを、今回使用している範囲だけ図解します。

memory map 1

このように GVRAM のアドレスが大きく違うのですが、それ以外の配置を考えるのも面倒なので、 展開後の画像データと割り込み関連(サウンド)は、前半の $0000~$7FFF に置くことにしました。

なお、88SR では $F000~$FFFF にメイン RAM とテキスト RAM がありますが、今回はテキスト RAM 側しか使用していません。

次の図は共通のメモリ配置です(アドレスはおおよその値です)。 空白部分は状況によって変わります。

memory map 2

空白部分は、タイトル画面やオプション画面では次のようになっています。 エンディング、GAME OVER、ネーム入力でもほぼ同じです。

memory map 3

フロア開始時、プレイ中には次のように変わります(フロア開始時は $3200~にサウンドや画像データを展開していきます)。

memory map 4




おわりに

80SR 版に手を付け始めたのが 2024 年 8 月中旬、完成が 2025 年 5 月 23 日ですから、結局 9 ヶ月ほどかかってしまいましたね。

1 月の時点で未実装な部分はほとんど無かったのですが、 残りの部分を詰め込むためにメモリ削減、 そこから高速化のためにさらにメモリ削減と、 なかなか思うように進みませんでした。

最終的なサイズは約 34KB で、テープからのロード時間は 600baud なら 10 分くらいでしょうか。

最初はメモリが足りるか疑問でしたが、 なんとか一通り入れられて満足しています。 処理落ちは結構気になりますが、総合的には自己採点で 95 点くらいだと思っています。

苦労しつつも Z80 プログラミングは何かと楽しくて、6 年ほど前に公開した「Z80 で円周率を計算」で「Z80 でプログラミングするのは、さすがにこれが最後かも」 なんて言いましたが、全然そんなことなかったです(笑)。

次は何を作ろうかな……。


inserted by FC2 system