古典的タスクシステムとギャラクシアンという2つのキーワードがテーマです。
「デジタルコンテンツ制作の先端技術応用に関する調査研究 報告書」という、2009年の時点でのゲーム開発に関する様々な情報がまとまっている資料があります。ネットからでもPDFファイルとして閲覧可能です。
「デジタルコンテンツ制作の先端技術応用に関する調査研究 報告書」
この資料のP162から数十ページにわたり、ゲームにおけるタスクシステムについて、タスクシステムの仕組みだけでなく、その歴史について書かれています。そこから、初期のタスクシステムについて記述されている箇所を引用します。
タスクシステムが使われたプログラムとして、Webサイト「Logician Load」(Peto氏)で紹介されている。同サイトでは、ギャラクシアンで使われたであろうタスクシステムが、リバースエンジニアリングによって、後のTCBを使ったシステムに発展したと記している。(ただし、本当にギャラクシアンがタスクシステムを使用したかどうかについては、確認はとれていない。)
Peto氏のWebサイトは現在では閉鎖してしまっているのですが、Internet Archiveで閲覧することができます。コンピュータゲームのからくり
そこから引用すると…
タスクの歴史は古く、NAMCOがギャラクシアンを作るときに考案されたとの説が有力ですが、定かではありません。
と書かれています。昔に私もこのページを見たことがあり、『ギャラクシアンからなのかぁ』と記憶していました。
そのサイトに記述されているTCBがどのようなものかを、失礼ながらも大雑把にまとめると、タスク単位で固有のメモリ領域と実行すべきプログラムのアドレス(ポインタ)を管理し、それらをリストかスタックのような形式で繋いで、順次実行するような仕組みということになります。
Linuxでシステム寄りのプログラムを書いたことのある人なら、TCBやPCB(プロセスコントロールブロック)という名前になじみがあると思います。UNIXのようなOSでは1970年代頃には使われていた仕組みのようです。MS-DOSやHuman68KにもPSPやPCBがありますね。
ギャラクシアン説は、ゲーム開発系の書籍でも触れられています。
Windowsプロフェッショナルゲームプログラミング2では…
「ゲームの世界では、1回の描画のことを1フレームと呼びます。~中略~ つまり、あらゆるキャラクターは、フレーム単位で移動します。通例、1フレームごとに、キャラクターの移動関数が1回呼び出されます。移動関数は、1回分の移動処理を行ったあと帰還します。この処理を実現するために、さまざまな方法が編み出されました。その歴史の教科書を紐解けば、古くはNAMCOのギャラクシアンにまで遡ると言われています」と書かれています。
筆者さんは、フレーム単位で処理を回していくという方式はギャラクシアンから、と説明されています(断言はしていません)。また、この本では、「~ギャラクシアン移行主流となったのは、この章で紹介する古典的タスクシステムでした」と続いていて、Peto氏のタスクシステムと同じようなTCBを使った方式(とその発展系)の実装と解説をしています。
ギャラクシアン以前にフレーム単位で処理を回す、つまり、VSync(VBlank)周期で動作していていたゲームがあったかどうか私は知りません。ただ、MAMEのDriverソースをみた感じでは、1978年頃のゲームでもVBLANKで割込みをかけているタイトルはあるように思えます。
他の書籍として、シューティングゲーム プログラミングでは…
「タスクシステムの由来についてははっきりしませんが、ナムコのギャラクシアンで開発されたという説があります」
と書かれています。この本でのTCBの実装方法は、タスク単位でワークエリアと実行すべき処理へのポインタを管理し、それらをリストで持つ方式です。
ところで、「デジタルコンテンツ制作の先端技術応用に関する調査研究」では、実際にナムコで開発に携われていた方へのインタビューも掲載されています。
P301ページからの、タスクシステムについての黒須一雄氏へのインタビューという内容で、ナムコで使われていたタスクの管理方法について書かれているのですが、ナムコ内ではタスクコントロールではなく、ジョブコントローラー(ジョブコン)と呼ばれていたことが読み取れます。
また、タスク(ジョブ/オブジェクト)の管理方法は、リスト構造を取りつつも、タスクの切り換え自体は、各タスクがスイッチさせるという方法になっています。Windows3.1やMacOS9の頃までに使われていた疑似マルチタスクのような実装方式で、概念としてはコルーチンのようなものでしょうか。
インタビューを読んだ印象としては、前述のTCBとは名前や仕組みが異なるようです。名称はともかく、管理方法は似てるような、微妙に似てないような感じがします。アセンブラとC/C++では扱いやすいデータの管理方法やプログラムの記述方法が変わってきますから、その辺の違いかもしれません。
前置きが長くなりましたが、ここからが本編です。
実際のギャラクシアンでは、どのようなタスク管理方法をとっていたのかを調べてみました。プログラム全体の1/3くらいしか読んでいませんが全体像は掴んでいます。あと、バージョン違いで管理方法が違うかも知れませんし、読み違いもありえるので参考程度ということで。
結論から書くと、ギャラクシアンでは、ジャンプテーブルとフラグによる分岐が主な処理方法でした。各種データの管理は固定サイズの配列で、これをループで順に処理していますから、いわゆる古典的タスクシステムは用いていません。
主処理
ギャラクシアンのCPUはZ80です。Z80でのジャンプテーブル処理ではJP (HL)が使われる事が多いですが、ギャラクシアンでは主にRST命令を使っています。
ORG $28 ADD A,A POP HL ; スタックから戻り先アドレスを取り出してテーブル先頭アドレスとして使う LD E,A LD D,00H ADD HL,DE LD E,(HL) INC HL LD D,(HL) EX DE,HL JP (HL) ; ------------------------- LD HL,RETADDR ; 戻り先 PUSH HL LD A,($4005) RST $28 DW $00E6 ; 処理先1 DW $0156 ; 処理先2 DW $03F2 ; 処理先3 DW $0536 ; 処理先4 DW $077B ; 処理先5
処理先では更にジャンプテーブルを用いて分岐している場合もあります。パラメータ(ここでは$4005に書かれている1バイトの値)を使ってテーブルを参照しています。
ゲーム中の主処理はこんな感じです。
; ゲーム中の処理 CALL Z0079 ; 自機操作と表示位置更新 CALL Z0080 ; 自弾の移動処理と画面外判定等々、ショット周りの処理 CALL Z0081 ; 敵弾の移動処理 CALL Z0034 ; スプライトで表示する敵の処理 CALL Z0033 ; 敵ワークエリアの位置・表示物情報をスプライトバッファへ設定 CALL Z0082 ; CALL Z0083 ; CALL Z0084 ; CALL Z0085 ; まだ読んでないけど、たぶん、自弾と降下中の敵との接触判定 CALL Z0086 ; CALL Z0087 ; 自弾を消すように指示されてるか調べて消す CALL Z0088 ; CALL Z0089 ; CALL Z0090 ; CALL Z0091 ; CALL Z0092 ; CALL Z0093 ; CALL Z0094 ; CALL Z0095 ; CALL Z0096 ; CALL Z0097 ; CALL Z0098 ; CALL Z0099 ; CALL Z0100 ; CALL Z0101 ; CALL Z0102 ; 降下中BOSS撃墜による硬直タイム減算処理 CALL Z0103 ; DEMOプレイ中の自機の動き???
これは、CALL命令の箇所だけを抜粋したのではなく、実際にCALL命令が上記のようにずらっと並んでいます。これがゲーム本体主要部のほぼ全てのプログラムです。処理の内容は、ゲーム中に起きる全ての処理を順番に呼び出しているというものです。各処理先では、今、この処理を実行すべきかどうかをフラグで判定しています。例えば、自弾の発射ルーチンの先頭では、自機が破壊されている状態だと自弾を発射できないので、自機の破壊状態フラグ(グローバル変数)を調べて即座にRET、といった具合です。
プログラムの全体象は、VBLANK割込み時にワークエリアからスプライトやBG面への転送を行った後に、ゲームの主処理を実行します。非割込み時は何をしているのかというと、敵編隊や文字列といった表示周りのイベント受付をしています。つまり、割込み中のゲームの主処理でイベントを発行し、非割込み時のメインルーチンでまとめてイベントを消化していきます。
Z80のメインループ(非割込み時)自体はほんの数行の無限ループ処理で、ゲームの主処理は割り込みルーチン側にあるという作りになっています。
敵編隊
画面上部に表示されている敵編隊は二次元配列です。縦横6×10の編成ですが、行番号・列番号の計算をし易くするためか、メモリ上では横16バイトとして管理しています。敵の編隊状態は存在するかしないかの1Bitの情報ですが、贅沢にも敵1機につき1Byte使っています。ちなみに縦方向は8行分あるので、ちょっと書き換えると8×16編成も可能です。敵の種別は行位置で決まります。
プログラムでは、この編成配列全体をループで処理しています。(つまり、1キャラ=1タスクではないということです)
降下攻撃の敵
ギャラクシアンでは最大で7(8?)機の敵が同時に攻撃してきます。1機あたり32バイトのデータで管理されています。(ところどころ抜けているのは未調査)
+00D: bit0:画面外退去状態 +01D: bit0:爆発状態 +02D: 状態番号(Jump table index) +03D: X座標 +04D: Y座標 +05D: スプライトで表示するキャラクタコード情報(そのままでは使わないで加工されるので注意) +06D: 旋回時の回転方向(00 or 01) +07D: 編隊中の行列位置を表す(上位4bitが行で下位4bitが列) +09D: 波状動作の基準Ypos +14D: スプライトで表示するキャラクタコード情報(+05D)に、加算される値(通常敵:00, BOSS:18hのいずれか) +16D: アニメーションカウンタ +17D: アニメーションカウンタ(スケール) +18D: Sprite Code +19D: 旋回処理時の移動増減値テーブルインデックス +22D: Sprite Palette +24D: 左右移動時の激しさ(00-03の範囲が許容値だがデータ上は01-03の範囲) +25D: 波状移動演算用 +26D: 波状移動演算用 +27D: 波状移動演算用 +28D: 波状移動演算用
C言語でいうところの構造体みたいなものですね。このデータが8機分用意されていて、敵の処理は8個のワークエリアを順次処理しています。
; スプライトで表示する敵の処理 LD IX,042B0H ; 敵のwork先頭 LD DE,00020H ; work size LD B,08H ; 敵の数 LOOP: EXX CALL ENEMY_PROC ; 状態別処理を実行 EXX ADD IX,DE DJNZ LOOP RET
敵の各種処理(移動や弾の発射等々)は、ワークエリアの+4Byte目にある値を使ってテーブルジャンプしています。処理先でこの値を変えることで処理内容を変えていきます。
まとめ
ギャラクシアンでは、最初に述べたようなワークエリアと処理アドレスポインタをリストで持つようなTCBを用いたタスクの管理ではなく、フラグとテーブルジャンプとVBLANK割込み単位での動作という方式で実装されていました。つまり、ギャラクシアンではジョブコンは使われてないということになります。ギャラクシアンのような処理方式がギャラクシアン以前から使われていたのかどうかは、それ以前のゲームのプログラムを調べてみない事にはわからないです。
ギャラクシアンの発売は1979年です。Z80を搭載したMZ-80が1978年、PC-8001が1979年に発売されたような時代です。MAMEのデータベースをみると、ギャラクシアンと同時期かそれ以前にZ80を使ったアーケードゲームはほとんどなかったようですから、ギャラクシアンが1979年当時のゲームシステム/プログラムとして画期的で先駆けであり、他社がギャラクシアンのプログラムを参考にしてゲームシステムの実装方法を発展させていったというのは充分にありえる話だと思います。
ちょっと前に趣味で読んだドンキーコング(池上通信/Z80/1981年)も、テーブルジャンプ、構造体、主処理がイベント待機、ゲーム本体がCALLだらけという作りでした。25面を遊んでる時でも50,75,100面の各処理を実行してますし。
ゲームではなく、コンピュータプログラミングにおけるデータを構造体の形式として管理する方法の歴史はもっと古く、1960年代には既にあったようです。もう話が昔過ぎてわかりません。
[2015/3/3 追記]
シューティングゲームサイド Vol.7に、ギャラクシアンを作った男たちという記事が掲載されています。ギャラクシアンの開発に携われた石村繁一さんと田城幸一さんへのインタビュー記事で、開発当時の事を克明に語られていて、ハードソフト両面の技術的な説明もあり、読み応えのある内容です。
その記事中では、ジョブコンについても触れているので、引用させて頂きます。
田城) 『ギャラクシアン』開発時にはまだ「ジョブコン」は無かったと思います。「ジョブコン」を開発したのは深谷正一さんで、私は使用したことがありません。
というわけで、開発者自ら、ギャラクシアンにはジョブコン(タスクシステム)が使われていないことを明言されていました。
[2015/10/11 追記]
アーケードゲーム『バラデューク』30周年イベントの岸本好弘氏のトーク内において、ジョブコンは深谷正一氏が作られたという話が出てきます。バラデュークではジョブコンが使われていたのですね。
おまけ
ギャラクシアンで使われている乱数処理は線形合同法でした。ナムコ作品で見る乱数の歴史で触れられてる8ビットLCG(5+1)です。背景の星はLFSRなのですね。
LD A,($0401E) LD B,A ADD A,A ADD A,A ADD A,B INC A LD ($0401E),A RET