1995年、セガサターンでのプログラミング

この記事は、1995年にNeversoft社で初となるゲーム「Skeleton Warriors」を開発した際に書いたドキュメントです。これは、私が68Kアセンブリ言語を使わずに作った初めてのゲームです。

こちらの写真から、当時の仕事環境を窺い知ることができます。右側にあるのがセガサターン用ソフト開発キット(”スモールボックス”とICEで構成されています)です。

ゲームの状態

以下のドキュメントで、セガサターン用ゲームソフト「Skeleton Warriors」用に書かれたコードを簡単に説明します。また、その中には現在でも使われている方法が多くありますので、そのいくつかをご紹介します。

このドキュメントの主な目的は、それぞれのモジュールの働きと、その対話方法を説明しながら、DanとKen、Jamesに現代のコードについて効率的に理解してもらうことです。またMick(筆者)が書いた残念なコードを振り返り、自分自身の気を引き締めたいと思っています。

更に、プログラムへのデータ(.GOBと.GOBファイル)の取り込みについても詳細を少し説明し、将来的にどう扱うかについて説明していきます。

ハードウェアの開発

今回、ターゲットにするマシンはセガサターンです。これには、2つの「SH-2」というRISCマイクロプロセッサと1つの68000が搭載されています。現在は、マスタのSH-2のみを使っていて、スレーブのSH-2は時間に余裕があれば使うことになるでしょう。68000はサウンドチップを動作させるために使われていますが、セガが提供しているサウンドライブラリを使うので、これに対するコードは書かなくてもいいでしょう。

ほぼ全てのプログラムが、ただのC言語で書かれています。また、SH-2アセンブリのアウトプットを生成するために、GNUのSH-2コンパイラが使われています。SH-2モジュールもいくつか使われていますが、そのほとんどで純粋なデータに限定されています。私は、今までにSH-2の重要な部分について何かを書いたことはありません。

私たちが使っている開発システムはPsyQです。これは、セガの標準的な開発システムではありませんが、利用した人たちはみんな、最高のシステムだと言います。代替システムはセガの子会社Cross Productsが開発したSNASMです。セガが提供するコードのほとんどは、SNASM開発システムで実行されることが前提になっていますが、簡単にPsyQとの互換性を保つことができます。

PsyQシステムには、PCにインストールしたSCSIインターフェースボードと、セガサターンに接続するカートリッジ、そしてその2つをつなぐケーブルが含まれています。ソースはPC上でコンパイルされ、実行機であるセガサターンにダウンロードされます。コードをPCからデバッグすることも可能です。

この接続は、マシン間のコミュニケーションを扱うTSRプログラム(PSYBIOS)によって制御されています。これによって、セガサターンは、CDからファイルを読み込むのと同じようにPCからファイルを読み込むことができます。この機能を利用し、各レベルでファイルを読み込むようにしています。

Mickの部屋には他にも大きくてうるさい筐体が2台あります。PCも2台保有しています。小さい方の筐体はE7000PC。インサーキット・エミュレータを備えたSH-2です。これはPsyQデバッガが停止しなかった場合に、プログラムがどこでクラッシュしたかを調べるのに役立ちます。メモリの書き込みを監視するのにも便利なのですが、まだその機能はあまり使ったことはありません。

2つめのうるさい筐体は「スモールボックス」として知られるものです(オリジナルの「ラージボックス」は小さい冷蔵庫ぐらいのサイズでした)。基本的にこれはE7000とCDエミュレータのためのインターフェースをサターンに追加したものです。国別コードを変更するためのスイッチと、PALとNTSCの切り替えスイッチが正面についています。

CDエミュレータはもう片方のコンピュータに入っているもので、コンピュータのハードディスクを仮想的にCDドライブとみなすための大きな基盤です。CDイメージを作成し、実際にCDでプレイした時にどうなるかリアルタイムモードでエミュレートできます。これでほぼうまくいきそうですが、まだいくつか問題があり、セガと協力して解決しようとしています。

コンパイルとリンク

最終的なプログラムの構造全般は1つのMakefile、すなわちMAKEFILE.MAKによって制御されています。ここにはプロジェクト全般の依存関係とターゲットが含まれていて、.GOBと.GOVファイルのコンパイルも含まれています。

個々のCのソース(.C)モジュールは、プログラムCCSH によって、SH-2(.OBJ)オブジェクトモジュールにコンパイルされます。最初に(C:\GNUSH2\BIN内の)GNU CプリプロセッサCPPSHを呼び出し、その後このアウトプットの中のCC1SHを呼び出し、SH-2アセンブリコードを生成します。最後に(C:\PSYQ内の)ASSHを呼び出し、これを最終的なオブジェクトフォーマットへとアセンブルします。

法外に大きなオブジェクトファイルを生成すると聞いていたので、C++は使わないことにしました。まだC++を試してみたことはありません。//でコメントアウトするなどの機能は便利そうなので、どうぞご自身で試してみてください。

私たちが使用したSH-2アセンブリ言語ファイル(.S拡張子)は、ASMSH(ASSHと同じものではなく、より複雑なマクロアセンブラ)を使って直接.OBJファイルにシンプルにアセンブルされます。この時点では、単にデータの組み込みに用いられ、マシン特有のコードはありません。

コードを格納しておくためのサターンのRAMは、1メガバイトのチャンク2つに分かれています。1つはアドレス$06000000、もう1つは$00200000にあります。$00200000の方のチャンクは主としてメインキャラクタの画像を格納するのに使用され、プログラムコードは実際には$06010000に格納されます(最初の$10000バイトはシステムスペースやスタックなどに使われます)。

コードは位置に依存し、特定のアドレス($06010000)で実行されるようコンパイルされます。他のアドレスでは実行できません。

.OBJファイルはPSYLINKプログラムとリンクして、MAIN.CPEというファイルを生成します。これは小さなヘッダを備えた実行可能なプログラムで、RUNプログラムと一緒にサターンにダウンロードすることができます。PSYLINKはTEST.LNKというファイルを使って、どの.OBJファイルを採用し、どこに格納するのかを特定します。

データ

ゲームはいくつものレベルに分割されています。異なるレベルで同じデータを利用することも多いですが、ほとんどはレベルによって異なっています。各レベルで利用するデータは最終的に2つの巨大なファイル、.GOVと.GOB(鉱山であればMINE.GOVとMINE.GOB)に集約されます。.GOVファイルは短いヘッダと、ビデオメモリに格納する必要がある全データを含んでいます。.GOBファイルはRAMに格納されるべき全データを含んでいます。

各レベルは以下に挙げるデータファイルから構成されます。

  • .SSQ – スプライトシーケンスファイル。

  • .SBM – ビットマップの背景に用いられるビットマップファイル。

  • .MAP –キャラクタがマップされた背景の両方のマップ。

  • .TIL – キャラクタがマップされた背景のタイルセットとパレット。

  • .PTH –パスとトリガポイントのためのデータ

  • .TEX – パスのテクスチャ。

  • .SSQと.SBMファイルはMickが開発したシーケンサ”SEQ”によって生成されますが、使えば使うほど的外れ。

  • .MAP、.TIL、.PTHと.TEXファイルはDanのマップエディタ”TULE”によって生成されます。これは使えば使うほどすばらしい。

これらのファイルはASMSHアセンブラにより、適切な.GOVと.GOBファイルにアセンブルされます。LEVEL.SとLEVEL1.Sというファイルを参照して、どのようになっているか見てみてください。レベル固有のデータは.GOVファイルに格納されているものもあります。

モジュール

  • TEST.S – いくつかのラベルを設定するだけのもの

  • MAIN.C – プログラムの最上位レベル。ハードウェア初期化、レベルの設定、レベル別プレイコードや、他の雑多な物はより適切なモジュールに格納されるべきでしょう。このモジュールに何かを入れておくのが一番楽なので、簡易版のテストのために、ゴミもかなりたくさん入っています。CDやPCのファイルサーバから起動するためのコードや、TIMINGカラーバーをオンオフするフラグも含まれています。

  • GFXLIB.C – ハードウェアにアクセスし、グラフィックス機能を実行する様々なルーチン。ほぼスクラッチからDanがプログラムしたもので、往々にして非効率です。ここのルーチンを多用するなら、何をしているのかよく見て、コードを書き直して高速なバージョンを作成した方がいいかもしれません。

とは言え全ての機能は正常に動作しますし、最初の実装やテストをするには十分に優秀なフレームワークを提供するものです。この偉業を成し遂げたDanをほめてあげてください。

  • SMP_PAD.C –サターンのジョイパッドを読み込むためのもの。ハードウェア依存の高いルーチン。

  • GLOBALS.C – 全てのグローバル変数といくつかの一般関数。グローバル変数の利用はプログラムの演習には適しています。しかし、SH-2にグローバル変数を実装すると遅くなるので、いずれグローバル構造体に変換するかもしれません。

MANPATHの状態を記述した変数を含んでいます。

  • MAN.C – 動きを処理し人物(Prince Lightstar、Talyn、Guardian or Grimskullなど、制御するキャラクタ)を表示します。現状では、このモジュールはほとんど人物を動かし、パスと衝突させるためのロジックです。各アクションに対しての適切なアニメーションも提供します。これに関してはまだやることがたくさんありそうです。

  • OB.C – ゲーム内のオブジェクト、特にSkeleton Warriorsやエイリアンなど敵キャラのオブジェクトの動きと表示を制御します。敵のAIと基本的な動きとトリガに関して言えばここで多くのゲームプレーがプログラムされます。データ構造はまだ固まっていません。特に衝突とアニメーションノ問題は完全には解決されていません。まだやることがたくさんあります。

  • DATA.S – 様々なテーブル。現状、メインキャラクタのアニメーションが主。

  • LAYER.C – パララックス背景のスクロール。キャラクタがマップされた背景を更新し、ビットマップをスクロールします。また、フォグレイヤ上のラインスクロール(波のようなエフェクト)も行います。現状、キャラクタがマップされたレイヤのマップは圧縮せずに保存されています。これは、genesis版で使っていたRLEフォーマットで圧縮する必要があります。Sonyより前にサターンの開発版システムを入手できたら、Kenにやってもらうかもしれません。

  • PAL.C – パレット。色は2048色から選べます。画面のピクセルはこの中からどんな色でも選ぶことができます。私は理論的にこれを256色のパレット8つに分割しました。PAL.Cはこれらを初期化、設定、サイクルを供給するコードを備えています。フェーディングやより複雑なサイクル、輝度の設定なども必要になります。

  • BUL.C –ブレット(ソードで切る、手で打つ、リストロケットなど)を別のオブジェクトとして制御する初期の設定。ブレットの複雑な使用のためにもっと作業が必要。適切な衝突とアニメーションのコードが必要。

  • PAD.C – ジョイパッドの状態を記憶するシンプルなモジュール。ボタンが最近押されたか、または今押されているかを記憶する。

  • START.C – 初級レベルが何かを1行で表示する。バッチファイルで簡単に変更できるようにしている。

  • PANEL.C – パワーバーを表示するシンプルなルーチン。

  • PATH.C – パスを描画する巨大なルーチンで、パスの衝突判定も行う。

  • MATH.C – シンプルなサイン、コサイン、ある角度での回転ポイント。

[アップデート]以下にMAN.Cのサンプルコードを掲載します。全てがハードコードされていて、グローバルの”Man”データ構造を参照しています。ハードコードされた数値も多いです。

/**************************************************************/
/* Trigger jumping if needed, also variable height jump logic */

Man_JumpTrigger()
{
  if ( Man.JumpFudge )
  {
    Man.JumpFudge--;
  }

  if ( Man.Mode != M_Crouch || Man_StandingRoom() )    // ok if not crouched, or there is headroom
  {
    if (Pad_Jump->Pressed)               /* jump button pressed */
    {
      if ((Man.Contact || (Man.Mode == M_Hang) || Man.JumpFudge) && Pad_Jump->Triggered && !Man.Blocking) /* and not already jumping */
      {
        if (Man.Mode == M_Hang && Pad1.Down.Pressed)
        {
          Man.Contact=0;
          Man.Mode=M_Jump;
          Man.AnimBase = LS_Jumping;    /* Change base anim to jumping */
          Man_TriggerSeq(LS_Jump);    /* start the jumping start anim */
          Man.YV.f = 0x10000;           /* and have no YV */
          Man.Y.i += 4;           /* and have no YV */
        }
        else
        {
          Pad_Jump->Triggered = 0;
          if ( !JetPacCheat )
            Man.YV.f = -0x00080000;     /* Initial jump speed */
          else
            Man.YV.f = -0x00008000;     // Initial speed in Jetpac mode
          Man.Contact = 0;          /* not on the ground any more */
          Man.JumpTime = 0;         /* just started jumping */
          Man.AnimBase = LS_Jumping;    /* Change base anim to jumping */
          Man_TriggerSeq(LS_Jump);    /* start the jumping start anim */
          Man.XV.f+=Man.FlyVel;

          if (Man.HangEnd && Man.Mode == M_Hang)  // if hanging
          {                   // and on the end of a path
            Man.HangEnd = 0;
            Man.X.i += 12*Man.Facing; // the move past end of path
            Man.JumpTime = -3;      // bit more fixed v jump time
          }
          Man.Mode = M_Jump;    /* change mode to jumping */

        }
      }
      else                        /* Already jumping */
      {
        if (Man.JumpTime++ < MaxJumpTime) /* Still in initial jump period */
          Man.YV.f -= 0x0005000;        /* So can maintain jump YV */
      }
    }
    else                      /* jump button not pressed */
    {
      Man.JumpTime = MaxJumpTime+1;     /* so can't alter YV again until landed */
    }

  }

}

OB.Cファイルが、ゲームの個々のオブジェクトの行動パターンを全て含んだ9000行の巨大ファイルにまで大きくなってしまいました。以下のような、ハードコードされた数値も大量にあります。

Drop_Arac(S_Ob *pOb)
{
  int t;
  if (pOb->Jump==1)
  {
    pOb->yv.f+=0x7fff;
    pOb->y.f+=pOb->yv.f;
    t=Path_GetYZ(pOb->x.i,pOb->y.i,pOb)-15;
    if ((t>pOb->y.i)&&(ty.i+20))
    {
      pOb->Jump=0;
      pOb->y.i+=15;
      Turn_Around(pOb);
      pOb->SeqFile=Sprites[SpriteMap[34]];
      Object_TriggerSeq(Arac_JumpLand,pOb);
    }
  }
  else
  {
    if (pOb->Frame==16)
      pOb->Jump=1;
    if (pOb->AnimStat==AnimDone)
    {
      pOb->t1=0;
      pOb->Mode=&Pattern_Arac;
    }
  }
  Command_Arac(pOb);
}

ひどいものですね。ゲームがとても小さく、68Kから脱却したばかりの時代には、このコードスタイルでもまだ大丈夫だったのです。