本稿では、私が長く続けている個人でのゲームエンジン開発で得られた知識や経験を紹介します。
嘘ですポエムです。アルコールとか入ってます。
本当はかっこいいこと言ってみんなの役に立つこと書きたかったです。書きたかったけど無理だったよ…。
ひとつのゲームエンジンを作り続けてもう何年も経ちます。その間コンセプトも定められず満足なメジャーリリースもできないままだらだら作り続けてしまったので、あんまり役に立つことは書けなさそう。
とりあえず昨今のゲームエンジンの開発に必要な技術要素と、私が開発している Lumino というゲームエンジンでの代表的な実装例を紹介する、みたいな体で書いてみようと思います。なにか間違ってそうでしたらコメントお願いします。
多分、ちょっとでも役に立つかもしれないのは、「ゲーム作ってたけど気が付いたらゲームエンジン作ってて、ゲームが出来てないけどゲームエンジン公開してみたくなっちゃった」みたいな方です。
どんなのを作ってるの?
いろんな言語で書けたり、
#include <Lumino.hpp>
using namespace ln;
void Main()
{
auto texture = Texture2D::create(u"logo.png");
auto sprite = Sprite::create(texture , 5, 5);
while (Engine::update()) {
# Executed per frame
}
}
# gem install lumino --pre
require 'lumino'
include Lumino
texture = Texture2D.new("logo.png")
sprite = Sprite.new(texture , 5, 5)
while Engine.update do
# Executed per frame
end
したいです。
序論
ゲームエンジンというチャームスペル
なぜゲームプログラマはゲームエンジン自作に魅せられるのだろう。
皆がそうというわけではないけど「ゲームを完成させたいなら、ゲームエンジン(or ライブラリ) を自作するな!」というのをよく見かけます。
それはその通りだと思うし、実際にやってみるとものすごい勢いで時間が溶けていくので Unity や UE4、ツクールとか使うのが現実的な方針です。それで仕事もできるようになるし。
それでもここ数年、新しいゲームエンジンが現れては消えていくのをたくさん見てきました。
ほとんどはランディングページもなくリポジトリだけあったり、テスト版をZIPで固めたものだけだったりしたけれど。
それでも作っちゃうのは、学習とかプロダクト開発とか、そういう建前じゃなくて、根源的にはやっぱりそれが自分の本当にやりたいこと、楽しみたいことなのだと思う。
それは絵を描いたり、音楽を作ったり、物語を書いたり、そういうアーティスト的な、なんていうのかな、なんかゲームのキャラクターのセリフだったと思うんだけど「何故自分の内から湧き出るイメージを具象化しこの世に残さずにいられるのか」っていうのがしっくり来てる。
趣味でゲーム作ってる人が新たな楽しみを知ってやりたいことが変わるなら、それはきっと正しい。趣味は人生を豊かにするもののひとつである。対外的にはどうかわからないけど。
またちょっと落とし穴臭いのが、ゲームのホントに基本的なフレームワークって小規模~大規模までほぼ同じでありシンプルなので、「あれ、これ俺ちょっと頑張れば作れちゃうんじゃない?」って思えてしまうのだ。
まぁそんな風にモチベの源泉が変なところにあったり、イイ感じに知的好奇心を満たしてくれるテーマであったり、いろんなものが混ざってゲームエンジンというものを書きたくなっちゃうのだ。
あと「このゲームはゲームエンジンも自作して作りました!」とか言えるとちょーかっちょいい。ただしモテはしない。
ゲームエンジンは救われづらい
自身の周り、例えばサークルでのゲーム開発で自作エンジンが使われていれば、そこで役に立てただけでも個人開発のゲームエンジンとしては成功だと思います。
公開を考え出してはじめて、あまりにも周りが強すぎることに気付いたりします。実際自分と同じように個人開発で公開しているようなものですら、名状しがたい規模で作ってたりするのがたくさんあったりします。
救いがあるとしたら、各種ジャンルに特化した専用のゲームエディタかな。RPG専用や、シューティング専用、みたいなやつ。
これも今やレッドオーシャンって感じですが、特化していくほどある程度のユーザーはついてくると思います。
例えば、
- PIXI.js に乗っている RPGツクールMV
- DXライブラリ に乗っているウディタ
- Unity に乗っている 宴
あ、一応捕捉しておくと、この記事で「ゲームエンジン」と呼んでいるのは、この DXライブラリ とか Unity とかのベースの部分です。
でこのベースになっている部分。この領域に挑むのは救いが無いです。ライバルが名実ともにめっちゃつよい。
もし救いがあるとすれば、それはゲームエンジン自体ではなく、そのようなベースシステムを構築できる技術力。それを持っているあなた自身の存在、って思います。
こんなエントリ読んでないでそのシステムを持ち込んで就活した方がいいと思います。
書いてて思ったけど、有名どころがユーザーをかっさらっていく構図はどんなソフトウェアでも同じだよなって思いだした。
うるせーおれはゲームエンジンつくるのがすきなんだよ
一緒に沈もう!
開発環境を整える
プログラミング言語
エンジン本体を書くための言語の話です。スクリプティングはのちほど。
ネイティブクロスプラットフォームで速度と品質を目指すなら、C++ か Rust かな。
小規模ゲームをターゲットにするならWeb周りの技術(Electronとか)を使いつつ Javascript でいいと思う。
最近は C# もブラウザ上でそのまま動くようになってきてるけど、ゲーム作れるかは調べきれてないです。
僕はC++への刷り込みがひどいのでオススメをがっつり語るのは止めておこうと思います。
ただ入門者がモダンC++を理解したうえで独自のシーングラフ作りつつOpenGLあたり使って画面に三角形出せるところまで行けたとして、だいたいその辺で大満足してフェードアウト → UE4 あたりへ GO するくらいにはモダンC++は難しいと思う。
Rust がっつり触ったことないけど、風のうわさでは並列処理なレンダリングエンジンを書くときに、Rust の並列処理セーフティがいろいろと指摘してくるので回避するのに苦労するらしい。
個人的には Javascript はなんか好きになれないんだけど、現実的な方法としてはすごくオススメします。
でも TypeScript はすごくいいぞ。
で、申し訳ないですが以下は C++ かつネイティブクロスプラットフォームで3Dなゲームエンジンを開発する想定で書いていきます。ひどい目標だ…。
GitHub にリポジトリを用意する
プロダクトをどのように公開するかはともかく、プロジェクトの第一の資産であるソースコードを保存しておく場所は真っ先に作っておくべきです。
なんかヨサゲなゲームエンジン作ってたけど「PC壊れてソースコード無くしました」みたいに SNS で自虐ネタ流した日には、ソフトウェアエンジニアとしての信用に致命的なダメージを受けます。ついでに単純にモチベが崩壊します。そんな人を何人も見てきました。自虐はほどほどに。
GitHub も個人リポジトリならコードを公開しないプライベートリポジトリを作ることができます。
ついでに Dropbox あたりも使って、開発資料とか関係するものは全部クラウドに上げておいた方がいいよ。
開発ツールを学ぶ
CMake
C++ のビルドツールとしては実質スタンダードだと思います。ビルドしたり、配布用パッケージ作ったり、サードパーティライブラリを取り込んだり、いろいろできます。CMakeLists.txt はちょっと書きづらいけど。
こういうのを使わずに、VisualStudio や Xcode のプロジェクトファイルを直接リポジトリで管理するのもありですが、ターゲットプラットフォームが増えるにつれてそのうち管理しきれなくなります。
どのみち Android-NDK の標準的なビルドツールが CMake なので、最初から使ってた方がいいと思う。
各 IDE やビルド方法
とはいえ CMake の生成物についての知識は入れておかないと、ゲームエンジンのランタイムはどうやって起動したらいいの?とかいう話になりかねないので、CMake だけ覚えれば万事解決ってわけじゃないのが悩みどころか。
- Visual Studio
- Xcode
- Android Studio
- (IDE じゃないけど Linux や Emscripten 向けに)Makefile
それぞれで、ビルド、実行、デバッグ、デプロイはできるようになっておきたい。
配布方法を考える
最初に配布を考えるの?って思うかもですが、イマドキの開発スタイルは、最初に小さなパッケージを作っておいていつでも動かせるようにしておき、それを壊さないように少しずつバージョンアップを重ねるのが良いぞ、っていう流れです。
というより、ゲームエンジンみたいな大きなものを個人で作っていくにはその考えがベースに無いと正直やってられんです。詳しくは CI/CD のところで。
ここではユーザーに届ける最終成果物は何?って話です。
一番簡単なのは ZIP とかに固めて配布かな。雑かもしれないけどこのスタイルをとっているものは結構多いと思う。DXLib とかウディタとか Cinder とか。
C++ 以外の言語だと、使いやすいパッケージ管理システムが付いているので、ユーザーはそれ経由でインストールしたいと思うかもしれない。(自分だったら思う
dotnet なら Nuget, Ruby なら Gem とか。
OSS を学ぶ
今日日開発されているシステムの多くは、既に出来上がっているモジュールを集めて繋げることで大部分が作られていたりします。
PNG 画像を読み込むために ZIP デコーダを自分で書くというのかい?という話。
まぁ技術的なところへの興味は絶えないけれど、自分で作るべきところと他に任せるべきところを考えておかないと、とてもじゃないけど手が足りないです。
というところでほとんどのケースでは OSS のライブラリを借りてくることになるですが、最低限ライセンスには気を配ろうよ、って話です。
ちゃんとやるなら最新バージョンや不具合情報を追跡とかしたいところだけど、これも時間との相談か。
テストを学ぶ
プログラムを組んだことがある人ならだれしも、ちょっとコードを変えただけで、全然関係ないと思っていた機能が動かなくなったような経験があると思います。いわゆるデグレード。略してデグレ。
新しく作った機能をテストするのはもちろん、デグレを防ぐためにリグレッションテストと呼ばれる類のテストを回し続ける必要があります。
プロダクトの規模が大きくなってくると、どれほど慎重にコードを書いていてもデグレのリスクは跳ね上がっていきます。サイクロマティック複雑度とか調べてみるとちょっと面白い。
コードレビューしてくれる協力者が一人でもいればだいぶマシになると思うけど、それでもゼロはなかなか難しい。
てことで繰り返し同じテストをする必要があるけどこれが本当に心をやられる。誰もやりたくないので仕事として成り立つくらいには心をやられる。個人開発でテスト会社に依頼するとかは難しいので、テストは真っ先に自動化するべき。
C++ だったら googletest 使うのがいいと思う。
CI/CD を学ぶ
↑ の配布やテストを自動的にやっちゃおうぜ、っていう取り組みです。
GitHub のリポジトリにコードを上げたとき、
- ゲームエンジン一式をクリーンビルドして、失敗したら教えてくれる(メールとかチャットで)
- ビルドしたものをリグレッションテストして、失敗したら教えてくれる
- テスト成功した一式を ZIP に固めて、いつでもダウンロードできるようにしてくれる
これを、各プラットフォーム (Windows, macOS, Linux...) で自動的にやらせるようにする。
これができたらいいよね、っていうより、大きなテーマで個人開発するにあたってはこの仕組みを作っておかないと、そのうち破綻します。(した
テストを一度書いたらそれを自動的に回すようにして、絶対にテストは失敗させない、失敗してたらすぐ直すくらいの心構えじゃないと、個人で大きなプロジェクトを長く続けていくのは不可能です。ていうか怖くてリファクタリングできなくなるので、すぐコードが腐る。
新しめのプログラミング言語は言語仕様レベルで不具合を防ぐような仕組みを設けてたりするから、実装難易度の高い C++ 特有の問題化もしれんです。
ドキュメントライティングを学ぶ
文書を書けるようになろう。小説とかじゃなくて、自分の作ったものを他の人に説明するためのものです。
こんなブログで紹介してる時点で説得力皆無だけど、口語とか使わずちゃんと技術文書として正しい言葉を使ったりしよう。
キレイに書けると google 翻訳さんがキレイに翻訳できるようになったりします。
あと文書構造をちゃんと知る。見出し、文節、箇条書きなどのこと。
世の中にはいろいろなドキュメントを作るツールがありますが、文書構造が適切に使えている文書は大抵どんなツールを使ってもキレイな最終出力が得られます。それは最終的に多くの人が見易いドキュメントを作ることになります。
なんか気に入らないから全角スペースでインデントしたり、変なところで改行して行間つけたりすると結果がぐちゃぐちゃになったりで、変なところでストレス溜めることになります。Word 上手く使えない人はこの傾向がすごく多い気がする。
デザインやレイアウトはツールに任せろ。自分は気に入らなくても、ほとんどの人はそれでよい。自分はドキュメントの中身に集中せよ。
開発環境まとめ
公開を考えなければ、ここまでやる必要はないと思います。
ていうかこれ全部できたら仕事できるよ。
設計を学ぶ
最初は設計手法なんて全くモチベーション持てないです。
原理理論は後回しでまずは動くモノを作って動かして全能感に浸りながら、画面の中で動くキャラクターを小一時間ほど不敵な笑みで眺めるのは多分正しいです。また心の健康によいです。
ただそのうちどうしても破れない「壁」にぶち当たります。自分の最初の壁は2万行くらい(適当)だったかな。main 関数何行書いたかなー
そんな時に助けになるのはやはり先人の知恵で、ここで初めて設計やソフトウェア工学に対する関心が出てきます。
設計の勉強のついでに、「技術的負債」っていう言葉を知っておくといいかも。長期メンテにあたっては、これと、先に書いた CI/CD とリファクタリングがものすごく重要になってくる。
聖書 → C++のためのAPIデザイン
変更に耐えられるようにすること
ソフトウェア設計について調べ出すと信頼性とか拡張性とかのいわゆる「品質特性」や「デザインパターン」などたくさん情報が得られると思います。
ただそれらは本当にあらゆるソフトウェアプロダクトをターゲットとした指標であって「ゲーム」や「ゲームエンジン」という領域では必要な要素はかなり絞られます。
極端な話、自動車や医療など、一歩間違えば人が亡くなるようなクリティカルシステムと、娯楽であるゲームでは品質に対する要求が全然違うということ。なので、ちゃんと自分のシステムに必要な手法を取捨選択できるようになろう。
というところで、ゲームエンジンとして重視したい品質ってなんだろうと個人的に強く思うのが、表題のとおり「変更に耐えられるようにすること」ではないかと。(まぁ他にも重要なのはあるけど "より" 重視するものとして。
ゲームはソフトウェアシステムという中でも特殊なもので、仕様はあらかじめ決められるものではなく、動かして、面白さを確認して、ダメなら修正を入れるというサイクルで作られることがほとんどです。
フォーマルっぽい言い方だと「アジャイル開発」と言います。情報処理技術者試験にも新しく入ってきてたような気がする。
一般的な実装やAPIを知ること
迷ったらスタンダードに合わせよ。
まぁそのスタンダードを知るところがまずひとつのハードルだったりするんだけど。
一番のよりどころは各言語の標準ライブラリかな。
特徴を出すべきところとそうではないところを、ちゃんとわきまえないと後々メンテが大変になる。(なってる
というか覚えることが増えるので、作る人も使う人も脳の負担が増えたりする。
新しいもの考えてるときは色々と想像が膨らむんだけど、一個人が頑張って考えたものなんてのは往々にして誰も幸せにならないことが多いので、そういう時に参考にするべきなのは一般常識、つまりスタンダード。
あと一般に合わせておくと、書くべきドキュメントが少なくても済んだりする。
世の流行りから学ぶ
複雑化するソフトウェアに何とか対応しようと、いろいろな手法が提案されています。
Web 界隈は特に顕著かな。React とか Vue とかはインパクト大きかった。
流行ってるものはそれなりに理由があるわけで、ただ煌びやかであるわけじゃないです。
関数型とか宣言的UI とかめっちゃ参考になる。参考にできるようになりたい。
ただ個人で大きいプロダクト作ってると、そういうの勉強して実装したぞ!ってなったあたりで次の流行りが来たりするからとてもつらい。特に Web 系はとてもつらい。
つらいので枯れた技術で作りこむのはアリだけど、サードパーティのライブラリとかはベンダーにサポート切られたりすることもあるので要注意。
プロジェクトの運営を学ぶ
ごめんなさい丸投げ → オープンソースソフトウェアの育て方
OSS にしなくても参考になると思う。
ゲームエンジンを学ぶ
前例を探そう。仕事の効率的な進め方の鉄板メソッドである。
守破離を踏襲せよ。真のオリジナリティは王道を知るところからだ。
というところで、ゲームエンジンへの要求の整理です。
見出しは聖典 → ゲームエンジン・アーキテクチャ と似せてあるので、それを合わせて読んでもイイかも。
前例を学ぶのは、意図しない車輪の再開発や落とし穴を踏むのを防ぐためですが、最初は「なんでそんな機能が必要なの?」と腹に落ちないことも多いです。
これも設計同様、実際に作ってみて、壁にぶち当たって始めて、なるほどそういうことか、ってなるのを繰り返して行くしかないのかなと思ったりしてます。
エラーの捕捉と処理
診断は全ての機能のベースになるものなので、これは最初に考えておかないと後々苦労することになります。
コツはできるだけシンプルに考えることかな・・・。エラーコードや例外の種類をたくさん増やしても管理しきれなくなるだけだし。
ゲームエンジンだと次のような分類が多いと思います。
- ロジックエラー(プログラマエラー
- ゲームエンジンを開発するプログラマーが原因で発生したエラー
- エンジンユーザーエラー(開発者エラー
- ゲームエンジンを使うクリエイターが原因で発生したエラー
- エンドユーザーエラー(プレイヤーエラー
- エンドユーザー(ゲームプレイヤー)が原因で発生したエラー
※"ゲームエンジン" をターゲットとしていることに注意。ミッションクリティカルなシステムではまた違ってくるので、システムの要求を考慮しよう。
ロジックエラー
null チェック忘れたなどが典型的な原因であるエラー。
これが発生した場合はログを残し、潔くアプリを落とすのが懸命です。
バグか仕様かに関わらずアプリが、そもそもプログラマが想定していなかった状態になっているため、無理に復帰させてもセーブデータが壊れたり脆弱性がチートの温床になったりします。
UE4 の アサーション が現実的な解かな、と思う。
エンジンユーザーエラー
テクスチャファイルの名前を間違えていたり、スクリプト API に変な値を渡してしまったりした場合のエラーです。
ゲームエンジンを使う側のデザイナー、プランナーさんのミスで発生するもので、これが起こったときにアプリを落とすのはダメ。開発効率が致命的に落ちるので。
テクスチャが無いなら真っ黒でいいからメッシュを表示してあげたり、画面上部に赤文字で「テクスチャ無いっす」って表示したり、いわゆる「復帰可能エラー」として扱うべき。
何をこの類のエラーとして扱うかはゲームエンジンのポリシーに依ります。
初期設定ファイルが無かったら、パッケージ異常と考えてアプリを中断するのか、デフォルト設定を使って継続するのか、とか。
エンドユーザーエラー
動作環境が問題だったり、ゲーム内のログインフォームで間違った文字入力した場合などのエラーです。
一般的に良いとされるエラーメッセージのお作法に則って、ユーザーに懇切丁寧に現象と復帰方法を知らせるメッセージボックスを出してあげるのがベターな対応でしょうか。
ゲームデザイン寄りの話なので、ゲームエンジンとしてどうにかできるのは動作環境エラーくらいかな・・・。
メモリアロケーション戦略
エンジン内部での毎フレーム new は大罪。オーダーは償却定数時間までなら許してやろう。とか、そういうポリシー。
リアルタイムグラフィックスは性質上、1フレームの中でだけたくさんメモリ使って、そのフレームが終わったら不要になるケースがたくさんあります。シェイプをCPUでテッセレーションして頂点バッファ作るときや、描画コマンドをキューに詰めるときとか。
ので、それ用のアロケータを作っておくと便利。というか必須。アライメントも馬鹿にならなかったりする。
DirectX Sample の LinearAllocator とかおすすめ。
たまにコマンドにラムダ式使ってるエンジン見かけるけど、STLの実装によってはサイズの大きい変数をキャプチャすると new が発生することがあるから注意。
算術
ベクトル、行列、クォータニオンといった3D線形代数はこの後のほぼすべてで利用します。
ので、簡単なものを自作するなりライブラリ使うなり、何かしら無いとお話にならんですので早めに用意しておきます。
ライブラリ使うなら Eigen がいいと思う。
あと乱数について。これは標準ライブラリの rand 関数は精度的に使い物にならんって注意しておけば大丈夫かな。C++11 以降はイイ乱数ジェネレータが標準ライブラリに入ったし。
エンジンの起動方法
エントリポイント(main 関数)をゲームエンジンの内部に隠蔽するか、しないか。
と言っても Web をサポートするなら実質隠蔽するしかない。
この場合ランタイムに、初期化時、フレーム更新時、などのコールバックを登録することになる。
ほとんどの場合は こんな感じ で
仮想関数を実装するスタイルになると思うけど。
アセット管理
画像や音声などの素材をどう管理するか、という話。なかなか fopen とか fstream で読むだけじゃ済まないのが悩みどころ。
まさか素材をパッキングや暗号化せずにそのままゲームフォルダに突っ込んでリリースした日にはネタバレが危険で危なかったり、素材の二次配布問題になることもあります。
DXライブラリのアーカイブ はシンプルでいい感じ。
圧縮が絡むとロード速度にすごい響いてくるのでいっそやらないのも手かもしれない。でも Web ターゲットだと通信のオーバーヘッドの方が大きいことが多いので必要になるかも。
あと非同期ロード。"Now loading..." を実装するために必要。
キャッシュや寿命の管理も入れておきたいところ。これが無いとゲームプレイ中にファイルアクセスのためカクカクしたりすることが多くなる。
キャッシュは、既に同じ名前のアセットがロードされていたらそれの参照を返すことで、ファイルアクセス回数を減らして高速化したり、メモリ使用量を抑えたりする仕組み。
寿命管理は、参照カウントなどを使用して、アセットが本当に不要になったらアンロードする仕組み。
ユーザー入力
ゲームパッドやキーボード、マウスからの入力を受け取るための機能。
ハードウェアだけではなく、タッチベースのソフトウェアキーボードや独自の仮想パッドから入力することもあるので、これらをまとめて抽象化して使えるインターフェイスがほしい。
それとキーコンフィグとそのプリセット。
「ゲームパッド」または「キーボード & マウス」で遊べるっていうゲームもたくさんあります。
マルチプレイヤーできるようにもしておきたいところ。
あと最近は VR や IoT ぽいセンサーとか従来の枠組みにとらわれない入力インターフェイスを持つデバイスがたくさん登場してるので、そういったものに対応しておくとキャッチーな機能としてアピールできるかもしれない。
レンダリングエンジン
ランタイムの中でも多分最も大きなテーマである絵を描く部分です。やりがいがある一方、いまなお進化を続けているテーマであり、すごい沼です。どっぷり浸かったら多分戻ってこれないです。なんかレイトレとか聞こえてくるし。
領域は大きく次の2つ。
- 低レベルグラフィックスAPIの部分
- それをぶん回してリアルタイム描画する部分
グラフィックスAPI
OpenGL は滅びる。りんごのてんてーがゆってた。
また DirectX11 以前では GPU の恩恵を 100% 得ることはできない。(主に並列処理)
ついでにこれら古い API は消費電力が大きい。モバイル全盛期にこれが痛い。
ということでネイティブは DirectX12, Metal, Vulkan へ、Web は多分 WebGPU への移行が求められています。
がんばれ。
リアルタイム描画
がんばれ。
正直つらい。
がんばれなさそうなときは既存の実装借りてくるのがいいと思う。個人的に filament 大好き。
この領域はいかに速く描くか、ではなく、「不要な部分を描かないことで時間を稼ぐ」がメインテーマになってくるので、そんな感じを目指すといいかも。
ただレイトレがどうなってくるかちょっとキャッチアップ追い付いてない・・・。
物理エンジン
正直なところ、"物理" の部分を積極的に使うことは少なく、"衝突検知" の部分の方がよく使われる気がします。まぁタイトル次第ではあるけれど。
物理エンジンはいろいろなライブラリがあるけど、それぞれ微妙にクセがあるので注意。例えば物理マテリアルが形状データにつくのか、剛体データにつくのか、とか。
無理にラップして抽象化しないのもアリかなと思う。Box2D とか Bullet とか、みんなクロスプラットフォームで動くし。
シーングラフ
このへんの設計手法はほぼ枯れつつあると思う。
ツリー構造でオブジェクトを作って、オブジェクトに意味を持たせるためのコンポーネントをたくさんアタッチしてゲームワールドを作り上げていく方式です。Unity とかで「コンポーネント指向」とか言われるアレ。
C++ だとシーンエディタを作る場合はこれらをセーブ・ロードできる仕組みを作るのに少し苦労するかも。
C# とか他の言語だとたいていリフレクションの機能を持ってるから楽なんだけど、C++ だと cereal とか使って頑張るのが現実解か。
あとマルチシーンのロードとか、オープンワールドのための先行ロードとかキャッシュとか考え出すとおなか痛くなるかも。(なってる
Audio
ただ再生するだけならすごく簡単。だけど SE 再生しようとすると一瞬カクッってなるの何とかしてほしいかなって思います。
これは音声ファイルを同期的に再生しようとしているため、ファイルオープンが終わるまでプログラムが止まってしまうのが原因なんだけど、個人でゲーム開発してて OpenAL とか直接叩いてますぜみたいなのでかなり多い。
要するに非同期実装が必須。
再生するだけなら素直に SDL_Mixer とか使った方が少しは救いがあると思います。
ただ最近はツクール仕様のループに対応している OGG ファイルを素材として配布されている方が多いので、ぜひ対応してみたいところなんだけど、そうすると非同期でストリーミング再生する必要があり SDL_Mixer の再開発っぽくなってきます。
でもやりたいんだよ。やったけど。
次の壁はエフェクト。Volume や Pan だけじゃなくて、エコーとかピッチとか、あるいはプログラマに波形を渡して独自のエフェクタ作れるようにしたりとか、ビジュアライザ作れるようにしたりとか。
WebAudio はホントによくできてると思います。汎用的を極めるならあんな感じにしたいところ。
アニメーション
毎フレーム座標を +1 とかすればそれでアニメーションです。でもそれだけじゃ「気持ちのいい」アニメーションの実現は難しい。
スキンメッシュアニメーション、パーティクルエフェクト、UI の遷移とかで使います。
やりたいことの根っこは2つの値の補間ですが、それをなんやかんや汎用的にまとめた、キーフレームアニメーションとかアニメーションカーブとか呼ばれる、いい感じでうにょうにょした値を作ってくれるモジュールです。
個人的には UI で EaseOutExpo とか使うのが好き。
スキンメッシュアニメーションの合成がひとつの壁か。
ビジュアルエフェクト
ビジュアルエフェクトはゲームの状況を効果的にプレイヤーに印象付けるための、非常に重要なファクターです。
ゲームのかっこよさの第一線を張る重要なものですが、これを作るのはホントに大変。
というわけで Effekseer とか専用のツールを組み込んだりします。
一方で火花散らすくらいの簡単なエフェクトならエンジンに組み込んで欲しいなーと思うところもあったり。単純なスプライトパーティクル。
それ以上の数百万粒子とかやべーやつはコンピュートシェーダ使えるようになったらぜひ。
でもコンピュートシェーダのパワーを100%発揮するにはさっきの DX12,Metal,Vulkan あたりが必要になるから道のりはかなり長そう。
スクリプティング
昔と比べて PC の性能が良くなったといっても、C++ だけでゲームを作るのはまだまだつらいです。主にコンパイル時間が。
エンジンを C++ で作るのは良いとしても、ゲームは変更と調整のオンパレードなのでちょっと変えてすぐに実行できるようじゃないとやってられんです。
実装方法としては、
- スクリプトのランタイムの中でゲームエンジンを動かす
- ゲームエンジンの中でスクリプトのランタイムを動かす
があるけど、それぞれコンセプトが全然違うので注意。後者の場合多分 Ruby を Web で動かせたりもするけど、パッケージ管理に制約が付いたりする。
ちなみに Lumino の Ruby 版は現在前者のみの対応となっています。
AI
AI の性質ってタイトルごとに違うからゲームエンジンとしてどこまでサポートできるかってのは難しいところです。
やっぱりビヘイビアツリーのサポートがベターか。
ちなみに私が今作ってる ARPG では これ に近い ビヘイビアツリーとルールベースAI が階層的になってるような行動決定システムが爆誕したりしたので、ゲームエンジンとしてサポートするならこんな変則的な使われ方も想定した方がいいのかもしれない。
ネットワーク
シーン内のキャラクターの同期あたりが大きなテーマになりそう。ユーザー入力との抽象化というか棲み分けが大変かも。
というかそれ以前にゲームのリアルタイム同期システムは、HTTP をベースとしたような一般的な Web 開発とは全然違ったりするので、「ネットワーク」のひとくくりで考えてると躓きポイント多い気がする。僕は P2P でキャラ同期したことある程度なので詳しいことわからんですが。
最近知ったけど IoT で使われてる MQTT とかイイ感じな気がしてる。
あと忘れがちだけどセキュリティ。でもこれはゲームエンジンの外側の話かな・・・。
これすごく重いテーマで、最近友人と匿名ランキングを HTTP で作るとき、チート対策はできるのか?みたいなこと話したりした。(いいアイデア無かった
ゲームエンジンを実装する
あーやっと作れる。
実装と言っても先に書いたように、OSS なライブラリをお借りしてつなぎ合わせていくのが多分現実的な落としどころなんじゃないかなと思う。
例えば自分も物理エンジンやフォントレンダリングを自作しかけたけど、結局 OSS のを利用することになりました。
ほんの少しの間は自作を使ってたけれど、メンテナンスに取られる時間が多すぎて他の開発がさっぱり進まなかったのであきらめました。
車輪を作ったことでいろいろ勉強にはなったけど、やっぱり実運用となると上手くいかないことの方が多い。
さて、Lumino の実装を少し紹介しようと思います。
主なモジュールごとに色々説明していきますが、リポジトリのincludeフォルダ を見てみると大体どんな人がいるのかつかめると思います。
プログラミング言語
Lumino はネイティブクロスプラットフォームかつクロスランゲージを狙っていたりします。
なので、エンジンのソースコードを解析して他言語用のバインダを自動生成できるようにしたかったです。
というところでまずはランタイムをいろんな言語から呼び出せるようにできる、ネイティブアーキテクチャで動かせるものであることが第一。
あとそれなりに安定しているパーサ(コンパイラのフロントエンド API)が提供されている言語がほしかった。
ので、安定(とは明言していないけど)して提供しているのはやっぱり昔からある clang、つまり C++。
これの検討したのはもう2,3年前だから、今なら Rust もイイ感じのコンパイラ API 提供してるかもしれないです。
Core
文字列クラスとか動的配列とか文字コード変換とかJSONシリアライザとかが入っている、ゲームエンジンとは直接関係ないけどアプリを作るうえで必要な基本機能をまとめたモジュールです。
C#の標準ライブラリっぽいのを再実装したみたいな闇の深いブツです。
char16_t ベースのライブラリが欲しかったんじゃ、というのが建前で実際は、
本業で世話してるシステムのあまりのレガシーっぷりと反OSSの姿勢にキレて「じゃあ俺の責任でライブラリを作れば文句無いよなァ!?」って感じで作って弊社の主力製品の中核にぶち込んでリリースしちゃった感じのやつ
です。
なので Core は別なサードパーティライブラリには依存してなかったりします。
でももう何年も前の話で文化も変わってきてるので、さすがに RapidJSON とか利用するのに置き換えたい。
Engine
Lumino の一番てっぺんにいる人が EngineManager です。Lumino を起動することは即ち EngineManager のインスタンスを作ることです。
この人が以下の各種モジュールの管理を担当する ~Manager (GraphicsManager とか AudioManager) とかのインスタンスを
作りまくります。
またメインループの制御も EngineManager が担当します。
Animation
AnimationCurve, AnimationTrack, AnimationClip あたりで、よくあるスキンメッシュやオブジェクト移動ルートを作るためのキーフレームアニメーションの定義を行います。モーションファイルを読み込むと作られるのがこの人たち。
これに対するアニメーションのインスタンスが、AnimationController, AnimationLayer, AnimationState です。
なんとかしてスキンメッシュアニメーションと UI のスタイルアニメーションを抽象化した API を作ってみたかったのですがびみょーに無理だったので、UI 用にはインスタンスとして AnimationClock とかが居たりします。
Asset
アセットファイルを暗号化アーカイブできるようにしてありますが、非同期ロードがまだ入ってないです。非同期は Promise っぽく作ってみたい。
ちなみにアセットの識別子は GUID を使う実装 (Unity とか) と、ファイルパスを使う実装 (ツクールとか) があるそうです。
Lumino も GUID にチャレンジしてみたけど、エディタサポートが無い状況では管理がつらすぎたので止めました。いや、その後エディタも作ってみてるけど今度はデバッグが辛すぎた。
ファイルパスを使う場合は Unix 形式(セパレータは /)一択なんだけど、システムの絶対パスと Asset フォルダをルートとした絶対パスと単純な相対パスを区別したかったり、やっぱりなんか細かいところが色々気になる・・・。
ってことで、最近は URL ぽい形式にしました。"asset://アーカイブ名/path/to/file" みたいな感じ。
どの方式も一長一短がすごいあるので、自分のエンジンがターゲットにする規模に合わせればいいと思う。
Audio
WebAudio ライクなノードベースでエフェクト自由に掛けられるスタイルで開発中。ただ再生するだけならできてます。
おそらくWeb 漁って OpenAL のサンプルとかでフツーに再生できるようにしたものは「プッシュ型」と呼ばれるオーディオストリームの実装になっているはず。1秒分を音声ファイルから取り出して OpenAL の API に突っ込むのを繰り返す方式です。
一方ノードベースでエフェクトを柔軟にカスタマイズできるフレームワークを作るには「プル型」と呼ばれる方式で実装することになるはずで、これは音声データが欲しいときに呼ばれるコールバックの中で、シーン内の音声を全部まぜまぜして返してあげる方式です。
C++ から使えるライブラリだと LabSound とかかなりよさそうに見えたけど iOS とか対応してなかった。あとコードほとんど Chromium のだった。
興味ある人は ここ から読みだすといいかも。
こういうの頑張って実装してお気に入りの音楽とか聴くと、体から色々な液体がたくさん流れると思う。必聴。
Effect
ビジュアルエフェクトの再生を行う人たちです。
最近 Effekseer を組み込みましたが、スタンドアロンでもパーティクルエフェクトや、スプライトのフリップアニメーションはできるようになっています。
エフェクトって効果音と同じように、ソースデータの管理するのすごくめんどくさいので、
Effect::emit("Laser01.efk", Vector3(位置));
みたいに再生開始だけメソッド呼んであげればあとはエンジンがよしなにやってくれるようにしてみてます。
あとパーティクルはそろそろパラメータの数がやべーことになってるのでエディタが無いとつらい。
Graphics
低レイヤーの描画機能を扱う人たちで、多分 Lumino のユーザーは Texture 以外はほとんど使わないかも。
0.8.0 で一度 fix したつもりになってましたが、Vulkan バックエンドを実装したことで、Apple が OpenGL を非推奨にした理由みたいにフロント API とのミスマッチで処理負荷がウェイウェイな感じになってきました。
で、今のところ DirextX11 に RenderPass を足したみたいな API になっています。
また描画スレッドの実装もこのモジュールの役目にしていて、UE4 みたいに 描画コマンドを作るためのラムダ式相当のファンクタを生成するマクロ とか少し黒魔術っぽいこととかやってます。
前述しましたがラムダ式は変なところで new してくれるので、アロケータも含めて自分でコマンドリスト作ってます。
ただもともとは OpenGL とか古い API だと描画が完全に終わるまで Present や SwapBuffers といった関数が制御返さないのが嫌で作ったシステムですが、
DirectX12 とか Vulkan だとそのあたりの制御も自分でできるので、もういらないんじゃないかと思い始めてます。
Scene
コンポーネントがアタッチされたオブジェクトをツリー構造で繋げて、シーングラフを表現する人たち。一応マルチシーンの Save・Load とかできるようにしてあります。
ぶっちゃけオブジェクトの姿勢を決めた後、次の Rendering モジュールを使って形状を描くだけなのであんまりいうこと無い。
World が一番てっぺんにいる人で、その下にたくさんの Scene がぶらさがる。その中にさらにたくさんの WorldObject (Unity でいうところの GameObject) がいる。そんなイメージ。
Rendering
多分スクラップアンドビルド回数が一番多いモジュールです。8 回くらいかな?
マルチシェーダパスとかレイヤーとかマルチビューとかデバッグ描画とか、なにか機能を増やしたくなるたびに「あー今のアーキテクチャじゃだめだ」ってなって色々なゲームエンジンのソースコード見に行って、必要な範囲だけ最小実装を続けたらいつの間にかいろんなことができるようになったけど屍の数がひどい、みたいな。
最初に立てたコンセプトと照らし合わせて、やるかやらないか、線引きするべきだったんだろうけどなぁ・・・。
さて、大体次のような流れで描画が行われます。
- SceneRenderer を準備する。次の Pass を持たせる。
- RenderingContext::drawXXXX() で描画コマンドを作ってコマンドリストに追加する
- コマンドリストを SceneRenderer に投げる。
- 最初に持たせたパスごとに描画コマンドを、
- フィルタリング(半透明を描くかとか)・ビューカリング
- Zソート
- バッチ化(ステートでコマンドをまとめたり)
- バッチの描画
- で最後にポストエフェクト。
書き出しちゃうと簡単だな。ここにたどり着くまで時間かかりすぎたけど。
ちなみに Lumino の標準は "Clustered Forward+ Shading" です。
あと最近はこの処理を描画スレッドに追い出した方がいいのではと思い始めています。あーまた変わる
Physics
物理エンジンは 2D 3D それぞれ領域を分けていて、Unity と同じく 2D 物理はクラス名に "2D" サフィックスがついてます。
バックエンドは Box2D と Bullet で、Physics モジュールの基本方針はそれらの API を統一するためのラッパーです。
ゲームエンジンとしての物理モジュールを作るよりも物理エンジン自体の使い方を理解する方が大変かもしれない。
Shader
Lumino はプログラマブルシェーダをサポートしています。
でも、シェーダは通常実行環境ごとに HLSL, GLSL, SPIR-V などたくさん用意しなければならず、それをユーザーにやらせるのは嫌だったので、glslang と SPIRV-cross を使って HLSL から他へのトランスパイラを作ったりしました。
UI
この人もスクラップアンドビルド回数がなかなかのもんです。
昔は Scene の中の一部としてボタンとかテキストを表示するためのものでしたが、今は UI の一部として Scene を描くような感じになってます。おかげで規模が大変なことになってます。
それもこれもマルチウィンドウで別窓にデバッグ情報を表示したいとか、この UI モジュールでエディタ作ってインプレースで 3D シーンを描画したいとか、複雑な要求に耐えられるようにしたい重いが積もり積もって取捨選択できなかったのがひとつの原因か・・・。
あと「UI なんて imgui でいいじゃん」ってよく聞くけど、個人的には imgui そのまま使ってエンドユーザー(プレイヤー)に見せる UI を作るのはちょっと疑問。キレイにアニメーションとかやるのに、結局 imgui をラップして Retained-mode な UI ライブラリ作ることになったりしがちだし・・・。(でも完全に自作するよりはマシかも)
でもデバッグ用やエディタ用の UI に使うなら imgui 以外の選択肢は無いと思います。
ちなみにこの UI モジュールの元ネタ(?)は、Windows アプリの皮をかぶった化け物であるところの WPF です。
一度は WPF の再実装みたいなことをやってましたが、C++ で柔軟なリフレクションするのはものすごく大変だったので当然の挫折。その後レイアウトの仕組みは残しつつ、HTML+CSS の真似事をしながら今の形になっています。
ぼくはいったい何を・・・
どうでもいいこと書きすぎた気がする。いったい何と戦っていたのだろう。
とりあえず色々な言語に、簡単に使えるちょっと背伸びしたゲームエンジンを提供したい。みたいなことが言いたい。
拙い文章ですが、読んでいただきありがとうございます。