ユーザ用ツール

サイト用ツール


フルーツフィールド_for_pc-6001mkii

フルーツフィールド for PC-6001mkII

はじめに

フルーツフィールドは、REMくんを操作してフルーツを集めるパズルゲームです。 画面内にはREMくんの行く手を阻むブロックがありますので、動かしたり、壊したりしながら、フルーツをすべて集めてください。 画面内のフルーツをすべて集めればクリアです。

オリジナルは、工学社のPiOという雑誌の1986年9月号に掲載されました。 「8機種1)そろってフルーツフィールド」と題して、同時に8機種のダンプリストが掲載されました。

ただ、その中にPC-6001シリーズは含まれていませんでした。 当時、PC-8801mkII所有だった私は、暇なことに、PC-8801版もPC-8001版も入力し、遊んでいましたが、PC-6001シリーズを保有していたわけでもなかったので、対応機種から漏れていることに気づきもしませんでした。

しかし、今回、PC-6001mkIIの実機を入手したので、PC-6001の勉強もかねて、移植してみることにしたのでした。

ルール

画面内には矢印の刻まれたブロックがあります。 ブロックはREMくんが押すことで矢印の方向に障害物にぶつかるまで進みます。 REMくんは向いている方向にある隣接したブロックを押すことができます。 押すときは、矢印の先以外の三方向から押すことができます。 ブロックが障害物にぶつかった状態で、矢印の後ろ側から押すことでそのブロックを破壊することができます。 但し、矢印同士が向かい合った状態にあるときは壁同様、どちらのブロックも破壊も移動もできなくなあります。 面内のフルーツをすべて集めればステージクリアですが、どうしてもとけなくなった場合はギブアップしてそのステージを最初からやり直すことができます。 100ステージすべてをクリアするとゲームクリアです。

動作環境

  • PC-6001mkII以降
  • モード5/ページ3

PC-6001mkII実機、および、PC-6001VW、PC-6001VXで動作確認しています。 PC-6001mkII/6601互換BASIC0.31での動作も確認しています。

ゲームのロードおよび実行

PC-6001VWやPC-6001VXのようなエミュレータの場合 ff.p6をロード用のテープイメージとしてセットして、モード5 (N60m-BASIC/N66-BASIC)、ページ数3で起動してください。 実機の場合は、loader.wavとff6001.wavをテープに順に保存するか、或いは、PCで順次再生されるようにしてくださいい。 BASICが起動したら、

cload

と入力します。

Found:loader

と出て、OKと表示されたら、続けて

run

と入力してください。

Program Loading...

と表示されたら、PCから実機へロードする場合はff6001.wavを再生してください。 テープに連続して録音してある場合、およびエミュレータでff.p6を使用している場合は、自動的に続きをロードし、タイトル画面が表示されます。

操作

REMくんはカーソルキーで上下左右に操作できます。 スペースキーを押すと、向いている方向で隣接しているブロックを押します。 シフトキーを押すとギブアップします。

押すことができます。 ブロックを壊せます。 ブロックは壊せません。

STOPキーでBGMをON/OFFします。 BGMはデフォルトではOFFで、メニューに戻るとOFFに戻されます。

コンストラクションモード

メインメニューで、1を選ぶとコンストラクションモードになります。 左上でカーソルが点滅しているので、カーソルキーで移動してください。 0-7を押すとカーソルの位置に対応するオブジェクトを置きます。

0. 空白 1. 壁 2. REMくん 3. フルーツ 4. 左 5. 右 6. 上 7. 下

REMくんを同時に二か所以上に配置はできません。 HOME/CLRキーを押すとステージを空白でクリアします。 DELキーを押すとカーソルが一つ戻ります。 オブジェクトを置くとカーソルが右へ進みますがこれがいやな場合は、スペースキーを押すと進まなくなります。 もう一度スペースを押すと再び進むようになります。 9を押すとテストプレイすることができます。 リターンキーを押すとステージを保存してタイトル画面に戻ります。

面データのセーブとロード

メインメニューから2を選ぶと作成した面データをテープに保存できます。 保存したいステージの範囲を指定できます。

メニューから3を選ぶとロードできます。 保存されたデータにはどのステージであるのかが記録されています2)ので、ステージ番号の指定などの必要はありません。

ゲームの終了

メインメニューから4を選ぶとゲームを終了してBASICに戻ります。 再度起動したい場合はロードからやり直すか、

EXEC &H9000

と入力してください。

ダウンロード

エミュレータ用のp6ファイルと、実機用のwavファイル(逆位相版も同梱)をまとめたものをこちらに置きます。

改版履歴

  • 2020/11 初版
  • 2020/12/09 BGMを鳴らすようにした。
  • 2020/12/10 ゲーム中の効果音もタイマー割込み側でならすように変更した。

技術資料

メモリーマップ

アドレス
プログラムエリア9000H-9EFFH
ワークエリア(変数)9F00H-9FFFH
キャラクタデータA000H-A45FH
タイトルデータA460H-A77FH
フォントデータA780H-A87FH
デモ画面用マップA880H-A8DFH
デモ操作データA8E0H-A96FH
プレイ中マップA970H-AA0FH
メッセージ文字列AA10H-AEC8H
BGMデータAEC9H-AF08H
マップデータ(80Hx100)B000H-CF3FH
コンストラクション用ワークCF40H-CF8FH

PC-6001mkIIのBIOS/ワークエリア

アドレス備考
アトリビュートエリア4000H-5FFFH
グラフィックエリア6000H-7FFFH
キー入力1061H出力:A(ビットマップ)
ジョイパッド入力1CA6H出力:A(ビットマップ)
キー入力0FBCH出力:A=文字コード
キーバッファクリア1058H
ダイレクトモード0442HBASICへ戻る
モード設定1390H入力:A=モード-1
表示ページ13EDH入力:A=ページ-1
使用ページ140CH入力:A=ページ-1
セーブ開始25B7HCMTをONにし、ヘッダD3を10byte出力、ファイル名6byteを出力
1byteセーブ1ACCH入力:A
セーブ終了1B06Hテープを閉じる
ロード開始1A61HCMTをONにする
ヘッダー読み込み2533Hヘッダ及びファイル名を読み込む
1byteロード1A70H出力:A
ロード終了1AAAHテープを閉じる
セーブ用ファイル名エリアFECBH-FED0H6bytes
ロード用ファイル名エリアFED1H-FED6H6bytes
キークリック音FA2DH0:OFF/1:ON

開発環境

フルーツフィールドは、ローダの書き込みにN60m-BASICを利用している以外は、すべてアセンブラで開発されています。 コードエディターはVisual Studio Codeを、アセンブラを含む開発ツール一式は、WSL2の上に展開したUbuntu上に用意しています。 アセンブラはZASMです。 普通に、git clone https://github.com/Megatokio/zasm.git して、ビルドしてあります。 MINGWなどで、Windows版を用意してもいいかと思いますが、WSL2は慣れるともう戻れないですね。 Windowsネイティブのバイナリが必要でないなら、WSL2で十分だし、便利だと思います。

WSL2はWindowsとのファイルの交換が容易なので、ビルドしたコードを即座にWindows上のエミュレータに送り込むことができます。 WSL2側からは /mnt/<ドライブ名>/<パス>で、Windows側からは \\wsl$\Ubuntu\<パス>でそれぞれ、互いのファイル空間にアクセス可能です。

ローダー

  • ヘッダー構造
ヘッダー(16bytes)
D3D3D3D3D3D3D3D3
D3D3 開始 終了 実行

PC-6001にはマシン語モニターがありません。 PC-6001mkII以降では、N60m/N66-BASICでは実装されていますが、シリアル通信機能と共有されているからか、1byteをを6進数0-9A-Fの2文字として保存するため、効率が悪く、あまり使いでがいいとはいえません。

そこで、cload/csave用のヘッダを流用し、ファイル名用の6byteを2byteずつ三つに分け、開始アドレス、終了アドレス、実行開始アドレスを保持し、1byteのデータをそのままの1byteで保持します。

ローダープログラムは読み込んだデータを開始アドレスから順に書き込み、終了アドレスになったら、実行開始アドレスにジャンプします。

ゲーム内のマップデーターの場合は実行開始アドレスは使いませんし読み込んでも分岐しません。

BASICから呼び出されるローダ本体は以下のようなコードになっています。 RST 20HはHLとDEを比較してくれるシステムコールで、結果がゼロフラグとキャリーフラグに反映されます。

       org     0f900h

       call    1a61h
       call    2539h
       ld      hl, (0fed1h)
       ld      de, (0fed3h)
read_next:
       call    1a70h
       ld      (hl),a
       inc     hl
       rst     20h
       jr      nz, read_next
read_done:
       call    1aaah
       ld      hl, (0fed5h)    ; get start addr
       jp      (hl)

       ret

       end

10進数変換(0~187まで)

ゲーム中、面数やフルーツの残数を表示するために、16進数のデータを10進数に変換する必要があります。

結構頻度高く呼ばれるので高速である必要があります。

正攻法、というか最もよく、かつ汎用性高く使われる方法は、10で除算して剰余を得る方法でしょう。

ただ、計算量が多く、ループもあるので、決して高速とはいいがたいです。

面数は00-99、フルーツ数は、フィールドをすべてフルーツで埋め尽くしても160なので、この範囲の数値が変換できれば問題ありません。

Z80にはDAAという命令があるので、これを利用すると0-187の数値を16進数から10進数に変換できます。

DAAは直前の演算によってAレジスタに得られた値を4bitx2けたの十進数に変換します。

16進数2けたの数値(ただし0x00-0xbb)を変換するにあたって、まず値を4bitずつ二つに分けます。 下位1ニブルをDAAで10進化して、保存しておきます。 上位ニブルはそのままでは桁あふれするので、右に4ビットシフトして、DAAで10進化し、これを add a,aで二倍してDAA。これを4回繰り返し、保存しておいた下位ニブルを足してDAAします。

ただし、これでは、100以上になるときにCFがうまく立ちません。 なので、ちょっと演算順序をかえてやります。 これでも188の時にはうまく処理できなくなるのですが。 (だから187以下のみサポート)

       push    hl

       ld      h,a
       and     0fh
       or      a			; clear HF
       daa
       ld      l,a
       ld      a,h
       and     0f0h

       rrca
       rrca
       rrca
       rrca
       daa

       add     a,a
       daa
       add     a,a
       daa
       add     a,a
       daa
       ld      h,a
       add     a,l
       daa
       add     a,h
       daa					; set CF if over 100

       pop     hl
       ret

CLS

BIOSのCLSルーチンはPC-8801/mkIIのそれ同様に遅い上に、呼び出すとSCREENモードを戻してしまうようで具合が悪いです。

なので、自前の画面消去ルーチンを組みます。

最も簡便な方法は、ldirを使って1byteずつクリアする方法でしょう。

ld hl,04000H
ld de,04001H
ld (hl),0
ld bc,40*200*2-1
ldir

ただ、これだとイマイチ速くありません。 そこで、16bitずつデータをストアできるpushを使って消去する方法を実装します。

 ld hl,0
 ld (stack_ptr),sp
 di
 ld sp,04000H + 40 * 200
 ld b, 100
next1:
 push hl
 ...
 push hl      ; 40回繰り返す(80bytes == 2lines)
 djnz next1
 ld sp,06000H + 40 * 200
 ld b, 100
next2:
 push hl
 ...
 push hl      ; 40回繰り返す(80bytes == 2lines)
 djnz next2
 ld sp,(stack_ptr)
 ei

spをいじるので実行期間中は割り込みを禁止します。 画面が下から消えますがよしとします。

モードとページ数

PC-6001mkIIはN60-BASICモードとN60m-BASICモードの二つがあります。 起動メニューの1~4はN60で5がN60mになります。

移植もとにしたPC-8001版は160x100x8色の解像度を持つので、PC-6001mkIIではN60m-BASICでSCREEN 3(160x200x15色)がちょうどいい解像度になります。

このため、フルーツフィールドはモード5でページ数3で起動することになっています。

なお、BASICでSCREEN 3,2,2とやるがごとく、アセンブラでモード変更を真っ先にやってしまうと、?FN Errorを起こして止まってしまいます。先に、使用画面、表示画面を1以外に切り替えておかないといけません。TIPSです。

文字フォント

モード3での文字表示は、8×16ドットになるので、文字がばかでかく、ゲーム中の表示には使いにくいため、最低限のフォントを4×8でデザインし利用しています。

0x20-0x5F の64文字のみを実装してあります。 英小文字はサポートしていません。

フォントは一文字4byteで構成されます。 上位ニブルが奇数ライン、下位ニブルが偶数ラインをそれぞれ表現しています。

表示するときは、指定された色にあわせて、VRAMに直接書き込みます。

フォントバイト位置
上位ニブル+0
x 下位ニブル
xx 上位ニブル+1
x 下位ニブル
x 上位ニブル+2
x 下位ニブル
xxx上位ニブル+3
下位ニブル

開発環境について

コードはアセンブラで記述しています。 アセンブルは、WSL2上にインストールした Ubuntu 20.4LTS上の zasm を利用しています。

$ zasm -uwy --z80 ff6001.asm ff6001.lst ff6001.bin

のようにしています。

エミュレータにロードするために、ヘッダをつけてP6形式にするツールをrubyで書いて使っています。

buf = ARGF.read

head = 0xd3.chr * 16
saddr = 0x9000
eaddr = saddr + buf.bytes.size

head[10] = (saddr & 0xff).chr
head[11] = (saddr >> 8).chr
head[12] = (eaddr & 0xff).chr
head[13] = (eaddr >> 8).chr
head[14] = (saddr & 0xff).chr
head[15] = (saddr >> 8).chr
print "#{saddr.to_s(16)} - #{eaddr.to_s(16)}\n"

open('ff6001.p6', 'w') do |f|
  f.write head
  f.write buf
end

更に、ローダのp6ファイルに、ゲーム本体の p6ファイルを、10byteほどの空白をあけて連結するだけのツールもrubyで書いてあります。

loader = nil
open('loader.p6') do |f|
  loader = f.read
end

pad = "\000"*10
prog = ARGF.read

open('ff.p6','w') do |f|
  f.write loader
  f.write pad
  f.write prog
end

これらが make から呼び出されて一発でロード可能な p6ファイルができるようになっています。

デバッグを迅速に行うために、何度もビルドしてはロードを行うわけですが、WAVファイルにすると3分弱の長さになりますので、短時間でロードとテストができるこの環境でなかったら、移植を完遂はできなかったかもしれません。

マップデータ

マップデータの構造は極めて単純です。 ステージは16×10のサイズで、配置されるオブジェクトは0~7の8種類、3bitでマッピング可能なので、1byteで4bitずつ2マス分のデータを保持します。 従って、1ステージ分のデータは16×10/2 = 80バイトになります。

下はステージ00のデータです。

0000b000  11 11 11 11 11 11 11 11  |........|
0000b008  11 10 11 11 11 11 11 11  |........|
0000b010  11 06 00 76 01 11 00 00  |...v....|
0000b018  11 01 10 11 01 11 01 10  |........|
0000b020  23 50 10 55 35 00 00 43  |#P.U5..C|
0000b028  11 11 10 11 01 11 01 10  |........|
0000b030  11 37 00 45 01 11 00 01  |.7.E....|
0000b038  11 10 11 11 11 11 11 11  |........|
0000b040  11 11 11 11 11 11 11 11  |........|
0000b048  11 11 11 11 11 11 11 11  |........|

プレイ中のマップはAA70H~の160バイトのエリアに1byteに一マスのデータとして展開され利用されます。

また、コンストラクション中のマップはCF40H~の160byteに作成され、RETキーを押すことで、2マスを1バイトにパッキングして、マップデータの当該ステージのエリアに上書きされます。

スクロール

VRAMをスクロールするには、現在のVRAMデータを読み出す必要があります。 しかし、VRAMのある4000H~7FFFHは、読み出す際にはROMへのアクセスになり、VRAMを読み出すにはバンクを切り替えてやらねばなりません。

ポート0F0Hに上位ニブルを 0D0Hにして出力することで、読み出しバンクをVRAMに変更できます。

スクロールはアトリビュートエリア(4000H-5FFFH)を動かした後、グラフィックエリア(6000H-7FFFH)を動かすことになるので、全画面分を一気にやると派手に色ずれを起こしてしまう。

なので、上から8line分(フォント一つ分の高さ)をアトリビュート、グラフィックスともにスクロールして、次の8line分を処理し……と小分けにして、色ずれを目立たないように処理しています。

	push	hl
	push	de
	push	bc

	ld	de,vram0
	ld	(vram0_scr_ptr),de
	ld	de,vram1
	ld	(vram1_scr_ptr),de

	in	a,(0F0H)		; read bank setting
	push	af
	and	0fh			; set 04000h-07fffh to RAM (VRAM)
	or	0d0h
	di
	out	(0F0H),a

	ld	b,24
scroll_up_loop:
	push	bc
	ld	de,(vram0_scr_ptr)
	ld	hl,320
	add	hl,de
	ld	bc,320
	ldir
	ld	(vram0_scr_ptr),de
	ld	de,(vram1_scr_ptr)
	ld	hl,320
	add	hl,de
	ld	bc,320
	ldir
	ld	(vram1_scr_ptr),de
	pop	bc
	djnz	scroll_up_loop
	pop	af
	out	(0F0H),a		; restore bank
	ei

	ld	c,4
	call	_cls			; clear botton 1 line
	
	pop	bc
	pop	de
	pop	hl
	ret

BGM

BGMはゲームのループ中にPSGのレジスタを叩いて出力しています。 チャンネルAで効果音を、チャンネルBでBGMを鳴らしています。 ブロックを動かしたり消したりするとBGMが飛んだようになりますが、PC-6001mkIIの全力です。3)

BGMはX1用のデータを、X1用の演奏ルーチンを移植したもので鳴らしています。 X1とPC-6001は同じPSGチップAY-3-8910を搭載しているので、基本的な操作は同じです。

またPC-6001とX1はAY-3-8910のレジスタのマッピングが全く同じになっているため、基本的に、I/Oアドレスだけ調整してやれば、そのまま音が鳴ります。

PC-6001がA0Hでレジスタの選択を、A1Hにデータの出力を行うのに対して、X1は1CxxHでレジスタの選択を、1BxxHにデータの出力を行っています。 X1はZ80のOUT (C),rがBCレジスタペアで16bit幅のアドレス空間を持てることを積極的に利用していて、PSGへのアクセスも16bit幅のアドレスを使っていますが、下位8bitは Don't Careなので、気にする必要はありません。

play_bgm:
    ld      a,12        ; set envelope 6911 ... 0.9s
    out     (0a0h),a    ; --> 1affh
    ld      a,1ah
    out     (0a1h),a
    ld      a,11
    out     (0a0h),a
    ld      a,0ffh
    out     (0a1h),a
    ld      a,9         ; set channel B to envelope mode
    out     (0a0h),a    ; (10H)
    ld      a,10h
    out     (0a1h),a
    ld      a,3         ; Clear Channel B CT
    out     (0a0h),a
    xor     a
    out     (0a1h),a
    push    hl
    ld      hl, (bgm_ptr)
    ld      a,(hl)      ; Read next data
    add     a,80h
    jr      nc,skip01   ; MSB to set CT B == 1
    push    bc
    ld      c,a
    ld      a,3
    out     (0a0h),a
    ld      a,1
    out     (0a1h),a
    ld      a,c
    pop	    bc
skip01:
    cp      7fh         ; rewind if 0xff
    jr      z, rewind
    cp      80h
    jr      z, noplay   ; rest if 00
    push    bc
    ld      c,a
    ld      a,2
    out     (0a0h),a    ; set FT B
    ld      a,c
    out     (0a1h),a
    pop	    bc
    ld      a,7         ; channel TONE A/B only
    out     (0a0h),a
    ld      a,0fch
    out     (0a1h),a
    ld      a,13        ; Envelope mode to 00xx
    out     (0a0h),a
    xor     a
    out     (0a1h),a
noplay:
    inc     hl
    ld      (bgm_ptr),hl
    pop	    hl
    ret
rewind:
    ld      hl, bgm_dat
    ld      (bgm_ptr), hl
    pop	    hl
    ret

タイマー割込み

BGM対応の最初の版では、ゲームルーチンの中からBGMの再生ルーチンをcallしていましたが、当然、再生が安定しませんでした。 そこで、タイマー割込みを使って、BGMを再生する方法に切り替えました。

タイマー割込みは1/512sごとに発生するので、ここで、適当な間隔でPSGをたたいてやれば安定して再生が行えるはずです。

タイマー割込みに関係するのは以下のリソースになります。

リソース内容
B0HI/Oポート 最下位ビットがタイマー割込みの enable/disable。0でenable
FA27HポートB0Hに出力されている値
FA06H~FA07Hタイマー割込みベクタ。デフォルトは0F74H

割込みベクタは、標準ではPLAY文やカーソルの点滅などの処理をしているようです。 最初、ベクタを保存して、BASICに戻るときに保存した値を戻していたのですが、リセットなど予期せぬ処理の後だと、ベクタが初期化されないでゲーム内のハンドラーをポイントしたままになったりしていたので、構わず 0F74Hを書き戻すようにしました。 タイマー割込みをフックするような何かが先にいると具合が悪いですが、まあ、そこまで考える必要もないでしょう、多分。 また、タイマー割込みもBASICが動いている状態では0F74Hを使った割込みが有効な状態なので、ゲームから復帰するときもタイマー割込みを有効にして戻っています。

BGMはエンベロープが0.886秒ほどで0になるような形状なので、450回に一回くらいPSGをたたけばいいかと思ったのですが、実際に動かしたら64回ほどでたたく必要がありました。どこかで何かの計算を間違えたのかな?

とりあえず、BGMの再生が安定しました。 というか、ゲームでBGM鳴らす場合はみんなこんな風な処理をしているものなんですよね?

競合

ゲーム中、ビープ音的な音を鳴らすわけです。ブロックを消したときとかフルーツを取った時ですが。 元々、BGMがなかった時分には、直接そこでPSGをたたいて音を出していたのですが、タイマー割込みで、BGMを鳴らしだすと、どうやら、PSGを触っている最中に割り込まれて、おかしな動作4)をするようになりました。

さて困った。

PSGの操作は、ポートA0Hにレジスタ番号を出力して選択したレジスタに対して、ポートA1H経由で値を出力するという手順で行います。 このレジスタの選択からデータの出力までの間に割込みが入って、向こう側でもPSGをたたいてくれると、こちらで選択したはずのレジスタじゃないものが選ばれた状態で帰ってくる可能性があります。 これが消音 – ボリューム0を出力する処理 – の最中に来たら、意図しないレジスタに0を出力してしまい、期待しない作用を引き起こす可能性がありますし、肝心のレジスタに0が出ずに音が止まらないということになります。 ビープ音鳴りっぱなしはエラー感、バグバグ感たっぷりでいただけません。

なので、最初に試したのが、レジスタの選択からデータの出力までの区間を割込み禁止 – dieiで括る – です。 が、どうもタイマー割込みはこれを無視しているようで、症状は一向に改善しません。

じゃあ、ビープ音を鳴らしている間はタイマーを止めるか? とも思ったのですが、タイマー割込み16回分くらいはビジーループで時間調整しているので、好ましくありません。 多分BGMがおかしなことになるでしょう。

B0Hへの出力のように、A0Hに出力した値もワークエリアに残すか、とも思いましたが、PSGたたくときにはガンガンレジスタを変えるので、outを一個発呼するために、ld (nnnn),aを一つ発呼するのはいかにもイケてません。 大体、レジスタを選択してからワークエリアを更新するまでの間に割り込まれたら何の解決にもありません。

割込みでPSGを操作して戻るときにチャンネルAの音量を0にするというのはよさげにも思えましたが、場合によっては音が鳴らないかもしれません。

となったら、もう、ビープ音もタイマー割込み側でならせばいいじゃん、という結論に至りました。

メインルーチン側では、ビープ音の音程、音量、そしてどのくらいの間鳴らすかの三つの値をワークエリアに書き込みます。 割込み側では、これらを読んで、指定された音程、音量を設定し、音の長さを0になるまで割込みごとに減算して、0になったところで音量を0にして終わります。

割込みは1/512s間隔で発生するので、平均待ち時間は1/1024s程度になります。 ビープ音を鳴らしたいタイミングから平均1ms未満の待ち時間ですので、テストプレイした限りでは、音が遅れて鳴るようなことはありませんでした。 めでたし。

万一、鳴っている最中に、別のビープ音を鳴らす処理(値の更新)があったら、まあ、音がつながって長くなったりするだけ(のはず)なのでいいことにします。

AY-3-8910のエンベロープには単調に減少するとか、単調に大きくなるとかはあるんですが、一定期間ONで周期が来たら0になるというようなものがないので、ビープ的な音を鳴らしたい場合には、一定期間鳴らしたらオフにするという操作がひつようになるため、まあ、こんなやり方で逃げるしかないのかな、と、いう感じです。

なお、カウンターは手抜きで、1byteしかとっていませんので、音の長さは 1/512s~1/2s になります。 まあ、0.5秒より長く鳴らすともうそりゃビープじゃないよな、ということで、フルーツフィールド的にはよいことにします。

副作用として、ビープ音のためにビジーループしていたのがなくなって、タイマー割込みでタイミングを見るようにしたので、連続してフルーツを取るのが多分少しだけ早くなっています。

16bit演算

Z80は8bitのCPUなので、基本的な演算は8bitのレジスタに対して行い5)、16bitの演算は適宜、Cフラグなどと組み合わせて複数回にわけて行う必要があります。

しかし、いくつかの演算については16bitレジスタペアに対しても使えます。

ところが、8bitの演算と全く等価ではない場合があるので注意が必要です。 と、いうか、失念してて、罠にはまりかけました。

16bitの減算処理、DEC命令においては、16bitのそれはフラグを変更してくれません。 なので、

    DEC HL
    JR  Z, END
    ...

のようなコードは動かないのです。 減算した後、別途比較を組む必要があります。

    DEC HL
    LD  A,H
    OR  L
    JR  Z, END
    ...

これなら動きます。 が、Aレジスタを使うし、比較のためにサイクルを食うし、忘れると動かないし、気を付けないといけません。

STOPキー?

BGMのON/OFFはSTOPキーで行います。 え、なんでSTOP?

これは、ゲーム中のキーの読み取りを1061HのBIOSコールで行っているのですが、このコールで読み取れるキーのうちゲームで未使用だったのがSTOPキーだけだったのです。

もう、何も残っていないので、これ以上何かを追加するとしたら、思案が必要です。。。

ビットキー
7スペースキー
6空き
5
4
3
2
1STOPキー
0SHIFTキー

なお、エミュレータにおけるSTOPキーは PC-6001VX、PC-6001VWともにEndキーに割り付けられています。 ご参考まで。

タイマー割込みからの復帰

割込みハンドラーですから、戻りはRETIじゃないかと思っていたのですが、サンプルなど見てもEIしてRETしているだけなんですよね。 なので、フルーツフィールド内でも同様にしていますが、とりあえず問題は起きてないので良いことにしておきます。

キー入力とジョイパッド

PC-6001にはゲーム用のキー入力ルーチンがあり、Aレジスタにビットマップ値でキー入力を返してくれます。 フルーツフィールドでもこれを利用していますが、DevTermについているジョイパッドに対応するために、ジョイパッドの入力を拾うルーチンも呼び出す必要が発生しました。 このルーチンもジョイパッドの操作をAレジスタにビットマップとして返してくれるのですが、このマッピングが、キー入力のそれと違うのです。 NECは何を考えて異なるビットマップを返すようにしたのでしょう?

-01234567
キーSHIFTSTOP-SPACE
ジョイパッドAB--

上のようなビットマップなんです。 いやらしいのは、そもそも方向とトリガー相当のもののマッピングがずれているし、右左が逆になっていたりするところ。

都度、それぞれの処理を書くのは面倒なので、既に、キー入力に合わせて処理を組んでいるので、ジョイパッドの入力をキー入力に合わせるように変換するコードを書いて、対応しました。

;;; gamekeypad
gamekeypad:
        call    gamekey
        and     a
        ret     nz        ; キー入力があったら戻る

        ld      a,1       ; ジョイパッド#1からの入力を見る
        call    joystick
        and     a
        ret     z         ; 何も押されてなければ戻る
        push    bc
        ld      b,0
        ld      c,a
        rlc     c         ; 左に1bitローテートして 10hでマスクして→を取り出す。  |-|-|-|-|→|-|-|-|
        ld      a,10h
        and     c
        ld      b,a
        rlc     c         ; 左に1bitローテートして 0chでマスクして↑↓を取り出す。 |-|-|↑|↓|-|-|-|-|
        ld      a,0ch
        and     c
        or      b
        ld      b,a
        rlc     c         ; 左に1bitローテートして 0a3hでマスクして←とABを残す。 |B|-|-|-|-|←|-|A|
        ld      a,0a3h
        and     c
        or      b                                                              |B|-|↑|↓|→|←|-|A|
        pop     bc
        ret

技術的ではないボヤキのようなもの

ウェイトループ

移植にあたって参考にしたPC-8001版は、随所にウェイトが入っており、それでもエミュレータ上で速すぎるくらいに速く動作しています。

PC-8001もPC-6001もZ80 4MHzであり、メモリアクセスにウェイトが入っているのも同じです。

であれば、同量のウェイトを仕込んでしかるべきだろうと思ったのですが、驚くほど遅かったので、最終的には、ステージセレクトと音を鳴らす時間を調整する部分以外のウェイトは全廃しました。

それどころか、ループ内で呼び出しているルーチンの徹底した高速化まで余儀なくされたのです。

恐るべし、PC-6001……。

なお、エミュレータを作成してくださっている方々の名誉のために申し上げますが、フルーツフィールドに限っていえば実機と同等の速度でエミュレータ上でも動作しています。

サウンド?

実は、移植中に、サウンド機能を実装してあるっぽい形跡を見つけたのです。 ステージ開始のとき、ステージクリアしたとき、あとギブアップしたときの三か所から呼ばれているサブルーチンがあったのですが、中では、HLレジスタのさしているアドレスから3byte読んでは、ブザーをパラメータに従って鳴らしに行くという動作を繰り返すようになっていて、先頭の1byteがFFHになるまでこれを繰り返すというものでした。

ただ、三か所とも、いきなり FFHをポイントして呼び出すので結局何もしないで戻ってきてしまいます。

おそらくはそこに音階データとなるものが仕込まれていたんでしょうが、何らかの事情で削除されてしまったようです。 FM-7やX1では音楽も流れるようなので、おそらくはそれと似たようなのが仕込まれていたのでしょうが、今となっては知る由もありません。 移植の際には、呼び出しも含めて綺麗に取っ払ってあります。

なお、現在、X1版のBGMを移植して、鳴らせるようにしてあります。

感想

PC-6001mkIIにフルーツフィールドを移植してみて思ったことは、なかなかいいハードじゃないかということです。

画面は160×200とやや低解像度だし、VRAMがアトリビュートとグラフィックの二つに分かれていたり、兎にも角にも遅いという側面はあるものの、頑張って全速力で動かせば、ゲームを遊ぶのには十分だし、音楽もPSGではありますが、そこそこ簡単に鳴らすこともできるし、仕上がったフルーツフィールドは、PC-8001版にはなかったBGMを持ち、ドット単位で15色の色を持ち、似て非なるものになりました。

ゲームとしての出来は、キメラ化の結果ではありますが、とてもよく仕上がったと思います。 勿論、PC-8001のハードウェアの制約を考えれば、オリジナルの出来は秀逸で、だからこそ、PC-6001mkII版があるわけですが、当時PC-6001mkII版があってもよかったなあ、と、改めて思いました。

start

1)
PC-8801, FM-7, PC-8001, MSX/MSX2, MZ-2000/2200/2500, X1, PC-1251/55, PC-1350 の8機種。ポケコンが二機種も入っているのに、そこそこのシェアがあったであろうPC-6001シリーズがなかったのは残念。
2)
正確にはマップデータの開始アドレスと終了アドレスが記録されているので、万が一将来的にマップデータの配置が変わるとデータは正しくロードできなくなります。そういうことがないようにはしていますが。
3)
タイマー割込みを利用するようにしたので操作にかかわらずBGMは安定して再生されるようになりました。
4)
具体的には音が止まらない
5)
特にアキュムレータであるAレジスタのみに使えるものが多い。
フルーツフィールド_for_pc-6001mkii.txt · 最終更新: 2022/09/15 18:40 by araki