FM TOWNSに元祖FPSゲーム「Wolfenstein 3D」を移植した話

1989年に富士通が発売したパソコン「FM TOWNS」に、1992年にid Softwareが発売した元祖FPSゲーム「Wolfenstein 3D」を移植してみました。意味が分からないと思いますが今は令和です。

Google ドライブ - FM TOWNS版Wolfenstein 3D 「Wolf4FMT」 実行ファイル & ソースコード

FM TOWNS マーティーを除くFM TOWNSシリーズ全機種で動作可能なはず。(追記:マーティーやメモリ2MB対応版作成)メモリ4MB以上、HDD必須、TownsOS V2.1以降向け。386 CPUでも一応動くがローレゾ|全画面設定では8~10fpsしかフレームレートが出ないと思います。FM TOWNS II HR (486 20MHz)以上でFASTモード設定起動を一応推奨。

FM TOWNSエミュレータ津軽」で互換BIOSを使用してCPU 66MHz設定(FM TOWNS II MX相当)のプレイ動画。YM3812用の音源データを無理やりYM2612(FM TOWNSFM音源)に流している関係上、ドラム音が無かったりおなしな鳴り方だがとりあえず音楽は鳴るし、効果音もバッチリだ。しかもオリジナル版には存在しない「Graphics」とかいうオプション項目を追加。我ながら謎の完成度。

ゲームを実行するには製品版Wolfenstein 3Dのゲームデータが必要です。SteamやGOGなどで売っている(MSストア版はデータがコピーできるか未確認)ので、持っている人は中の拡張子.WL6群(CONFIG.WL6除く)をWOLF4FMT.EXPと同じディレクトリに入れてFM TOWNS OS上で実行すれば遊べるぞ。

はい、そこ、わざわざそんな手間かかることするなら今どきのPCで「ECWolf」使って遊べばよくね?とかド正論はいちゃダメ。

 

ところでなんでこんなものを作ってしまったかというと、事の発端はFM TOWNS版DOOMを作ったときの記事でコメント欄に「Wolfenstein 3D」は移植できる?という質問が来たこと。

DOOMと違い、Wolfenstein 3Dは思い入れはないし、IBM PC特化のソースコードとなっていて移植が面倒だったのでやる予定はなかったのだが、DOSエクステンダを経由して32bitプログラムを動かしているFM TOWNSと同じようにDOSエクステンダで32bit化した「Wolf4GW」というコードが公開されていたのでこれをベースに移植していけばいけるのでは?とちょっと魔が差して挑戦してみたのが1年前。今じゃありません、1年前です。が、頑張ってみたもののどうやってもゲーム開始直後にフリーズしたり、グラフィックが分割されて表示されるバグが取れず、そのうち移植作業が面倒になって放置していたのだが、今年に入り「CELESTE」「くるんくる~ぱ」と作ってきて直接VRAMを弄ってFM TOWNSのグラフィック描画ができるようになったのだからもうちょっと頑張ればできるかも、となんとかやる気を振り絞って再開。

今まで弄ったコードを全部破棄してバグの原因をようやく見つけ、日に日に現れる新作ゲーム(「Dead Space(2023)」「ホグワーツ・レガシー」「龍が如く 維新 極」「バイオハザード RE:4」「ライザのアトリエ3」「Forza Horizon 5 Rally Adventure」)をプレイして何度も作業を中断させながらとりあえず遊べるレベルのフレームレート・音ありで動かせる状態にまでできました。

本当はFM TOWNS マーティーでも動かせるようにページング方式のデータ管理(メモリが少ない環境では一部のデータだけ読み込んでおいて、必要になったら入れ替える)方法を実装する予定だったのだが、正直思い入れもないWolfenstein 3Dを移植する作業自体がなかなかモチベーションが上がらないのに、これもまた思い入れもないFM TOWNS マーティーとかいう機種に対応させるのは面倒、仮に実装できたとしてCDからのデータ読み込みなのでゲーム中に何度も読み込みで停止してゲームにならないのが分かり切っているのでやめにしました。ソースコードはあるのでマーティー対応版が欲しい人は自分で作ってください(いない)。

追記:その後FM TOWNS マーティー対応版を作成

 

ちなみに、何故今まで動かなかったというと

fixed sintable[ANGLES+ANGLES/4];
fixed *costable = sintable+(ANGLES/4);

というSIN値、COS値を格納するグローバル配列の宣言があって、この記述の通りCOSの値はSINの一部と重複することを利用してSIN配列を共有してメモリ節約を元ソースでは行っているのだが、この記述はFM TOWNSのHigh Cコンパイラでは「costableの先頭のみに現時点のsintable+(ANGLES/4)の値が格納されるだけ」でSINと共有されない。

宣言時に同時にポインタ先を示す上記の書き方だと

fixed sintable[ANGLES+ANGLES/4];
fixed *costable;

*costable = sintable+(ANGLES/4)

のような解釈をされる様だ。

sintableの中身を初期化する関数内でcostable = sintable+(ANGLES/4);と記載したところ、見事動いてくれた。

 

ATMでもデジカメでもなんにでも移植されている「DOOM」と違い、見た目的に負荷も少なさそうだし、古いので実装も簡単そうなのに殆ど有志移植がない「Wolfenstein 3D」のコードを解説。

ゲーム自体はIBM-PC向けに出したもののNeXT Computer上でクロス開発を行っていた関係で機種依存となるような部分は極力抑えられている「DOOM」と違い、「Wolfenstein 3D」はゲームの実行環境も開発環境もIBM PCソースコードC言語なものの16bitプログラムを動かすDOS&80286環境ということで64KB毎にメモリ空間が分かれているポインタを操作する、高速化のためにx86アセンブラ部分は全体の10%以上、これも高速化のためにVGAグラフィック専用機能を使うという状況。数ファイル書き換えればそのマシン用に移植できるDOOMと違い、Wolfenstein 3Dは書き換え作業が相当面倒。

FM TOWNS移植版では32bit版となる「Wolf4GW」をベースにしたことで32bitメモリ空間をリニアに使え、面倒なメモリ操作部分や一部アセンブラ部分をCに置き換えられているので移植負荷が減ったが、それでもVGA専用機能を使った部分が面倒だった。

まずVRAM上でのピクセル毎の並び順が

0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F

とバイト毎に並んでいればいいのだがVGAだと256KBのVRAM空間を64KBでバンク分割されている関係でWolfenstein 3D側のデータは

0|4|8|C¥1|5|9|D¥2|6|A|E¥3|7|B|F (¥はバンク切替部分)

のような形となっているこれをそのまま「*vram++ = *source++;」とかいう形でVRAMに展開すると、

とそれっぽいもののメチャクチャな画像が表示される。

そこで

    int width_2 = width >> 2;
    int offset = VRAMWIDTH - width;
    int y2 = 0;

    for(y = 0;y < height; y++)
    {
        for(x = 0;x < width;x++)
        {
            *vram++ = source[(y2 + (x >> 2)) + (x & 3) *  width_2 * height];
        }
        vram += offset;
        y2 += width_2;
    }
}

 

という「(y2 + (x >> 2)) + (x & 3) * width_2 * height」と計算してからソース元のデータを取得することで

と正常に表示されるようになった。

 

高速化についてもVGAには1バイト書き込めばバンク切替せず別のバンクの同じアドレスにも同じ値を書き込める機能(つまり最大4ピクセル分一度に更新可能)があり、Wolfenstein 3Dでは左横側から縦一列ずつグラフィックを描画するので、もし次のピクセルでも同じグラフィックが描かれることが分かった場合、

if ( (pixx&3) && texture == lasttexture) //前の縦列のテクスチャと差異がない場合
{
        postwidth++;
        wallheight[pixx] = wallheight[pixx-1];  //前の壁の高さをそのまま入れる
        return;
}
ScalePost(); //壁を描画する関数。wallheight配列の高さ分描かれる
wallheight[pixx] = CalcHeight(); //主人公の視点からの壁までの奥行きから描く壁のピクセル数を計算して入れる
postsource+=texture-lasttexture;
postwidth=1;
postx=pixx;
lasttexture=texture;
return;

というように壁描画関数(ScalePost)自体を飛ばして、描画関数内では「VGAMAPMASK(masks[postwidth-1][postx&3]);」と記載したマクロでバンク同時書き込みのレジスタを変更して、後から1~4列分書いてしまう遅延描画を行っている。

だが、バンク切替が不要でリニアにVRAMが並んでいるFM TOWNSにはもちろんこんな機能はない(あるのは指定ビットの書き込みを禁止するマスク機能のみ?)。

この時点で同じCPU速度だったとしても速度が出ないとわかるので、別の手段で高速化。

画質は諦めて横解像度を半分として1バイト毎に書き込まず一気に2バイト書き込んでしまう、更にテクスチャ座標の取得方法も変更したローレゾモードを新設した。

void ScalePost_Low(void)
{
    _Far word *vram;
    _FP_SEG(vram) = 0x10c;
    _FP_OFF(vram) = 0x0;

    int ywcount, yoffs, yendoffs;
    fixed frac, fracstep;
    int height;
    word col;
    height = wallheight[postx];

    ywcount = height >> 3;

    fracstep = 16777216 / height;
    yendoffs = (viewheight >> 1) + ywcount;
    if(yendoffs >= viewheight)
    {
        yendoffs = viewheight;
        frac = (((height >> 2) - viewheight) >> 1) * fracstep;
    }else
    {
        frac = 0x4000;
    }

    yoffs = _max(((viewheight >> 1) - ywcount), 0);

    ywcount = yendoffs - yoffs;

    vram += ( (yoffs * SCREENWIDTH_WORD + (postx >> 1)) + (vbuf >> 1)); // << 9 = * SCREENWIDTH_WORD
    do
    {
        *((byte *)&col+1) = *(byte *)&col = postsource[frac >> 16];
        *vram = col;

        vram += SCREENWIDTH_WORD;
        frac += fracstep;
    }while(--ywcount);
}

テクスチャ解像度(64ドット)/壁の高さ(wallheight)でピクセル毎の小数ありの移動ドット数(fracstep)を割り出して描画ピクセル毎に加算→整数部のみ切り出して求めることができるが、16777216という数字はテクスチャの解像度が64ドット、壁の高さピクセルを格納するwallheightが小数部3ビットの固定小数点、更に桁数を高めて精度を上げるためにこの数字にしている(小数部16ビット)。

精度の問題で並びがズレて少しジャギーが出てしまうが、解像度自体縮小している関係でそこまで目立つような感じではなかった。主人公と壁との距離を求めるレイキャスト自体の計算自体端折っているが、これだけで一気に2倍近くフレームレートが向上。一応オリジナル通りの解像度と描画方法にも切り替えることができる(GraphicsオプションからLow-Res Modeトグルを切り替え)

新設したグラフィックオプション

 

オリジナル通りの解像度

 

ローレゾモード

壁が若干荒くなるものの距離によっては目立たないし、壁以外のスプライトは解像度は変えていない。