このエントリーはC++アドベントカレンダー23日目の記事です。
古(いにしえ)のゲームを発掘したけれど動かない
クラウドストレージに放り込んでいたデータの整理をしていたところ、20年以上前に自作したゲームが出てきました。
C++でWin32APIとDirectXを使って開発したWindowsゲームです。
確かWindows7までは動いた記憶があります。今の環境(Windows11)でも動くのかと試しに実行してみましたが、動きませんでした。(互換モードなら動くとの情報もあり、こちらも試しましたがダメでした。)
では、今の環境で再コンパイルしたら動くのだろうか?をふと試したくなりました。
動かなくなったゲームを再コンパイルして復活させよう!
という事で、この記事では発掘したソースコードを今の環境で再コンパイルして動くか?を試してみたいと思います。
勢いでC++のアドベントカレンダーに投稿してしまいましたが、ここ10年は触っていない「C++何もわからない」おじさんのネタ記事です。
ゲームを作った当時は「C++完全に理解した」と思っていた気もしますが🐶
発掘したゲームのソースコード一式
とりあえず、GitHubに上げました。
(下記リンクは修正前のコードが入ったコミット)
- C++: ソースコード(.cpp、.h)
- mapData: ゲーム内の各ステージのマップ情報
- bitmap: タイトル画像、キャラクタなどの画像データ(.bmp)
- mds: BGM用MIDIストリームファイル(.mds)
- waveData: 効果音データ(.wav)
- ゲームの実行ファイル(.exe)
当時のメモ書きを見てみると、当時の開発環境とゲームの動作環境は下記とのこと。恐ろしいほど低スペックですね。
開発環境
- OS:windows98
- CPU:celeron 333MHz
- メモリ :32MB
- ビデオメモリ:4MB
- directX:ver5.0
- コンパイラ:Microsoft visual C++5.0
動作環境
- OS:windows98以降
- CPU:Pentium133MHz以上
- メモリ:32MB以上
- VRAM:2MB以上
- DirectX:ver.5.0以上
- グラフィック:解像度640×480, 256色
とりあえずビルドしてみよう
今の開発環境
- Windows11(64bit)
- VisualStudio 2022
- DirectX 12 (がインストールされていました)
1. Visual StudioでC++のプロジェクトを作る
C++空のプロジェクトに.cppと.hを放り込む
-
発掘したソースコード一式をプロジェクトフォルダに入れ、ソースファイル(.cppと.h)をプロジェクトに登録
プロジェクトに依存ライブラリの参照を追加する
プロジェクトのプロパティを開き、「リンカー」-「入力」‐「追加の依存ファイル」にDirectDrawなどの依存ライブラリを追加。とりあえず下記があれば良さそう。
- ddraw.lib
- dsound.lib
- Winmm.lib
ここまででとりあえずビルドしてみましょう。
2. ビルドエラーの対処
当然ながら大量のエラーと警告が出ました。このエラーを1つずつつぶしていきましょう。(警告は一旦無視します)
ビルドエラー1: main関数が未解決
未解決の外部シンボル main が関数 "int __cdecl invoke_main(void)" (?invoke_main@@YAHXZ) で参照されました
これは、空のプロジェクトでは、サブシステムが「コンソール」になっていることが原因でした。
プロジェクトのプロパティ「リンカー」-「システム」で「サブシステム」をWindowsにすることで解消。
ビルドエラー2: 文字セットの問題
型 "const char *" の引数は型 "LPCWSTR" (aka "const WCHAR *") のパラメーターと互換性がありません
これは文字セットの問題でした。既定値は「Unicode」になっていますが、当時は「マルチバイト文字」が前提でした。
プロジェクトの「構成プロパティ」-「詳細」で「文字セット」をUnicodeから「マルチバイト文字セットを使用する」に変更
これでかなりの量のエラーが取れました。
ビルドエラー3: 文字列リテラルのConst
型 "const char *" の引数は型 "LPSTR" (aka "char *") のパラメーターと互換性がありません
ファイルの読み込む関数の呼び出しでエラー。文字列リテラルがそのまま渡せなくなっている?
const_cast
を使い文字列リテラルの型 const char*
からconst
をはがしてやることで対応。
これが正しい対処かは不明ですが、単にファイルPathを渡しているだけなので問題ないはず。
if(!DDLoadBmp(&lpDDOF[OFFSCRN_CHAR], const_cast<char*>("bitmap\\MyChar.bmp"),
10,236,BMP_X,BMP_MC_Y,BMP_MC_W,BMP_MC_H)){
return(FALSE);
}
ビルドエラー4: 宣言時の型省略
型指定子がありません - int と仮定しました。メモ: C++ は int を既定値としてサポートしていません
昔は、型を省略するとintとして扱ってくれて下記はOKでしたが、型を明示しないとコンパイルエラーになるようです。
static int nYSpd =-0x98000;
3. 実行時エラー
これでコンパイルは通るようになりましたが、起動してみると下記のエラーが
デバッグしてみると、ディスプレイのカラーモードを変更する箇所で失敗しているようです。
//カラーモードを調べる-256色に切り替える_______________________________
dwColor=GetDeviceCaps(hDeviceDC,BITSPIXEL);
DeleteDC(hDeviceDC);
if(dwColor!=8){
ZeroMemory(&dm,sizeof(dm));
dm.dmSize=sizeof(dm);
dm.dmBitsPerPel=8; //256色モード
dm.dmFields=DM_BITSPERPEL;
if(SUCCEEDED(ChangeDisplaySettings(&dm,CDS_TEST)))//一度テストする
{
ChangeDisplaySettings(&dm,CDS_UPDATEREGISTRY);//切り替え用として保存
fChgColor=TRUE;
}else{
MessageBox(gData.hwnd,"カラーモードの変更に失敗しました",
"Direct Draw",MB_OK|MB_ICONSTOP);
return(FALSE);
}
}
戻り値を見てみると、-2(DISP_CHANGE_BADMODE)が返却されていました。対応していないカラーモードのようです。
どうやら256色(8bit)のカラーモードは対応していないよう。
24bitカラーモードに変更してみます。
#define BPP 24 //24bitカラーモード
dm.dmBitsPerPel=BPP;
そうすると、今度はパレットデータを登録するSetEntries
でエラーになりました。
パレットデータは8bitカラーモードのみで使用するものですので、パレット関連の処理は全て削除しました。
//ビットマップからのパレットデータを登録
err = lpDDPL->SetEntries(
0,
dwStartingEntry,
dwCount,
&ape[dwStartingEntry]
);
これでひとまず、ディスプレイモードの問題は解消しました。
ポインタの64bit問題
今度は下記メソッドの実行時にアクセス違反のエラーとなりました。
//------------------------------------------------------------
//デバイスオープン
//------------------------------------------------------------
mmResult = midiStreamOpen(
&midiStream.hMidiStream,//デバイスハンドルの取得
&uiID, //デバイスIDの指定
1, //1に予約
(DWORD)StreamProc,//ストリーム用プロシージャ
0,
CALLBACK_FUNCTION);//コールバック関数としてストリーム用プロシージャを使用
0x00000000E58E11EA で例外がスローされました (TamaChan.exe 内): 0xC0000005: 場所 0x00000000E58E11EA の実行中にアクセス違反が発生しました
これは、コールバック関数StreamProc
のポインタをDWORD
で渡していることが原因でした。
DWORDはunsigind long
(32bit)ですが、ポインタはプラットフォームによって精度が変わり、64bitプラットフォームでは64bitになるとの事です。
ポインター精度整数型
ポインターの精度が変化すると (つまり、32 ビット プラットフォーム用にコンパイルされると 32 ビットになり、64 ビット プラットフォーム用にコンパイルされた場合は 64 ビット)、これらのデータ型はそれに応じて精度を反映します。 したがって、ポインターの算術演算を実行するときに、これらの型のいずれかにポインターをキャストすることは安全です。ポインター精度が 64 ビットの場合、型は 64 ビットです。
ここでは、ポインタ精度のDWORD_PTR
でキャストして渡すのが正解のようです。
mmResult = midiStreamOpen(
&midiStream.hMidiStream,//デバイスハンドルの取得
&uiID, //デバイスIDの指定
1, //1に予約
(DWORD_PTR)StreamProc,//ストリーム用プロシージャ
0,
CALLBACK_FUNCTION);//コールバック関数としてストリーム用プロシージャを使用
また、コールバック関数の引数wParamもポインタを受け取るためDWORD
からDWORD_PTR
に修正する必要がありました。
void CALLBACK StreamProc(HMIDIIN hMidiIn, UINT msg,
DWORD hInst, DWORD_PTR wParam, DWORD lParam)
{
}
起動した!
ここまででようやく、ゲーム画面が起動しました。操作も問題ありません。BGMも軽快に鳴っております。
ただ、まだ1つ問題がありました。
4. キャラクタの背景が透過されない問題
背景を透過させるには、キャラクタなどの画像を描画するSurfaceオブジェクトに透過させたい色を登録する必要があるのですが、カラーモード8bit時の設定が残っていました。これが原因かもしれません。
#define COLOR_KEY 15 //透過色を指定(8bit,パレット番号)
DDSURFACEDESC ddsd;
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
ddsd.dwWidth = dwWidth;
ddsd.dwHeight = dwHeight;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | dwMemoryFlag;
//...
//透過色を指定
ddck.dwColorSpaceLowValue = COLOR_KEY;
ddck.dwColorSpaceHighValue = COLOR_KEY;
Bitmap画像から透過色(薄水色の部分)のRGBを調べて、下記の様に設定してみましたがうまくいきません。
#define COLOR_KEY RGB(153,217,234)//カラーキー(透過色)
RGBマクロの定義を見てみると、B|G|R のbit構成で32bit整数を組み立てているようです。
#define RGB(r,g,b) ((COLORREF)(((BYTE)(r)|((WORD)((BYTE)(g))<<8))|(((DWORD)(BYTE)(b))<<16)))
// 32bitの構成
// 0000 0000 bbbb bbbb gggg gggg rrrr rrrr
このBit構成は、環境によって異なっていた気もします。調べてみるとどうやらSurfaceオブジェクトのPixelFormatにその情報があるようです。
RGBのbit構成を調べてカラー値を組み立てる
Surfaceオブジェクトの詳細情報が格納されている SurfaceDescからRGB各色のBitマスクが取れました。
//dwRBitMask: 0000 0000 1111 1111 0000 0000 0000 0000
//dwGBitMask: 0000 0000 0000 0000 1111 1111 0000 0000
//dwBBitMask: 0000 0000 0000 0000 0000 0000 1111 1111
RGBマクロとはRとBが逆ですね。これを元にColor値を組み立てる関数を作りました。
//DDSurfaceのPixelFormatのBitMask値を見てRGB値からDWORD値を組み立てる
DWORD tagDDRAW::RgbToDword(LPDIRECTDRAWSURFACE lpDDPR, BYTE r, BYTE g, BYTE b)
{
DWORD
dwRBits = 0, dwGBits = 0, dwBBits = 0,
dwR = 0, dwG = 0, dwB = 0,
dwRMask = 0, dwGMask = 0, dwBMask = 0;
int
nRStart = -1, nGStart = -1, nBStart = -1,
bRBitOn = FALSE, bGBitOn = FALSE, bBBitOn = FALSE;
//SurfaceDescからbitマスクを取得
DDSURFACEDESC ddsdPR;
ZeroMemory(&ddsdPR, sizeof(ddsdPR));
ddsdPR.dwSize = sizeof(ddsdPR);
lpDDPR->GetSurfaceDesc(&ddsdPR);
dwRMask = ddsdPR.ddpfPixelFormat.dwRBitMask;
dwGMask = ddsdPR.ddpfPixelFormat.dwGBitMask;
dwBMask = ddsdPR.ddpfPixelFormat.dwBBitMask;
//各マスクについてONビットの開始位置とONビット数を調べる
for (int i = 0; i < 32; i++)
{
bRBitOn = (dwRMask >> i) & 1;
bGBitOn = (dwGMask >> i) & 1;
bBBitOn = (dwBMask >> i) & 1;
if (bRBitOn) {
if (nRStart == -1) {
nRStart = i;
}
dwRBits++;
}
if (bGBitOn) {
if (nGStart == -1) {
nGStart = i;
}
dwGBits++;
}
if (bBBitOn) {
if (nBStart == -1) {
nBStart = i;
}
dwBBits++;
}
}
//8ビット未満ならその差を右シフト
r = r >> (8 - dwRBits);
g = g >> (8 - dwGBits);
b = b >> (8 - dwBBits);
//マスクの開始位置まで左シフト
dwR = ((DWORD)r) << nRStart;
dwG = ((DWORD)g) << nGStart;
dwB = ((DWORD)b) << nBStart;
//ORでつなげて返却
return(dwR | dwG | dwB);
}
これを使って透過色を指定することで、無事にキャラクタの余白部分を透過することができました。
まとめ
無事に古(いにしえ)のゲームを復活させることができました。
動かなかった原因としては下記2点になると思います。
- 256色カラーモードが使えなくなっていた
- そもそも32bitプラットフォームを前提にコーディング&ビルドしていたものを64bit PCで動かそうとしていた
何はともあれ無事動くようになって良かったです。正直、これだけの修正であっさり動いてしまうとは思っていませんでした。
DirectDrawはDirectX8.0で廃止されたとの事ですが、DirectX12となった今でも当時のコードがそのまま動くのには驚きです。
ソースコード
最後に修正したソースコードをあげておきます。
プレイ動画のリンクも付けておきましたのでどんなゲームか気になりましたら見てやってください。