はじめに
これは私がHTML5ゲームを作れるようになりたいと思うようになってから, DXライブラリで作ったゲームがブラウザ上で動くようになるまでのおよそ2ヶ月間の記録を, 忘れないうちに書き留めたものです.
その時に思っていたこと考えていたことも記事に含めているため, かなり駄文になってしまっています.
emscriptenを使った開発でつまづいたところを ぶつかった壁一覧 に並べているので, そこから該当部分に飛んでください. また, 太字に目を通せばある程度この記事の概要が掴めるように書いたつもりです.
この記事で触れられている内容の成果物へのリンクを先に並べておきます
- DxPortLib(Emscripten対応板) GitHubリポジトリ (https://github.com/nokotan/DxPortLib)
- DxPortLibを使って移植してみた過去作のゲーム (https://github.com/nokotan/DxLibToHTML5Test)
ぶつかった壁一覧
- MAINMODULE=1, SIDEMODULE=1 を使いこなす ~ emscripten でスタティックリンク
- emscriptenでメインループを回す
- 外部ファイルを使えるようにする
- 音が鳴るようにする
emscriptenを使ってビルドするのを思い立つまで
EgretEngineとかcocos2d-jsとかを使ったHTML5ゲームの黎明期が終わっているのかな, 自分がそんなゲーム開発事情を知らないまま無謀な就活をやっていたのかなどと思うようになっていた2020年1月, DxPortLibとHTML5という単語がどこかで目に入ってきました. どこに書いていたのかなどはもう覚えていませんし, 気のせいだったのかもしれません.
でも, DxPortLibがSDL2ベースで書かれていることは知っていて, emscriptenがSDL2に対応していることに気づくまではそんなにかかっていなかったと思います. ならば, emscriptenとDxPortLibを組み合わせることができるならば, ひょっとしたら自分が大学のサークル活動で関わったDxLib製のゲームがブラウザで動かせるかもしれないと思ってからは, 研究(交通工学でした)そっちのけでemscriptenと向き合っていました. 幸いにも, emscriptenのインストールは苦労しなかったと覚えています.
参考記事
- EmscriptenとCMakeでのビルド方法 (https://gist.github.com/faithandbrave/9b3d439d135e63abdbe7)
- emscriptenでC/C++プログラムをwebブラウザから使うまでの難所攻略 (https://www.slideshare.net/llamerada-jp/cmu29)
ビルドが通って画面が表示できるようになるまで
基本構成として, DxPortLibのスタティックライブラリ(libDxPortLib.a)をソースコードからビルドして, そのスタティックライブラリをゲームのソースコードとリンクして組み込んでいたのですが, これを実現するまでにかなり苦戦を強いられました.
MAINMODULE=1, SIDEMODULE=1 を使いこなす
※ 2020/ 5/ 9 追記
最新版の emscripten では、関数ポインタまわりの実装が改善されたのか、MAINMODULE=1
, SIDEMODULE=1
のオプションを追加しなくてもリンクができるようになっています。むしろ、出力されるバインディング用 JavaScript ファイルと WebAssembly ファイルの肥大化につながるため、ダイナミックリンクを目的とするとき以外はつけないことを推奨します。
DxPortLibのスタティックライブラリのビルドにCMakeを使っていたのですが, 次のようにすればemscripten側がよしなにリンクしてくれると思っていました.
# DxPortLib側
add_library(DxPortLib STATIC ${SOURCES})
# ゲームプロジェクト側
link_libraries(Game DxPortLib)
ビルドは問題なく通るのですが, 実行時に次のようなエラーを吐いてしまいます.
Error: To use dlopen, you need to use Emscripten's linking support, see https://github.com/emscripten-core/emscripten/wiki/Linking
Error: exception thrown: Error: abort(To use dlopen, you need to use Emscripten's linking support, see https://github.com/emscripten-core/emscripten/wiki/Linking). Build with -s ASSERTIONS=1 for more info. (evaluating 'new WebAssembly.RuntimeError(what)'),RuntimeError@[native code]
dlopen? なにそれemscriptenでもできるの? など思いながら, https://github.com/emscripten-core/emscripten/wiki/Linking を参照していました. どうも SIDEMODULE, MAINMODULE というコンパイルオプションがあやしいまではわかったのですが, この組み合わせ方が分からなくて2, 3日費やした記憶があります. (特定の組み合わせで -fPIC オプションをつけてコンパイルしろみたいなエラーだったり, dyncall_v で関数が undefined だみたいな実行時エラーが出ていたのですが, 再現できなかったのでこれらのエラーの詳細については割愛)
結論としては, ライブラリ側(DxPortLib側)のコンパイルオプションに SIDEMODULE=1
, ゲームプロジェクト側にMAINMODULE=1
を指定することで少なくともエラーが出ずに黒い画面だけ出すというところまでたどり着きました.
# DxPortLib側
set(CMAKE_C_FLAGS "-s SIDEMODULE=1")
set(CMAKE_CXX_FLAGS "-s SIDEMODULE=1")
# ゲームプロジェクト側
set(CMAKE_C_FLAGS "-s MAINMODULE=1")
set(CMAKE_CXX_FLAGS "-s MAINMODULE=1")
それと, CMAKE_C_FLAGS
とCMAKE_CXX_FLAGS
を間違っていることに気づかないという小さなミスもこの解決を遅らせたといういい思い出なのです.
参考記事
- emscripten: Linking https://github.com/emscripten-core/emscripten/wiki/Linking
emscriptenでメインループを回す
あまり対処に時間がかからなかったものですが, 最初はかなり面食らったので, emscriptenでメインループの回し方にも触れておきましょう.
メインスレッド上でゲームループを回してはいけません. その代わり, emscripten_set_main_loop
を使いましょう. さもなければブラウザーがフリーズしてしまいます.
# include <emscripten.h>
void mainloop() {
// ここにゲームのループの中身を書く
}
int main() {
emscripten_set_main_loop(&mainloop, 0, EM_TRUE)
}
ごくまれにemscripten_set_main_loop
を使っていると, dyncall_v で関数が undefined だみたいな実行時エラーを吐くことがあります. その時は, emscripten_set_main_loop_arg
を使うと回避できることが多いです.
# include <emscripten.h>
void mainloop(void* unused) {
// ここにゲームのループの中身を書く
}
int main() {
emscripten_set_main_loop_arg(&mainloop, 0, EM_TRUE)
}
参考記事
- emscriptenでC/C++プログラムをwebブラウザから使うまでの難所攻略 (https://www.slideshare.net/llamerada-jp/cmu29)
外部ファイルを使えるようにする
外部ファイルを使えるようにするためには, --preload-fileオプションを追加するだけだったので楽だったのですが, いかんせんもともとのファイル配置とフォルダ階層が1つずれてしまうのがネックだった記憶があります. ただ, フォルダ名を指定できるので, わざわざファイルを1つずつオプションに追加する必要がないのはありがたいところと思っていました.
set(CMAKE_EXE_LINKER_FLAGS "--preload-file ./assets")
参考記事
https://emscripten.org/docs/porting/files/packaging_files.html
frameBuffer を使うようにする
黒い画面が映るようになってから, 画面の描画に取り掛かったのですが, 次はなぜか画面の上下が逆になる怪現象に悩まされていました. これまた, openGLとDirectXの座標系の取り方の違いなのかななどと思っていましたが, 同じくOpenGLを使うmacOS版ではそのような問題は起こっていない...なぜだろうとまた数日悩みました.
結論は, OpenGLES2がframeBufferに対応しているのにもかかわらず, DxPortLib側がOpenGLES2使用時はframeBufferを使わないという謎設定になっていたので, そこを修正したのでした.
音が鳴るようにする
Safari, Google Chromeではユーザーの操作に合わせて AudioContextの設定をする必要があったので, とりあえずゲームの最初にクリックしてもらってからゲームの初期化を開始するという設計にしました. この対応にもほとんど苦労しなかった記憶です.
function gameStart() {
canvas.removeEventListener("click", gameStart);
callMain();
}
var Module = {
canvas: (function() {
var canvas = document.getElementById('canvas');
canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
return canvas;
})(),
onRuntimeInitialized: function() {
Module.canvas.addEventListener('click', gameStart);
}
}
DxPortLibとemscriptenを使ってHTML5ゲームが作れるようになってから
DxPortLibを使ってmacOS版をビルドしていたDxLib製ゲームを優先して, 2日に1つずつ移植を進めていくペースで移植を続け, 最終的に6つのDxLib製ゲームのHTML5移植が完了しました.
DxPortLibを使って移植してみた過去作のゲーム: https://github.com/nokotan/DxLibToHTML5Test
ソースコードの変更も,
-
WinMain
からmain
に変更 - メインループを
emscripten_set_main_loop
とコールバック関数の組みあわせに変更 - 外部ファイルの読み込みパスを変更
程度で済むのがほとんどで, 移植が出来上がるたびにDxLibを使っていた頃の懐かしさに浸っていました.
次に何が待っているのか
DxPortLibが本家DxLibより早くandroid, iOSに対応して, かつmacOSにも対応しているのはいいのですが, 残念ながら3Dの機能が全く実装されないまま長く放置されてしまっています.
しかし, DxLibの長所は2Dゲームが簡単に開発できることの他にも, MMDが動かせることにもあるのではないかと思っています.
すでにこの記事の執筆地点で実は本家DxLibのHTML5移植がある程度進んでいます. 次にqiitaに投稿することがありましたら, その開発経緯とつまづいた点をお話ししたいと考えています. ここまで長文にお付き合いありがとうございました.
参考資料
- DxLibの3Dアクションサンプルプログラム https://nokotan.github.io/DxLibToHTML5Test/DxLibSampleTest/DxLibHTML5Test.html