aburi6800のブログ

コンピュータのプログラミング、ゲームに関するニッチな情報を書いていくブログです。

【MSX】インディ・ショーンズ 洞窟の秘宝 開発メモ

この記事について

恥ずかしいのですが、せっかくですので、先日公開した拙作のBASICゲームの作成メモを公開します。
だいたいこんな感じで書いていって、手元に資料化しています。参考になれば幸いです。
なお、絵はブギーボードに書いたのをスマホアプリでスキャンしています。(なかなか便利)
ゲームの概要については、前回の記事を参照願います。

aburi6800.hatenablog.com


概要

洞窟を舞台で探検家が主人公。
洞窟の奥に眠る財宝を目指して進んでいくストーリー。

  • 自機と照準を別々に操作。
  • スペースキーを押している間は照準を移動。離すと弾を撃つ。
  • 弾はと照準に向かって飛ぶ。着弾まで次の弾は撃てない。
  • 上にはコウモリ、地面はガイコツ
  • コウモリの落とす毒やガイコツに触れるとアウト
  • ガイコツはジャンプしてかわそう
  • コウモリをある程度倒したらレベルクリア
  • 5レベル?クリアで財宝にありつける(デモ+ボーナス)

(5/27 追加ルール)
- ナイフを投げると-10点。ゼロのときに投げて外すとミス。
- 得点はナイフの移動量による。1回移動すると加点+10。(たとえば8回ナイフが動いて当たると+80点、なので画面上部にいるのを倒すのは難しいけどお得)

概要は最初に書いて、頭の中でイメージを作ります。
このあと、画面のモックやキャラクターの初期デザインを作って、雰囲気を固めておきます。 このゲームの場合は、勢い余ってタイトル画面まで作っちゃいましたね(^^;


悩ましいところ

  • ナイフ移動中もコウモリとの当たり判定を行うか?(なんか簡単になりそう)
     →簡単になりすぎたのでしない
  • ガイコツをジャンプしてかわしたら得点入れるか?(判定方法がよくわからない)
     →しない(上記追加ルールから、序盤ステージではガイコツは出ないが0点でミスにできなくなる、ナイフを投げられない制御が必要になる、などの理由による)
  • コウモリの移動量、あるていど大きくするべきか?(あまり小さいと当てやすくなるかも)
     →今のままとする(が、突撃モードは移動が雑なので、もう少し調整するかも)
  • ちょっと単調、狙って当てる緊張感がない
     →追加ルールで対処(減点は2面以降はあまり意味がないが、クリアボーナス=面×100とかでなんとか調整できるか?)


キャラクター

プレイヤー

設定

  • 左右に移動、上方向(斜めも可)でジャンプ
  • ジャンプ中は一定の軌道で移動、その間は操作できない
  • スペースキーを押すと照準を移動、その間は移動できない
  • 照準移動中にスペースキーを離すとナイフを投げる、その直後から移動可能となる
  • ナイフはスペースキーを離した時点の照準の場所へ飛ぶ、到達するまで次のナイフは撃てない
  • コウモリやガイコツ、毒などに当たるとミス。

処理

  • トリガフラグ判定
    • ON :
      • 照準移動処理 (サブルーチン) へ
    • OFF :
      • プレイヤー移動処理 (サブルーチン) へ
  • ショットフラグ判定
    • ON :
      • ナイフ移動処理 (サブルーチン) へ
  • プレイヤー移動処理 (サブルーチン)
    • ジャンプフラグ判定
      • ON :
        • X,Y座標と移動量を計算
        • 画面端判定、座標補正
        • スプライトアトリビュート更新
        • Y座標が画面下限(地面)に到達したらジャンプフラグ=OFF
      • OFF :
        • 入力に応じてプレイヤーを移動
        • 上方向の場合はジャンプフラグ=ON
    • ジャンプ判定
      • ジャンプフラグ=OFF かつ 上方向の入力 (8,1,2) の場合
        • ジャンプフラグ=ON
    • 移動処理
      • 入力方向から移動量計算
      • 画面端判定、座標補正
      • スプライトアトリビュート更新
  • 照準移動処理 (サブルーチン)
    • 方向入力に応じて照準を移動
    • トリガ入力=OFFになったらトリガフラグ=OFF、ナイフ発射処理 (サブルーチン) へ
  • ナイフ発射処理 (サブルーチン)
    • ナイフ発射フラグ判定
      • ON :
        • 処理しない
      • OFF :
        • プレイヤー位置と照準位置から、移動量を計算
        • ナイフ到達位置に照準位置を設定
        • ナイフ発射フラグ=ON
        • ナイフのスプライト表示
  • ナイフ移動処理 (サブルーチン)
    • ナイフ座標に移動量を加算、スプライト移動
    • 座標判定
      • ナイフ到達位置の場合 :
        • ナイフスプライト消去
        • ナイフ発射フラグ=OFF
        • コウモリとの当たり判定、ヒットしていたら :
          • 対象のコウモリの状態を非表示に
          • スコア加算
          • コウモリ残数をデクリメント、ゼロならゲーム状態をレベルクリアへ


コウモリ

設定

  • 同時に最大2体まで出現
  • 飛行中と降下中の2つの状態を持つ
    • 飛行中:画面上部を一定の軌跡で飛ぶ、毒を落とす
    • 降下中:プレイヤーめがけて降りてくる、地面まで来ると上に移動、飛行中に戻る
  • プレイヤーに触れるとアウト
  • 毒は以前作った逆スクロールルーチンを使用する

処理

  • ゲームループごとに1→2→1と処理対象のコウモリを切り替える
  • 状態を判定 :
    • 非表示 :
      • 画面表示数 = 最大出現数 なら出現させない
      • 状態=飛行中、飛行初期設定 (サブルーチン) へ
    • 飛行中 :
      • 乱数 < 降りてくる確率 の場合 :
        • 状態=降下中、降下初期設定 (サブルーチン) へ
      • 上記以外の場合 :
        • 座標計算
        • 乱数 < 毒を出す確率 の場合 :
          • 現在のテキスト座標に毒を表示
        • 目標座標に到達したか?
          • 到達した :
            • 飛行初期設定 (サブルーチン) へ
          • 到達していない :
            • 処理なし
    • 降下中 :
      • 座標計算
      • 目標座標に到達したか?
        • 到達した :
          • 状態=上昇中、飛行初期設定 (サブルーチン) へ
        • 到達していない :
          • 処理なし
    • 上昇中 :
      • 座標計算
      • 目標座標に到達したか?
        • 到達した :
          • 状態=飛行中、飛行初期設定 (サブルーチン) へ
        • 到達していない :
          • 処理なし
    • プレイヤーとヒットしたか?:
      • ヒットした :
        • ゲーム状態=ミス
      • ヒットしていない :
        • 処理なし
  • 飛行初期設定 (サブルーチン)
    • 目標座標X, Y をランダムに設定
    • 移動量X、移動量Yを設定
  • 降下初期設定 (サブルーチン)
    • 目標座標X,Y をプレイヤー位置に設定
    • 移動量X、移動量Yを設定


コウモリは最初2体で考えてましたが、少なくて画面が寂しかったので、3体にしました。


ガイコツ

設定

  • 同時に最大2体まで出現
    • ゲームループごとに1→2→1と処理対象のガイコツを切り替える
  • 画面端から出現、まっずぐ横に移動する
  • プレイヤーに触れるとアウト

処理

  • 出現前 :
    • 出現タイマーをデクリメント、ゼロになったら出現
    • ランダムで出現位置(左端・右端)と移動方向を設定、出現フラグON
  • 出現中 :
    • 出現フラグONなら座標に移動方向を加算、スプライト移動
    • 画面端に到達したら出現フラグOFF


設定

  • ラウンドが進むと出現
  • ランダムな位置で画面上部から落ちてくる
  • スクロールて自動的に移動

処理

  • 岩出現タイマー判定
    • >0 :
      • 岩出現タイマーを-1
    • =0 :
      • ランダムなX座標で岩を表示
      • 岩出現タイマーに初期値を設定
    • =-1 :
      • 処理なし


キャラクターや障害物は、最初一定周期で出そうと考えてましたが、ランダムにしてしまいました。


ゲーム状態


意味
1 タイトル〜ゲーム初期化
2 ラウンドスタート
3 ゲームメイン
4 ラウンドクリア
5 ゲームクリア
6 ミス
7 ゲームオーバー


変数表


変数名 内容 説明
GS ゲーム状態 1=タイトル
2=ラウンドスタートデモ
3=ゲームメイン
4=ラ ウンドクリア
5=ミス
6=ゲームオーバー
7=オールクリア
RD ラウンド数
SC スコア
LF 残機
TL 残りターゲット数
N スプライト表示対象のキャラクター番号 スプライトプレーン番号はこの値に+2する
0:プレイヤー
2 : 照準
3 : ナイフ
4~5 : コウモリ
6 : ガイコツ
X(n),Y(n) スプライトX,Y座標 16倍して保持
JY(n) ジャンプ中のY座標加減算値
TX(n),TY(n) 移動先のスプライトX,Y座標 16倍して保持
WX(n),WY(n) スプライトX,Y移動量 16倍して保持
F(n) 敵の状態フラグ 値 :
0=未発生
1=発生(右向き)
2=発生(左向き)
3=発生(やられ)
n(インデックス) :
4~6=コウモリ
7=ガイコツ
P(n) スプライトパターン番号
R(n) スプライトアニメカウンタ 0,1で交互に変更
C(n) スプライトカラー番号
C ゲームループ内で処理対象とする敵キャラクターの番号
SF ショットフラグ 1=ナイフ投げている
0=ナイフ投げていない
BF 操作モード 0=プレイヤー操作中
1=照準操作中
JF ジャンプフラグ 初期値は7、ジャンプ中にデクリメントする
JY(n) ジャンプ中のY座標加減算値
JS ジャンプ時に入力されていたSTICK値
TF 目標座標到達判定フラグ
TL ターゲット残り
MC ナイフ移動カウンタ 発射時にゼロに初期化、移動の都度+1
X,Y キャラクター初期位置 キャラクター移動初期処理(2000行目〜)で使用
TX,TY キャラクター目標位置 キャラクター移動初期処理(2000行目〜)で使用
SP キャラクター移動スピード キャラクター移動初期処理(2000行目〜)で使用
X1,Y1,X2,Y2 キャラクター移動デモの移動開始座標、移動終了座標 キャラクター移動デモ処理(5400行目〜)で使用

BASICだと変数名に最大2文字しか使えないので、変数表は必須ですね(^^;


スプライトパターン


キャラクター PLANE PTN1 COL1 PLANE PTN2 COL2
ショーンズ 右向き1 0 0(0,1,2,3) 5 1 1(4,5,6,7) 10
ショーンズ 右向き2 0 2(8,9,10,11) 5 1 3(12,13,14,15) 10
ショーンズ 左向き1 0 4(16,17,18,19) 5 1 5(20,21,22,23) 10
ショーンズ 左向き2 0 6(24,25,26,27) 5 1 7(28,29,30,31) 10
ショーンズ ミス 0 8(32,33,34,35) 5 1 9(36,37,38,39) 10
照準 2 10(40,41,42,43) 14 - - -
ナイフ 3 11(44,45,46,47) 15 - - -
コウモリ1 4 12(48,49,50,51) 5 - - -
コウモリ2 4 13(52,53,54,55) 5 - - -
ケルトン 右向き1 5 14(56,57,58,59) 14 - - -
ケルトン 右向き2 5 15(60,61,62,63) 14 - - -
ケルトン 左向き1 5 16(64,65,66,67) 14 - - -
ケルトン 左向き2 5 17(68,69,70,71) 14 - - -


その他メモ

  • パターン番号の算出方法

    • プレイヤー
      • PUT SPRITE時 : [対象のパターン番号] + [アニメーションカウンタ] * 2
      • VPOKE時 : ( [対象のパターン番号] + [アニメーションカウンタ] * 2 ) * 4
    • その他
      • PUT SPRITE時 : [対象のパターン番号] + [アニメーションカウンタ]
      • VPOKE時 : ( [対象のパターン番号] + [アニメーションカウンタ] ) * 4
  • プレイヤーのパターン番号
    S=2,3,4 : 0
    S=6,7,8 : 4
    上記以外 : 変更なし

  • キャラクターの移動処理はおそらく一つでOk、移動先と移動量の設定やアルゴリズム(途中の目標座標や移動速度の変更など)は個別で。座標などはコウモリもガイコツも同じ配列で管理する。

  • 計算の都合で、座標値は内部で16倍して持ち、表示時に16で割る。注意。


レベル設定

  • レベル1 (練習ステージ)

    • コウモリ
      • 下に降りてくる
  • レベル2

    • コウモリ
      • 毒を落とす
      • 下に降りてくる
  • レベル3

    • コウモリ
      • 毒を落とす
      • 下に降りてくる
    • ケルトン出現
  • レベル4

    • コウモリ1
      • 毒を落とす
      • 下に降りてくる
    • ケルトン出現  - 岩が落ちてくる
  • レベル5

    • コウモリ
      • 毒を落とす
      • 下に降りてくる
    • ケルトン出現  - 岩が落ちてくる

【MSX】インディ・ショーンズ 洞窟の秘宝

またまた、MSX-BASICで簡単なゲームを作ってみました。 以下のURLで遊べます。

webmsx.org

遊び方

タイトル画面
- タイトル画面でスペースキー(またはトリガボタン)でスタート。

ゲーム画面
- 洞窟にいるコウモリを倒してください。
- 決まった数を倒すと、ラウンドクリアです。
- 5ラウンドクリアでエンディングです。
- ショーンズは左右に移動、上方向を押すとジャンプします。
- 照準はスペースキーを押しながらカーソルキーで動かせます。
- スペースキーを離すとナイフを投げます。
- コウモリを倒すと、ナイフが飛んだ距離に応じてスコアが入ります。外すと-10ptsです。
- ショーンズがコウモリなどにあたったり、スコアが0になるとミスになります。
- 残機がゼロのときにミスをすると、ゲームオーバーです。

ソースなど

githubでもソースとディスクイメージを公開しています。
詳細については、こちらを参照してください。
github.com

ではでは。

【MSX】KOBUTA RESCUE

ちょっとした気分転換に、MSX-BASICで簡単なゲームを作ってみました。 以下のURLで遊べます。
webmsx.org

遊び方

タイトル画面
- タイトル画面でスペースキー(またはトリガボタン)でスタート。

ゲーム画面
- 子ぶたがとことこ歩いてくるので、穴に落ちないようにスペースキー(またはトリガボタン)で床を動かし、家まで導いてください。
- ゲームが進むとオオカミが登場します。オオカミは家に入れてはいけないので、穴に落としましょう。
- 子ぶたが穴に落ちるか、オオカミが家につくとミスになります。
- 3回ミスでゲームオーバーです。

ソースなど

githubでもソースとディスクイメージを公開しています。
詳細については、こちらを参照してください。
github.com

ではでは。

【MSX】Z80マシン語入門してみた(第2回)

さて、前回から間を置いてしまいましたが、今回からは、私が制作したオールマシン語ゲーム「Corridor Runner」の制作過程を追って、どのように1つのゲームを作っていったのかを書いていこうと思います。
マシン語もゲーム制作も、私の独学ですので一般的なものではありませんが、MSXに限らずこれからゲームを作る人のヒントになれば幸いです。

いざ、オールマシン語へ!

最初は、前回作成した逆スクロールの処理を使って、なにかひとつBASIC+マシン語でゲームを作ってみようと考えました。
しかし、どうしても滑らかに複数のキャラクターを動かしたく、それにはやはりBASICでは役不足で、マシン語に頼るしかありません。
また、逆スクロールのプログラムを作ったことで、なんとなくアセンブリ言語でのプログラムの作り方がイメージできたので、思い切ってオールマシン語に挑戦することにしました。

実は中学生の頃に一度マシン語に挑戦したのですが、挫折した経験があるので、そのリベンジという気持ちもありました。

プログラムを作る準備

まず、開発環境を準備します。
個人的にメインのデスクトップ環境をLinuxubuntu)にしていることもあって、マルチプラットホームで使えるものが条件になりました。

現状では、Windows+WSL2の環境にLinuxを入れる、という手もありますね。

アセンブラ

はじめは、一番重要なZ80アセンブラです。
これがなくては何も始まりません。
今では便利なことに、PCでZ80のコードをアセンブルすることができます(クロスアセンブラ)。 代表的なものでは以下があります。

  • SDCC
    8ビットの各種プロセッサのバイナリを生成可能なCコンパイラで、WindowsMac OS XLinuxをサポートしています。
    MSXで利用できる形式(romイメージやテープイメージなど)を直接出力できないので、変換が必要です。
    こちらに詳しい説明がまとまっています。

  • z88dk
    SDCCと同じく、8ビットの各種プロセッサのバイナリを生成可能なCコンパイラです。
    こちらもWindowsMac OS XLinuxをサポートしています。
    各マシンで直接ロード可能な実行バイナリを生成することができます。

  • pasmo
    MSXPenで採用されているコンパイラです。
    MSXのBLOAD形式でロードできるバイナリを出力できます。
    このコンパイラを使用したzDevStudioという統合環境もあるようです。

  • ZASM
    Z80アセンブラですが、8080構文をZ80に変換可能な機能を持っています。
    SDCCのCソースをインクルード可能で、ZX7を使用した自動圧縮やリストファイルにCPU累積サイクルを含めることができる、ユニークな機能を持っています。

  • Z80AS
    Win32コンソール上で動作する、Z80コンパイラです。
    日本人の方が作成されています。
    Intel HEX形式の他、SHARP MZシリーズ用のテープイメージ形式、MZ2000用のディスクイメージ形式、PC8801用のディスクイメージ形式での出力が可能なのが特徴です。
    ダウンロードはこちらで可能です。(ver0.12)

  • AILZ80ASM
    .NET 6環境で動作するZ80アセンブラです。C言語はサポートされていません。
    この中では比較的新しく作成されており、作者の方も日本人ですので、ドキュメントも参照しやすいものになっています。

この中から、以下の条件で絞り込みました。

  • WindowsLinuxの両方で使える
  • MSXのromイメージ、テープイメージで出力できる
  • (将来的にやるかもしれないので)C言語も扱える
  • (将来的にやるかもしれないので) MSX以外の他のZ80マシンのオブジェクトも出力できる

また、開発が活発に行われているということもメリットとして考え、私はz88dkを選択しました。

もちろん、どれも一長一短ありますので、お好きなものを選択頂いて構わないと思います。
特に、C言語は不要で、ドキュメントが読みやすいほうが良いという方は、Z80ASやAILZ80ASMを選択したほうが良いと思います。

エディタ

次にエディタです。 何でも良いのですが、言語を問わず無料で利用でき、Gitも使え、プラグインが充実しているVisual Studio Codeを選択しました。
導入したプラグインは、以下です。

  • ASM Code Lens
    アセンブラ構文の強調表示、入力補完、ラベルなどの参照検索、シンボルのコメントをホバー表示、シンボルの名前変更、ラベルの参照数表示など、強力なサポート機能を持っています。
    正直、これがないと、とてもではないですが開発できませんでした…。

  • MSX Z80
    MSXBIOSエントリやワークエリアのシンボルがサポートされます。
    ですが、ASM Code Lensが強力すぎて、いまいちこのプラグインの効果がわかりません…。

  • Z80 Assembly Meter
    Z80アセンブリソースコードからクロックサイクルとバイトコードサイズを測定するプラグインです。
    ソースの選択範囲についてクロック数とバイトサイズが計算され、画面下のステータスバーに表示されます。
    かなり便利で、MSXでのM1サイクルにも対応しており(要設定変更) 、処理速度を求めるロジックのコーディングに役立ちました。

  • Z80 Instruction Set
    Z80ニーモニックのオペコードと説明、フラグの変化などがホバー表示されます。
    ちょっとした命令の確認を非常に効率的に行うことができて便利です。

だいたいこのあたりで十分だったのですが、他にも便利なプラグインがありましたら教えていただけるとありがたいです。

エミュレータ

実は開発開始時点ではMSXの実機を持っていなかったのと、動作確認はPCで行う方が早いので、エミュレータを導入しました。
現状、開発に使える(デバッガに対応している)MSXエミュレータは、以下があります。

  • BlueMSX
    古くからある有名なエミュレータで、デバッガも搭載されています。
    サイトやエミュレータも日本語に対応していて導入しやすいのですが、10年以上更新されていないため、やや精度には劣るのと、Windows以外のバイナリが配布されていません。

  • openMSX
    現在も活発に開発が進んでいるエミュレータです。
    WindowsMac OSLinuxで利用できます。
    少々使い勝手にクセがありますが、開発でも便利な機能があり、debuggerも用意されています。

今回はLinuxで開発したいので、消去法でopenMSXになりました。

ただ、openMSXのデバッガの使い勝手がイマイチだったので、最終的にはMAMEを使用しています(後述)。

デバッガ

さて、最後にデバッガです。
プログラムが期待した動作をしなかったとき、画面に情報を表示させておくのも有効ですが、途中で暴走などをしてしまう場合はデバッガでメモリ(プログラムカウンタ、スタック)やレジスタの状態を確認する必要があります。 デバッガとして使えるツールは、以下のものがあります。

  • openMSX debugger
    openMSXと組み合わせて使えるデバッガです。
    コンパイルして生成したリストファイルを読ませて、openMSXに接続することで、ブレークポイントの設定やメモリやレジスタの確認・更新を行えます。
    また、VRAMの内容も参照できます。
    z88dkのリストファイルにはそのままでは対応していないので、パッチを当てて、ソースからビルドする必要があり、少々敷居が高いです。

  • DeZog
    Visual Studio Codeプラグインで動作する、Z80デバッガです。
    ソースコードに直接ブレイクポイントを設定してデバッグ実行させることが可能になります。
    ただし、DeZogからopenMSXへの接続がうまくいかないため、エミュレータにはMAMEにC-BIOSを組み込んだものを使用します。
    このあたりの導入については、こちらに詳しく説明がありますので、参照してみてください。

開発当初はopenMSX debuggerを使用していましたが、DeZogの作者の方にz88dkのリストファイルに対応頂いたので、現在はDeZogを使わせて頂いています。

プログラム以外で必要だったもの

プログラミング以外では、グラフィックとサウンドのツールを準備しました。

グラフィック関連ツール

グラフィック用の準備したツールは、以下になります。

  • Edge
    Windows用のグラフィックエディタです。
    パターンの回転やアニメーションの確認などもできるため、私はこれにMSX1のカラーパレットを定義して、各方向のキャラクターパターンやエンディングの1枚絵を書きました。
    作成したデータは画像ファイル形式でしか保存できませんので、以下の各ツールで打ち込み直しています。
    wineを使えばLinuxでも動作可能させることができます。

  • nMSXTiles
    MSXのSCREEN2/4に対応したグラフィックエディタです。
    編集したデータはアセンブリソース形式、バイナリ形式などで出力可能です。
    パターンネームテーブルのデータだけ使えば、SCREEN 1でのデータとしても使うことができます。
    スプライトパターンも作れるようですが、私には使い方がよくわかりませんでした…。

  • TinySprite
    Webブラウザ上で使えるスプライトパターンエディタです。
    必要最低限の機能しかありませんが、16x16ドットのキャラクターが基本であれば問題ないと思います。
    作成したパターンはBASICのDATA文、アセンブリソース形式などで出力できます。

今回は採用を見送りましたが、他にも以下のツールがあります。

  • 99x8Edit
    日本の方が開発された、TMS9918/V9938をターゲットにしたグラフィックエディタで、タイル、スプライト、マップの各データの作成が可能です。
    また、PNG画像のインポートやエクスポートデータの圧縮、CRTフィルタといった機能もサポートしています。
    .NETアプリケーションですがWindows固有のAPIを使用しているためか、Linux(Ubuntu)では動作しなかったため、使用は見送りましたが、Windows環境で開発される方は是非利用してみてください。

サウンド関連ツール

サウンド用に準備したツールは、以下になります。

  • PSGSoundDriver
    ゲームに使うサウンドドライバを探してみたのですが、汎用的なものでは以外と要件(簡単に使えて効果音も鳴らせるもの)に合致するものが見つからなかったので、自作しました。
    これについては、別途記事にしたいと思います。(いつになることやら…)

  • LovelyComposer
    Python製のチップチューン作曲ツールです。
    有料アプリですが安価であること、PSGと近い構成であること、感覚的に作曲できることから、利用させていただきました。
    データはjson形式なので、自作サウンドドライバ用のデータに変換するツールも作成しています。

いよいよ開発へ!

さあ、道具が一通り揃ったところで、ようやく開発できるようになりました!
ここから長い長〜い道のりが待っているのですが、長くなったので今回はここまで。
しかし、ツールの選択だけでも大変なんですよね…。
30年以上前のMSXマイコンですらこうですから、今のWebアプリケーションの開発などはもっと大変なのではないでしょうか。
この記事が、少しでも皆さんのツール選定のヒントになってくれると嬉しいです。
では、次回をお楽しみに。

【MSX】Corridor Runner

Z80アセンブリの勉強で、ゲームを作ってみました。 こんな雰囲気のゲームです。

www.youtube.com

以下のURLで遊べます。

WebMSX

遊び方

  • タイトル画面でスペースキーを押すと、スタートします。

タイトル画面

  • カーソルキーの左右で方向変更、下でパワーを溜めて離すとパワーに合わせて前進します。
  • 出口まで行くと、ラウンドクリアです。

ゲーム画面

  • アイテムチップを取ると得点になります。
  • アイテムチップは連続して取ると得点がアップします。
  • 最初に20,000pts、以降50,000ptsごとに残機が増えます。
  • 床から落ちたり、敵に触れるとミスになります。
  • 全16ラウンドをクリアするとゲームクリアです。
  • ゲーム中は[F1]キーで一時停止できます。
  • ゲームオーバー時に[F5]キーを押すとコンティニューできます。(4面ごと)

ソースなど

githubに全て公開しています。

github.com

【MSX】Z80マシン語を勉強してみた(第1回)

はじめに

往年のマイコン少年なら誰しも憧れた、マシン語
当時のマイコン雑誌には何ページにもわたってダンプリスト(16進数の羅列)が載っていたりして、その入力すら厳しいものでしたが、そこから動くゲームはどれも本格的でBASICでは味わえないものばかりでした。
私も少年時代にマシン語のプログラムにチャレンジしましたが、当時は動くものが作れず、挫折してしたクチです。

そして時は流れ、現在。

改めて勉強しなおしてみると、なんということでしょう!
色々なことが理解できるようになり、ある程度動くプログラムが作れるようになりました。
せっかくですので、ここでは、その過程を記録していこうと思います。

まずは逆スクロールからやってみよう

入門書でよくあるのは、メモリの値を取ってきて計算して格納して…みたいなことが書かれていますが、正直つまらないですね。
やはり、画面で動くもので体感するのが、一番楽しく勉強できるというものです。

ということで、まず手始めに、昔挫折した逆スクロールの処理からやってみることにしました。
最初からすべてをマシン語で書くのは難易度が高いため、マシン語では逆スクロールする処理だけを作り、BASICで画面描画とマシン語の呼び出しをする構成としました。

まずは開発環境とか情報とか

WebブラウザMSXのBASICやアセンブリのエディタとエミュレータが統合された無料て利用できる開発環境である、MSXPenというサイトを使いました。

MSXPen

Z80アセンブリニーモニックは、こちらを参照しました。

Yamamoto's Laboratory 8ビット CPU Z80命令セット

また、VRAMへのアクセスはBIOSを使用していますが、BIOSコールについてはこちらを参照しました。

MSX Datapack wiki化計画 Appendix A.1 BIOS 一覧

BIOSって?

ここでBIOSとキーワードが出てきましたので、ちょっと余談を。
このBIOSというのは、MSX規格のマシンで用意された、便利なサブルーチン郡のことです。
これにより、MSX規格として各マシンのハードウェア構成の違いが吸収され、違う機種でも同じアプリケーションが修正なしで実行できるようになっていました。

当時のコンピュータでは、OSというものがなく(強いていえばBASICがOS代わりだった)、CPUからメモリ以外のハードウェア(画面やキーボード、音源など)へアクセスする際は、I/Oポートを直接叩いて行っていました。(このI/Oの叩き方にもお作法があったりして、プログラミングの難易度が上がる要因にもなっていました)

MSXではこのI/OポートやVDPをアクセスする処理が予めBIOSとして用意されていますので、アセンブリ言語でプログラムする敷居が非常に低くなっています。
また、そもそもハードウェアとしてゲームプログラムに使いやすいスプライトやジョイスティック等もサポートされているため、入門機としては非常にお勧めできるマイコンのひとつです。

私もこのBIOSのおかげで、煩わしいI/Oポートへのアクセスをすることなく、処理に集中してプログラムができました。

その代償として、またROMカートリッジのサポートもあり、メモリアクセスのスピードが他のZ80マイコンよりも遅く、若干見劣りすることになりますが… また、さすがにVRAMへのアクセスが遅いのは問題視していたようで、VDPへの直アクセスは公式に認められていました。

アセンブリで書いてみよう

では早速、逆スクロールする処理を考えてみましょう。
アセンブリマシン語)のプログラムはメモリやレジスタに対する操作を書く、ということを念頭に置き、ニーモニック表やBIOSの一覧とにらめっこして、プログラムの処理の手順を以下のように考えました。

  • 画面最終行から処理開始
  • 一行上の表示内容をコピー
  • 次に処理対象行を一行上にして、同じ処理を行う
  • これを画面最上行の一行前まで繰り返す
  • 最後に画面最上行を空白で埋める

ここから、実際に作ってみたアセンブリプログラムは、次のようなものになります。

  ORG 0xC000          ; 開始アドレス

START:
    LD HL,&1AF8         ; HL=&H1AF8(処理開始VRAMアドレス)
    LD BC,&17           ; BC=行数カウンタ &H17(23)回

; 外部ループ処理 開始
; 全行に対しての処理を行う
OUTLOOP:
    PUSH BC             ; BCをスタックに退避
    LD BC,&19           ; BC=桁数カウンタ &H19(25)回

; 内部ループ処理 開始
; 1行に対しての処理を行う
INLOOP:
                        ; ■1行上のVRAMアドレスのデータを読む
    PUSH BC             ; BCをスタックに退避
    PUSH HL             ; HLをスタックに退避
    LD BC,&20           ; BC=&20(32)
    SBC HL,BC           ; HL=HL-BC
    CALL &004A          ; BIOS RDVRM呼び出し
                        ; - HL : 読み取るアドレス
                        ; - A  : 読み取ったデータ
    POP HL              ; HLをスタックから復帰
    POP BC              ; BCをスタックから復帰

                        ; ■現在のVRAMアドレスにデータを書き込む
    CALL &004D          ; BIOS WRTVRM呼び出し
                        ; - HL : 書き込み先のVRAMアドレス
                        ; - A  : 書き込むデータ

                        ; ■ひとつ左のアドレスに移動
    DEC HL              ; HL=HL-1

                        ; ■桁数カウンタ減算
    DEC BC              ; BC=BC-1
    LD A,B              ; A=B
    OR C                ; A=A OR C
    JR NZ,INLOOP        ; NZ(ゼロでない)なら、INLOOPラベルにジャンプ
; 内部ループ処理 終了

                        ; ■1行上の処理開始VRAMアドレスに遷移させる
                        ; 画面右に7文字分の余白を設けるため、VRAMアドレスから7を減算する
    LD BC,&07           ; BC=&07(7)
    SBC HL,BC           ; HL=HL-BC

                        ; ■行数カウンタ減算
    POP BC              ; BCをスタックから復帰
    DEC BC              ; BC=BC-1
    LD A,B              ; A=B
    OR C                ; A=A OR C
    JR NZ,OUTLOOP       ; NZ(ゼロでない)なら、OUTLOOPラベルにジャンプ
; 外部ループ処理 終了

    LD HL,&1800         ; HL=&H1800(処理開始VRAMアドレス)
    LD BC,&19           ; BC=桁数カウンタ &H19(25)回

; 内部ループ処理 開始
; 1行目のクリア処理を行う
ENDLOOP:
                        ; ■現在のVRAMアドレスにデータを書き込む
    LD A,&20            ; A=&20(" ")
    CALL &004D          ; BIOS WRTVRM呼び出し
                        ; - HL : 書き込み先のVRAMアドレス
                        ; - A  : 書き込むデータ

                        ; ■ひとつ右のアドレスに移動
    INC HL              ; HL=HL+1

                        ; ■桁数カウンタ減算
    DEC BC              ; BC=BC-1
    LD A,B              ; A=B
    OR C                ; A=A OR C
    JR NZ,ENDLOOP       ; NZ(ゼロでない)なら、ENDLOOPラベルにジャンプ
    
    RET                 ; BASICに戻る

END START

ゲームで使うことを想定して、右側7桁はスコア表示などのためにスクロールさせずに固定するようにしました。
そのため、スクロールするのは、(0,0)~(24,23)までの範囲になります。

まずは、処理を開始するVRAMアドレスを求めます。
画面右下から左上に向かって処理するため、求めるのは画面右下のアドレスになります。
SCREEN 1の画面(パターンネームテーブル)の先頭アドレスは1800Hです。
32桁×24行で画面を初期化するので、32×24-1=767=2FFHを1800Hに加算した1AFFHが画面右下のアドレスになります。
ここから、固定表示させる7桁分を減らした1AF8Hが処理開始アドレスになります。
この値をHLレジスタに設定します。

次にOUTLOOPですが、これは行数分ループさせます(外ループ)。
そのため、事前にBCレジスタに行数である23(全体で24行だが、最後の1行は処理不要のため処理が必要なのは23行となる)を設定しています。

INLOOPは一行を桁数分ループする処理です(内ループ)。
BCレジスタをスタックに退避後、桁数である25を設定し、内ループに入ります。
このループでは、以下のような処理をしています。

  • HLレジスタの値をスタックに退避
  • HLレジスタの値から32を減算したアドレス=一行上の表示文字を取得(BASICのVPEEKと同じ)
  • HLレジスタの値をスタックから戻し、VRAMのそのアドレスに取得した表示文字を書き込み
  • HLレジスタの値から1を減算する=処理対象の位置を1文字左へ移動する
  • ループ回数(桁数)から1を減算し、ゼロになるまで繰り返し

内ループ終了後、外ループに戻りますが、そのときに画面右7桁を処理対象外とするために、HLレジスタから7減算します。
スタックに退避したBCレジスタを戻して1減算し、ゼロになるまで外ループを繰り返します。

外ループが終わったら、画面最上部をスペースで埋めて、BASICへ戻ります。

レジスタだけで処理しているものの、スタックを無駄に使っていたり、8ビット値の範囲での繰り返しなのにカウンタに16ビットレジスタを使っていたり、そもそもループをネストする必要がなかったりと、あまりいいお手本ではないですね。 まあ、覚えたてで書いたプログラムなので、大目に見てもらえればと。

レジスタって?

上の説明では、簡単に「HLレジスタに設定します」などと書きましたが、そもそもレジスタってなんでしょうか?
これは、CPUの中に予め用意された、変数のようなものです。
MSXに搭載されているZ80というCPUでは、A,B,C,D,E,H,Lの7種のレジスタが使えます。
これらに加えて、フラグ・レジスタ、インデックス・レジスタが用意されています。
また、各レジスタでは8ビットの値(0~255)しか格納できないのですが、2つのレジスタを組み合わせて16ビットの値を扱うこともできます。
この組み合わせとして、BC,DE,HLとインデックスレジスタがあります。

さて、これらのレジスタは、どれでも自由に使えるのですが、その用途は命令によって限られています。
例えは、計算をする場合やメモリへの値の出し入れは、Aレジスタしか使えません。
アドレスを示すレジスタとしてはHLレジスタが使われることが多いです。
また、ループ制御(DJNZ命令)を使う場合は、ループ数をBレジスタに設定します。
このように、処理で使う命令に合わせて、使えるレジスタが自然と決められていくことになりますが、プログラムを書いていくうちに自然と慣れていくと思います。

BASICから動かしてみよう

さて、せっかく作ったマシン語プログラムも、動かさないと意味がありません。
マシン語プログラムを呼び出すBASICプログラムを作ってみましょう。

100 SCREEN 1,2,0:WIDTH 32:KEY OFF:COLOR 15,4,7
110 CLEAR 200,&HBFFF:DEFUSR=&HC000
120 BLOAD "PROGRAM.BIN"
130 FORI=0TO24:LOCATE25,I:PRINT"|";:NEXT
140 LOCATE26,1:PRINT"SCROLL";:LOCATE26,2:PRINT" DEMO";  
150 FORI=0TO30:LOCATEINT(RND(1)*25),INT(RND(1)*25):PRINT"*";:NEXT
160 A=USR(0):LOCATEINT(RND(1)*25),0:PRINT"*";:GOTO160

110行目のCLEAR文では、BASICで使うメモリをBFFFHまでに設定し、DEFUSR文で呼び出すマシン語プログラムの先頭アドレス(C000H)を設定しています。

なお、DEFUSRのあとに0~9の番号を書くことで、10個までマシン語のプログラムの呼び出しアドレスを設定できます。

120行目でマシン語プログラムを読み込み、130~150行目で画面の初期表示をした後、160行目でスクロール処理と星の表示を行っています。

ちなみに、ロードするアドレスのC000Hはどうやって決定したかというと、特に意味はありません。
ユーザーエリアが8000HF37FHまでなので、この範囲内であればどのアドレスを指定しても良いのです。
ただし、あまり若いアドレスを設定すると、BASICプログラムが入りきらなくなりますので、その場合はE000Hとかにしましょう。

また、システムワークエリアがF380Hから存在するので、そこまでに納まるようにしましょう。

以上で、何とか動くものができました。
この処理をMSXPenで動作させるURLは、以下になります。

https://msxpen.com/codes/-Mht5HeZgQfTPNV9vx31

このURLにアクセスすると、自動的に実行されます。

メモリにあるマシン語プログラムを見てみよう

さて、アセンブリソースから作ったマシン語プログラムと、BASICプログラムの2つを使って、画面を逆スクロールすることができました。
しかし、マシン語プログラムは実際どのようにメモリに格納されているのでしょうか?

BASICプログラムに以下の行を追加して再度実行してみましょう。

200 SCREEN0:A=&HC000:FORI=0TO63STEP8:PRINTRIGHT$("0000"+HEX$(A+I),4);":";
210 FORJ=0TO7:PRINTRIGHT$("00"+HEX$(PEEK(A+I+J)),2);" ";:NEXTJ:PRINT:NEXTI

そして、プログラムをストップして、MSXの画面で

RUN 200

と打ってEnterキーを押してみてください。
すると・・・

16進数がたくさん表示されました。
これがアセンブリソースをコンパイルして作られた、マシン語プログラムです。
最後のC9がBASICに戻るRET命令ですので、ここまでで全部で60バイトになるようです。

C9以降が00ではないことがあると思いますが、無視してください

なお、この16進数の値をBASICプログラムのDATA文として書いて、POKE文でメモリに書き込めば、BASICプログラムだけで逆スクロールさせることができます。
参考として、以下にBASICだけで書いたプログラムを載せておきます。

100 SCREEN 1,2,0:WIDTH 32:KEY OFF:COLOR 15,4,7
110 CLEAR 200,&HBFFF:DEFUSR=&HC000
120 GOSUB200
130 FORI=0TO24:LOCATE25,I:PRINT"|";:NEXT
140 LOCATE26,1:PRINT"SCROLL";:LOCATE26,2:PRINT" DEMO";  
150 FORI=0TO30:LOCATEINT(RND(1)*25),INT(RND(1)*25):PRINT"*";:NEXT
160 A=USR(0):LOCATEINT(RND(1)*25),0:PRINT"*";:GOTO160
200 FORI=0TO59:READA$:POKE&HC000+I,VAL("&H"+A$):NEXT:RETURN
210 DATA 21,F8,1A,01,17,00,C5,01
220 DATA 19,00,C5,E5,01,20,00,ED
230 DATA 42,CD,4A,00,E1,C1,CD,4D
240 DATA 00,2B,0B,78,B1,20,EB,01
250 DATA 07,00,ED,42,C1,0B,78,B1
260 DATA 20,DC,21,00,18,01,19,00
270 DATA 3E,20,CD,4D,00,23,0B,78
280 DATA B1,20,F5,C9,00,00,00,00

MSXPenで作ったプログラムを保存する

少し話が脱線しますが、MSXPen上で作成したプログラムを保存する方法を紹介します。

一つ目は、右上の「Share」というボタンを押して、表示されたURLをメモしておく方法です。
このURLにプログラムコードも含まれているので、アクセスすると全て復元されます。
この記事のように、ブログなどにも貼り付けることが出来るので便利です。

二つ目は、エディタに書いたソースをコピーして、他のテキストエディタなどに貼り付けて保存する方法です。
PC上で再利用したい場合に有効です。
なお、MSXPenではPasmoというアセンブラが使われており、PCにインストールすることでコンパイルすることができます。
詳細は以下を参照ください。

Pasmo, ensamblador cruzado Z80 portable / portable Z80 cross assembler.

最後は、実行時に作成されたディスクイメージをエクスポートする方法です。
MSXPenでは、アセンブリソースは自動的にコンパイルされてPROGRAM.BINというファイル名で、BASICプログラムもAUTOEXEC.BASというファイル名でディスクイメージに保存されます。
そして、起動時にこのディスクイメージを読み込んで実行する仕組みになっています。
ですので、このディスクイメージを保存することで、配布や他のエミュレータなどでの実行が容易になります。

さいごに

ここまでで、BASICプログラムからマシン語の処理を呼び出して動かすことができました。
例にした逆スクロールもですが、フィールドマップ表示やフォント加工など、BASICでは時間がかかる処理を一部マシン語にすることで、それまでと動きが変わると思います。

最初からすべてをマシン語で書くのは大変ですので、このような一部の処理からマシン語で書いてみては如何でしょうか。

【Python】ソーサリアンのようなタイトル表示を実現してみる

 久しぶりの更新です。

 昔、主にPC8801mk2SRなどの8ビットパソコン用に、「ソーサリアン」というRPGが発売されていました。
 このゲームは、当時としてはとてもスケールの大きいものだったのですが、そのタイトル画面がシンプルなのに壮大で美しく、ゲームのイメージにピッタリのものでした。

 今回、このソーサリアンのようなタイトル表示をやってみましたので、解説します。
 言語はPython、モジュールはpyxelを使用します。

 なお、実際に動いているようすは以下になります。

厳密に言うと、PC88版というよりは、メガドライブ版に近いのですが、まあ雰囲気が良ければヨシ、ということで…

事前準備

事前に以下を準備します。

  • 元絵
  • バッファ(元絵と同じサイズ、このバッファの内容を画面に表示する)
  • 青色、緑色、赤色の各色要素のカラーリスト(後述)

処理説明

 全体の処理としては、以下のようになります。

  1. 明度インデックスをゼロに初期化
  2. 以下4回ループ (1) ループ回数によって処理開始位置を以下で設定する。
         1ループ目:X,Y
         2ループ目:X+1,Y+1
         3ループ目:X+1,Y
         4ループ目:X,Y+1
    (2) オフセットX座標をゼロに初期化
    (3) 以下、Y座標を1ドットおきに処理する。
        a. 以下、X座標を8ドットおきに処理する。
         (a) 元絵の処理対象座標+オフセットX座標のドットの色を取得
         (b) 取得した色が青要素のカラーリストに含まれていれば、
           青要素のカラーリストの明度インデックスの色でバッファにドットを描画
         (c) 取得した色が緑要素のカラーリストに含まれていれば、
           緑要素のカラーリストの明度インデックスの色でバッファにドットを描画
         (d) 取得した色が赤要素のカラーリストに含まれていれば、
           赤要素のカラーリストの明度インデックスの色でバッファにドットを描画
        b. バッファの内容を画面に描画
    (4) オフセットX座標を+2
    (5) オフセットX座標が8以下なら(3).に戻る
  3. 明度インデックスを+1
  4. 明度インデックスが5未満なら2.に戻る

 このうち、(a)〜(d)が描画色に関わる処理で、だんだん明るい色に描画していくための部分(描画色設定)になります。
 その他の処理は、1ドットおきにメッシュのパターンで描画する部分(バッファ描画)になります。
 どちらか一方でもフェードインの効果は出せますが、今回はこの2つを組み合わせて、じわ〜っとフェードインしてくる効果を出しています。

 以降、処理をバッファ描画と描画色設定に分けて解説します。

バッファ描画について

 まずはバッファ描画のアルゴリズムから解説します。
 イメージ的には、全体を2x2ドットで仕切り、それぞれの左上、右下、右上、左下の順にドットを埋めていく処理になっています。
 ただし、全体を一度に描画していくと味気なかったため、横8ドット単位で同時に描画して、少しづつ右に描いていくようにしています。  

  1. 最初は(X,Y)から横8ドット置き、縦2ドット置きに描画する

  2. オフセットX座標を+2して、同様に描画を繰り返す

  3. 横8ドット分の処理が終わった状態

  4. 次に(X+1,Y+1)を起点に、同様に横2ドットおきに描画していく

  5. 横8ドット分の処理が終わった状態

  6. 次に(X+1,Y)を起点に、同様に横2ドットおきに描画していく

  7. 横8ドット分の処理が終わった状態

  8. 最後に(X,Y+1)を起点に、同様に横2ドットおきに描画していく

  9. 横8ドット分の処理が終わった状態

 これで全部埋まりました。
 ここまでの一連の処理を、この後説明する明度インデックスの回数分繰り返していきます。
 なお、実際の実装では、毎フレーム処理すると速すぎるため、1フレームおきに実行しています。

描画色設定について

 続いて描画色の設定についてです。
 上記のバッファ描画でドットを描画する色について、段階的に明るくしていく処理になります。
 これは、全体のループ回数から、予めリストに定義しておいた描画色を決定しています。
 ループ回数が最初のほうが暗い色で青系に揃えておいて、明るくなるにつれて各色を発色させていくような定義にしています。

 ここからはpyxelを前提にした話になりますが、カラーパレットは標準で以下のようになっています。

pyxelのカラーパレットは16色なので、MSXでも同様の処理を作ることができると思います。 また、PC8801mk2SRなど8色でも、アナログパレットで同様な処理を実現可能かと思 います。

 このカラーパレットを元に、暗い色から明るい色への変化をカラーリストとして定義していきます。
 また、カラーリストは一度に全色分定義すると処理が複雑になるので、赤要素、緑要素、青要素に分けて定義します。

赤要素のカラーリスト:
緑要素のカラーリスト:
青要素のカラーリスト:

赤要素のカラーリストに8(赤色)などが含まれていませんが、これは実際に使用した画像では未使用だったためです。
汎用的に使える処理にするには、赤要素のカラーリストを(1, 2, 8, 9, 10) などにしたり、要素数を増やす必要があると思います。

先のバッファ描画の中でドットを描画する際には、以下の処理を行います。

  1. 各色要素のカラーリストに、元絵から取得したドット色が含まれているかを調べる
  2. 含まれている場合はその色要素のカラーリストから、現在のカラーインデックス(ループ数により0~4となる)のカラーコードで描画する

その結果、最初は全てカラーコード1で描画され、次は全てカラーコード5、その次から徐々に各色の色が付くような描画となります。

ソース(抜粋)

 これまで説明したアルゴリズムの実装コードを掲載します。
 実際にゲームで使っているもので、あまり良いコードではないですが、以下が毎フレーム実行されています。

    # タイトルロゴの色パターン 
    # 各要素は赤系、緑系、青系の順で定義している 
    TITLE_COLOR = ( 
        (pyxel.COLOR_NAVY, pyxel.COLOR_DARKBLUE, pyxel.COLOR_ORANGE, pyxel.COLOR_YELLOW, pyxel.COLOR_YELLOW), 
        (pyxel.COLOR_NAVY, pyxel.COLOR_DARKBLUE, pyxel.COLOR_GREEN, pyxel.COLOR_LIME, pyxel.COLOR_WHITE), 
        (pyxel.COLOR_NAVY, pyxel.COLOR_DARKBLUE, pyxel.COLOR_CYAN, pyxel.COLOR_LIGHTBLUE, pyxel.COLOR_WHITE), 
    )

    # タイトルロゴのフェードイン処理用 
    # タイトルロゴの座標、大きさ 
    TITLE_X = 0 
    TITLE_Y = 0 
    TITLE_W = 152 
    TITLE_H = 40

    # バッファの書き出し座標 
    TITLE_BUFF_OFFSET_X = 0 
    TITLE_BUFF_OFFSET_Y = 208

    # 処理中の色パターンインデックス
    title_color_idx = 0

    # 元画像の取得元ピクセル座標 
    title_get_x = 0 
    title_get_y = 0 

    # タイリング表示用のループカウント、0~3で1ループ 
    title_get_loop_cnt = 0

(中略)

    def update_title_fadein(self): 
        ''' 
        タイトルロゴフェードイン 
        '''
        if pyxel.frame_count % 2 == 0: 
            # タイトルロゴのピクセルを走査し、バッファに描き込む 
            for _y in range(self.TITLE_Y + self.title_get_y, self.TITLE_H + 1, 2): 
                for _x in range(self.TITLE_X + self.title_get_x, self.TITLE_W + 7, 8): 
                    # イメージバンク0の指定した座標のピクセルの色を取得する 
                    _pick_color = pyxel.image(0).get(_x, _y) 
                    # 取得した色が現在のからーグループ以降に含まれているかを調べる 
                    # 青系 
                    if self.TITLE_COLOR[2][self.title_color_idx:].count(_pick_color): 
                        pyxel.image(0).set(self.TITLE_BUFF_OFFSET_X + _x, self.TITLE_BUFF_OFFSET_Y + _y, self.TITLE_COLOR[2][self.title_color_idx]) 
                    # 緑系 
                    elif self.TITLE_COLOR[1][self.title_color_idx:].count(_pick_color): 
                        pyxel.image(0).set(self.TITLE_BUFF_OFFSET_X + _x, self.TITLE_BUFF_OFFSET_Y + _y, self.TITLE_COLOR[1][self.title_color_idx]) 
                    # 赤系 
                    elif self.TITLE_COLOR[0][self.title_color_idx:].count(_pick_color): 
                        pyxel.image(0).set(self.TITLE_BUFF_OFFSET_X + _x, self.TITLE_BUFF_OFFSET_Y + _y, self.TITLE_COLOR[0][self.title_color_idx]) 
                     
            # 横2ドット移動 
            self.title_get_x += 2 
            # 8ドット分処理したか 
            if self.title_get_x > 8: 
                # 8ドット分処理したら、次の処理に向けて準備する 
                self.title_get_loop_cnt += 1 
                # ループカウント1の場合:x+1ドット、y+1ドット目から処理する 
                if self.title_get_loop_cnt == 1: 
                    self.title_get_x = 1 
                    self.title_get_y = 1 
                # ループカウント2の場合:x+1ドット、y+0ドット目から処理する 
                if self.title_get_loop_cnt == 2: 
                    self.title_get_x = 1 
                    self.title_get_y = 0 
                # ループカウント3の場合:x+0ドット、y+1ドット目から処理する 
                if self.title_get_loop_cnt == 3: 
                    self.title_get_x = 0 
                    self.title_get_y = 1 
                # ループカウント4の場合:ループカウントをリセット、次のカラーグループでx+0ドット、y+0ドット目から処理する 
                if self.title_get_loop_cnt == 4: 
                    self.title_get_loop_cnt = 0 
                    self.title_get_x = 0 
                    self.title_get_y = 0 
                    self.title_color_idx += 1 
                    if self.title_color_idx >= len(self.TITLE_COLOR[0]): 
                        self.state = self.STATE_TITLE

さいごに

 解説は以上となります、参考になれば幸いです。
 説明が少々わかりにくい部分(特に描画色設定のあたり)があると思いますので、折を見て図などを追加できればと考えています。