開発状況 2019 #11
引き続き、ゲーム作りながら更新を続けています。
CI/CD 環境を GitHub Actions へ移行中
これまで AzurePipelines を利用していましたが、GitHub Actions を試しています。
それに伴い、ずーーーっと dev-0.9.0 ブランチで作業していたのを master メインで開発するようにしました。
Effekseer (Vulkan) 組み込み中
少し前から、Lumino の Graphics バックエンドは Vulkan をメインで使用するようになりました。
Lumino が利用している Effekseer も Vulkan バックエンドに(主に私が)対応中で、ちょうど Lumino がテストプロジェクトみたいな形で開発を進めています。
で、大枠は安定して動くようになってきました。また細かいところが対応しきれてないけど。ミップマップとか。
UI
コンボボックスが出来上がってきました。
Ruby 版 Lumino
C++ コード解析して gem 作る黒魔術に目途が付いてきました。
GitHub Actions への移行完了に合わせて、pre 版の gem を公開したいと思います。
年内くらいには行けるかな?
開発状況 2019 #10
今月はエンジンもゲームもあまり進捗ないです。
・・・ないというか、見せられるものがあんまりないです。 個人的には見た目より、特にセーブデータみたいなデータ構造を後回しにするとたいていロクなことにならない嫌な思い出があるので、今は表に出てこない後ろの部分を詰めています。
Lumino もメインは不具合修正です。
Graphics
Vulkanバックエンドが安定してきたので、OpenGLよりも優先して使用するようにしました。
また RenderPass などを Graphics モジュールから API として公開することで、ドローコールのたびに VkRenderPass をプールから検索するようなオーバーヘッドを無くしました。
OpenGL の頃より最低でも3倍くらいは速くなりました。
Binding
(息抜きプログラミング。・・・いや、一応ロードマップ的には次のリリースに入る機能だけど・・・。
仮想関数のオーバーライドに対応しました。
Ruby で Component のサブクラスを作ってカスタムコンポーネントを作成できるようになりました。
あとなんか例のBlawnで動かしたい気持ちが強くなってきてます。
開発状況 2019 #9
関数型のゲームフレームワークってどうなんだろう?
少し前からアクションゲームを作り始めています。
アクションゲームと言えばRPGと並ぶ人気ジャンルで、開発ツールも色々あります。
ただ今回は Lumino の供養も兼ねていたりするので、必要なだけの機能を持ったアクションゲームのシステムをスクラッチで作ることにしました。
ステートマシンという魔物
アクションゲームと言えば超巨大な状態遷移モデルであり古くは大量の switch-case を書き下さざるを得ず、慎重に慎重を重ねてシステムを作っていました。
しかし、最近は次の要素をつなげて作ることが多いそうです。
- アクション自体を定義する「ノード」
- "歩く", "ジャンプ" など
- アクション間の遷移条件を定義する「トランジション」
- ボタンが押されたら "ジャンプ" へ遷移する、など
なるほど良さそうです。
でも実際にこれでキャラクターの取りうるアクションをすべてつなげるとどう見えるんだろう?switch-case よりは簡単に扱えるんだろうか?
いろいろツールを見てみると、ジャンプしたり攻撃したりダメージ受けたりを網羅するとこんな感じになるみたいです。
・・・これをメンテするのはちょっと無理かな。
プログラマ出身としては switch-case の方がまだメンテしやすいと思う・・・。おとなしく枯れた手法を使うべきか。
ちなみにこの例では、同じノード間で矢印がたくさん出ているところがありますが、「壁に接触したら」「地面に接触したら」「坂に接触したら」みたいな遷移条件を個別に指定する必要があるらしいです。
関数型というアプローチ
最近仕事で Web 系のシステムを作っていて、React を学ぶ機会がありました。
React は状態遷移を上手く(数学的に)単純化することに成功しているフレームワークかなと思っていて、何とかこの単純さをゲームシステムに持ってこれないかなぁと、ここ最近試行錯誤しています。
基本はこんな感じ。
- 状態遷移に関係するすべてのパラメータを "ActionState" というひとつのクラスにまとめておく。
- "ActionStateをもとにした遷移条件" と "動作" をまとめて "Action" として定義しておく。
- Action は、常に ActionState を監視する "PassiveAction" と、プレイヤーやAIの入力判断を受けてアドホック的に実行する "AdhocAction" がある。
- PassiveAction は ActionState が少しでも変わったら定義されている Action の条件をすべて評価してゆき、"優先度" の最も高い Action を遷移先として採用する。(落下、被ダメなど)
- AdhocAction は PassiveAction の遷移処理に割り込む形で評価・実行される。より優先度の高い PassiveAction が実行されていた場合などはリジェクトされたりする。(ジャンプ、攻撃など)
(Adhocが入ると関数型としてはグレーか・・・?)
目標
ある Action を作るとき、
- その Action に遷移したときに「必ず成り立っていなければならない条件」
- その Action の振る舞い
- 優先度
だけを考えれば良い、ところまで持っていきたい。(もう箱と線をつなぐのはイヤです・・・)
もちろん状態と振る舞いの分離や副作用の排除は徹底的に。これは大前提以前の約束事。
これで Action の正当性、再利用性、保守性を上げていきたい。なぁ・・・。
実装の雰囲気
ActionState は↓こんな感じ。
class ActionState { Vector2 movingTargetPosition; // アクターが移動するべき位置。停止中は 0 Vector2 velocity; // アクターの速度 Direction direction; // アクターの向き bool isOnGround; // true の場合は地面に立っている, false の場合は空中にいる }
Action は↓こんな感じ。これは "走り" の Action。
class Action_Run : public Action { // "走り" への遷移条件 bool checkPossibleTransition(const ActionState* newState) override { // 地上にいる & 移動先が指定されている return newState->isOnGround && newState->movingTargetPosition != 0.0f; } // 動作本体 void onUpdate(Actor* actor, const ActionState* currentState, int frameIndex) override { // actor の走りモーションを再生したり。 // currentState->movingTargetPosition が 0 でなければ、その方向に actor の速度を向けたり。 } }
アクションを登録するところは↓こんな感じ。後ろの方が優先度が高く、同一優先度は許可しません。
passiveActions.add(new Action_Idle()); // 待機 passiveActions.add(new Action_Run()); // 走り passiveActions.add(new Action_Rise()); // 上昇 (ジャンプ) passiveActions.add(new Action_Fall()); // 落下
こんな風に準備しておいてからボタン入力のところで次のようにすると、"走り" へ遷移できます。
if (Input::isPressed(InputButtons::Right)) { // →が押されていたら、Actor の少し右側を移動先とする actionState->movingTargetPosition.x = player->position().x + 1; }
AIの場合は↓こんな感じ。
// player の右側へ移動 enemy->actionState->movingTargetPosition.x = player->position().x + 1;
もうひとつ例を。落下は↓こんな感じ。物理エンジンの結果を受けて isOnGround や velocity が変更されたらこれに遷移します。
class Action_Fall : public Action { // "落下" への遷移条件 bool checkPossibleTransition(const ActionState* newState) override { // 空中にいる & 速度が下向き(ちなみにジャンプ直後などの上昇中は符号反転) return !newState->isOnGround && newState->velocity.y <= 0.0f; } // 動作本体 void onUpdate(Actor* actor, const ActionState* currentState, int frameIndex) override { // 0フレーム目、actor の落下モーションを再生したり。 // currentState->movingTargetPosition が 0 でなければ、その方向に actor の速度を向けたり。 } }
ひとまず冒頭の絵みたいに上手く動いていますが、スーパーオレオレメソッドなのでまだ色々詰める必要が出てくるかもしれません。
Rendering モジュール更新
Lumino に話を移します。
Effekseer の低レイヤーグラフィックスをお手伝いしている一環で、Metal や DX12 も勉強しないとなぁ・・・な気持ちになってきているので、試しに Lumino を Metal に乗っけてみるか、なことを考えていました。
Vulkan 対応したことでモダンAPIに必要なリソースの取り回しはわかってきたのですが、Metal や DX12 対応で似たようなコードを何千も量産しなければならないのはちょっと辛い。
というところでコードの共通化を考えたいのですが、上手いこと共通化するには 0.8.0 までで公開してきた Graphics モジュールの API を変えないとならない・・・。
Lumino 使ってるのは多分自分だけなので変えるのはいいのですが、そのインパクトをバッチリ受けるのは Graphics の上に載って3Dシーンのレンダリングを担当する Rendering モジュールでした、ということでリファクタリングしてました。
スプライトの頂点バッファをリングバッファ使っていたりしたのをやめて、CommandList の Begin 前にすべてのグラフィックスリソースを fix させて、リソースの転送のために RenderPass が途切れないような仕組みに変更しています。
メモリ使用量が1.5倍くらいになるけど、コードがスッキリ&描画速度1.5倍くらいになる・・・かな?(実装中)
開発状況 2019 #8
Lumino の開発をおしまいにしようか悩んでました。
ことの始まりはとあるOSSのプロジェクトにコミットをお願いされたことです。多分 Lumino 作りながらじゃリソースがきつい。
宣伝もせずユーザーもいないのにずーっと長いこと開発を続けるのは馬鹿だっていうのは何年も前かわかってたことだけど、それでも続けてたんだからモチベの源泉はそんなことじゃないんだろうなと思いつつ、 しかしいいかげん区切りをつけるにはいいタイミングかとか、自分が書いたコードがより多くの人の役に立つのはどっちだろう?という感じでしばらく悩み続けたり、Lumino の位牌を買おうとしたりしていました。
だけどやっぱりすぐに捨てるのは無理でした。
8年も作ってるともう愛着以上のなにかがあって、いざ離れてみると悲しくてどうしようもないし、なんか変な夢見るし、頭痛くなるし、なんかこう呪いっぽい。
とはいえこれはどこかで区切りつけないと多分いろいろな人に迷惑かけそうな気がしてます。
なので、がんばって供養することにしました。
一番残念なのがゲームエンジンをゲームエンジンとして一度も使わずにクローズしてしまうことなので、Lumino でゲーム1本作ります。
プランは5月に引いたのがあるので、ひとまず1年で。
で、モノは2Dアクションです。
またタイルエディタ自作とか変なことやってしまったので、これはこれで利用していきます。
開発状況 2019 #7
5~6月は投稿を完全に忘れるくらいお仕事が盛り上がっていました。
実は5~12月くらいで1本ゲーム作る計画を立てて、それに沿って Lumino の開発を進めてたりしたのですが、いきなり仕事の影響で出鼻をくじかれてやる気無くしてた感じです。
マルチプログラミング言語
次の v0.9.0 のメイン機能です。
まず、こんな感じでマクロを使って C++ のクラスやメソッドをタグ付けしておきます。
LN_CLASS() class Sprite : public VisualObject { ... };
このコードを clang で解析して、
みたいなことをします。
まずは Ruby をターゲットにしていますが、次のようなコードで、画像を表示することができます。
require 'Lumino' Engine.initialize texture1 = Assets.loadTexture("img.png") sprite1 = Sprite.new(texture1) while Engine.update do end Engine.finalize
こんなふうに、C++ インターフェイスを他の言語に公開するのがすごく簡単になります。
メソッドのオーバーライドやイベント通知もサポートするよ。
エディタ
ゲーム開発を始めて最初のころは「コードだけでもなんとか行けるでしょ」とか思っていましたが、シーン作り出すあたりから限界が見えてきました。 やっぱり微調整のイテレーションオーバーヘッドが主な理由です。
というところで、まずは GUI フレームワークの検討から始めました。
要件は、
- クロスプラットフォームなデスクトップアプリ
- Lumino を組み込めること
- C++ 以外の言語でエディタ拡張を書けること
ということで、候補は次の通り。
- Qt Quick
- Qt Widgets
- imgui
- LuminoUI (自作)
Qt Quick
そこそこキレイなアプリが作れるフレームワーク。ロジックを C++、ビューを QML (JavaScriptライク) で書きます。
レガシーなフレームワークありがちな、細かいレイアウトの調整ができない問題がクリアされていて、見た目をかなり自由に作ることができます。
ネックは情報量でしょうか…。バグか仕様かわからずコードまで読みに行ったりしてました。
もうひとつはグラフィック固有の問題ですが、OpenGL などネイティブグラフィックスAPIの呼び出しはレンダリングスレッドで行う必要があるため、 Lumino のような外部のレンダリングライブラリを取り込むときは細心の注意が必要です。
これに対応するため Lumino のマルチスレッド対応とかやってましたが、Qt API (妙なところに const 付いてたりとか) に合わせこむため余計にクラス増やしたり、モノによっては本体のコード追ったりするのに疲れて断念しました。
っていうか、よく考えたらエディタ拡張を書きたいときでも View 作るのに QML 必須になってしまうか…。
Qt Widgets
枯れていつつも長く使われているのは重要な長所です。でも空ウィンドウ開くだけでメモリリークするのはちょっと心に悪いです。
簡単な UI 作るのにはすごく素直に動いてくれるので、Qt Quick に対しては長く使っていました。Ruby ラッパーとかからも呼べるし。
レンダリングライブラリの統合も、OpenGLWidget を使うのもいいし、ネイティブのウィンドウId取ってそこに書くのもいいし、簡単でした。
ただ、ゲーム系というかエンタメ系というかの GUI ツールって結構独特なウィジェットが多くあります。それに耐えられるのか…ってところで、 「結局 QPainter で直書きするくらいなら Lumino で書きたい」みたいな魔が差して断念。
imgui
これの検討を始めるころには、Lumino の UI モジュールがそこそこいい感じに動くようになってきたので、 ものすごく悩んだのですが採用は見送りました。
主な理由は UE4 が Slate を自作した理由とほとんど同じです。 https://docs.unrealengine.com/en-US/Programming/Slate/Architecture/index.html
それでも便利なのは変わらないので、デバッグ用途では使っていくかも。
LuminoUI (自作)
死ぬわアイツ
正直 100% ゲーム用の UI フレームワークとして作っていたので、エディタで使うつもりはありませんでした。
しかし、色々とアレもやりたいコレもやりたいを詰めていった結果、徐々に レイアウト、スタイリング、アニメーション、イベントシステム周りが WPF+HTML(CSS)+Qt みたいなかなりガチ仕様になってきていました。
コンポーネントは圧倒的に足りてないけど、ベースが結構いい感じになってるので、シンプルなエディタなら作れるのでは?ついでにほかの言語のラッパーも簡単に作れるし、エディタ拡張もLuminoだけで完結するし。 …と自惚れた結果、全部 Lumino でやるという(多分地獄の)道を選ぶことにしました。
見た目は雑なシーンビューアですが、ゲームジャンルごとに必要そうな小エディタ(拡張機能)を組み合わせて、作りたいゲームに合わせたエディタを構築できるような仕組みを考えています。
3Dシーンエディタ・2Dモーションエディタ・RPGイベントエディタを組み合わせて、2.5D RPGを作ったりとか。
Effect
ゲーム作りを始めたので、エフェクト出せる仕組みが欲しくなりました。
Lumino はパーティクルシステムを持っていますが、もっと凝ったエフェクトも出せるようにしたいな、ということで Effekseer の組み込みを始めています。
で、Lumino はちょっと前に Vulkan への実験的な対応を行い、開発中は基本的に Vulkan で動かしていますが、Effekseer が Vulkan 対応してない!という状態でした。
対応を待つか、Lumino 側でラップするか… とかいろいろ考えていましたが、せっかくなので Effekseer の本流にコミットすることにしました。
GitHub は長く使っていますが、今回初めて、ほかのプロジェクトへプルリク送る運びとなりました。
Graphics
ロードマップ上では fix 済みのモジュールなのですが、Vulkan 対応を受けて色々思うところがあったので、少し API を変えようとしています。
具体的には RenderPass と CommandList の実装です。
ねらいはこんな感じ。
- 描画速度向上
- GPU 機能の並列実行 (特に並列コンピュートシェーダ)
- 他のアプリへの組み込み易さ
他のアプリへの組み込み
今の Lumino は Graphics や Audio など様々なモジュールがひとつになった巨大なライブラリですが、そのうち小分けにして配布しようと思っています。
- クロスプラットフォームな3D描画やりたいときは LuminoGraphics 使えるよ
- オーディオグラフ作りたいときは LuminoAudio 使えるよ
- ステートフルな UI 作りたいときは LuminoUI 使えるよ
みたいな。
・・・自分のリソース絶対足りてないなぁ
開発状況 2019 #4
平成最後ですがいつも通りです。
master ブランチにマージした CI ビルドが終わったら 0.8.0 リリースです。この記事投稿してる時も多分どっかのサーバでガシガシやってると思います。
Graphics
OpenGL ドライバで fix です。API は DirectX11 に近い感じで着地となりました。
シェーダは HLSL5 で書いて、Web やモバイルでも動かせちゃったりします。
Vulkan は Experimental ですが一応使えます。GPU が対応していれば・・・。
チュートリアル
未完成 & 穴だらけですが、チュートリアルを作る気持ちだけ公開しました。
まだまだ変更入れたい予定です。
今後について
次の 0.9.0 はトランスコンパイラの実装です。clang 使ってコード解析してラッパーを自動生成するやつです。
ただそれやる一方で、ちょっとゲーム1本くらい作ろうと思います。C++で。
アクションにするかローグライクにするか、色々ネタがたまってるからどうなるかは計画中ですが、できればコードは OSS にして Lumino の Example project にしてみたいと思います。
開発状況 2019 #3
今月は主に、チュートリアルで使う2D物理演算とUIシステム、それと Vulkan 対応を進めていました。
Font
これまではフォントを使うときは必ずプログラムのどこかでフォントファイルを登録する必要がありましたが、 mplus フォントをビルトインフォントとして使わせていただくことにしました。
ただやっぱりファイルサイズが大きいので、ひとまず Lumino をライブラリ単体として使うときは ASCII サブセットのみの対応としておきます。 (lumino-cli を使ってデプロイするときは日本語ちゃんと使えるように Asset をくみ上げる予定)
Rendering
UI要素を描画するために必要な機能をいくつか追加しました。 特に、背景と枠。
Animation
プロパティアニメーションを実装しました。 ただ当初はチュートリアルで使おうと思っていましたが、多分直近では不要になりそうなのでちゃんとテスト書いて公開するのはちょっと先になります。
Physics
2D物理システムについて、レイキャストやコリジョンコールバックなど、ゲーム開発に最低限必要な機能を実装しました。 ドキュメントが全然足りてないのが最大の問題・・・。
Graphics
Windows では Vulkan がかなりいい感じに動くようになってきました。 次は macOS, Android あたりで対応したいけど、0.8.0 に向けての Vulkan 対応は Graphics モジュールのモダンAPI対応によるインパクトを見るのが主な目的なので、Windows 以外はもう少し先です。
今後について
Graphics モジュールの API がほぼ固まったので、0.8.0 リリースに向けて仕上げていきます。まぁ、主にユニットテストとドキュメントですが。
Vulkan をどうやって使ったかはどこかでブログ書いておきます。っていうか、残しておかないと自分が忘れる・・・。