1989年に富士通 が発売したパソコン「FM TOWNS 」に、1992年にid Software が発売した元祖FPSゲーム 「Wolfenstein 3D」を移植してみました。意味が分からないと思いますが今は令和です。
Google ドライブ - FM TOWNS版Wolfenstein 3D 「Wolf4FMT」 実行ファイル & ソースコード
FM TOWNS マーティー を除くFM TOWNS シリーズ全機種で動作可能 なはず。メモリ5MB以上、HDD必須、TownsOS V2.1以降向け。386 CPUでも一応動くがローレゾ|全画面設定では8~10fpsしかフレームレートが出ないと思います。FM TOWNS II HR (486 20MHz)以上でFASTモード設定起動を一応推奨。
VIDEO
FM TOWNS エミュレータ 「津軽 」で互換BIOS を使用してCPU 66MHz設定(FM TOWNS II MX相当)のプレイ動画。YM3812用の音源データを無理やりYM2612(FM TOWNS のFM音源 )に流している関係上、ドラム音が無かったりおなしな鳴り方だがとりあえず音楽は鳴るし、効果音もバッチリだ。しかもオリジナル版には存在しない「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からのデータ読み込みなのでゲーム中に何度も読み込みで停止してゲームにならないのが分かり切っているのでやめにしました。ソースコード はあるのでマーティー 対応版が欲しい人は自分で作ってください(いない)。
ちなみに、何故今まで動かなかったというと
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トグルを切り替え)
新設したグラフィックオプション
オリジナル通りの解像度
ローレゾモード
壁が若干荒くなるものの距離によっては目立たないし、壁以外のスプライトは解像度は変えていない。