Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

Mirrativでのビジュアルスクリプティング(Bolt)活用事例とテクニック

こんにちは、ミラティブUnityエンジニアの菅谷です。 ミラティブでは先日Mirrativアプリ内で遊べるライブゲーム「エモモランあーるぴーじー」をリリースしました。

エモモの衣装やアイテムを装備、強化して敵を倒しながらステージを進んで行くランゲームです。

エモモで遊べるオリジナルのゲームで、全てミラティブが開発しています。 ゲームの開発にあたって開発効率を高めるためにBolt(VisualScripting)を導入し、エンジニアでなくとも新しい機能や動きを追加できるように挑戦してみました。 今回リリースを迎えることができたのでBoltを使用したメリット・デメリットを紹介します。

実際にゲーム内で使われているBoltのコード例

Boltの紹介とその特徴

BoltはノードベースでプログラミングができるUnityのアセットです。UnrealEngineでのブループリントに相当するツールに思っています。元々は外部ディベロッパーが作成しアセットストアで販売する有償アセットでしたが、Unityが公式で無償化し誰でも使えるようになりました。Unity2021からはVisualScriptingの名前でUnityツールとして導入されています。 実際にBoltを利用してみて以下のような特徴を感じました。

①エンジニアでなくとも処理が組みやすい

本来はC#(スクリプト)で行うような処理をほとんどそのままノードベースでプログラミングできるようになっています。 また、ノードが繋げない=次の処理が対応していないことがすぐに分かるためコンパイルエラーに相当する問題が発生せず、セミコロンがない、変数名が間違っているなどの初歩的なミスがそもそも起こりません。また、実行中の処理が視覚的に見えるためどこでエラーが起きているかがわかりやすいです。

数値(Integer)同士の比較はできるためノードが繋げられるが、数値と文字列(String)の比較はできないため繋ぐことができない。

②Unityの機能をフルに使える

Behavior DesignerやArborといったグラフエディタと比べるとBoltはUnityの機能自体がそのまま使えるようになっています。例えばアニメーションやTimelineをノードで制御したり、エフェクトやサウンドのコントロールもBoltから直接行えるようになっています。そのため本来はスクリプトで制御するようなUnityと密接に関わるようなロジックもBoltで組めるようになっています。

③ロジックのアセットバンドル化ができる

Boltの処理はMacro AssetというUnityのアセットとして保存されます。これはアセットバンドルとしてビルドし配信できるため、アプリ本体の更新をせずに処理を追加・更新できるようになっています。Mirrativの開発速度やリリース速度は非常に速いため、アセットバンドルの更新だけですぐに反映される仕組みはエンジニアであってもメリットです。

Boltの採用理由と経緯

今回のゲーム開発では、上記メリットに加えて非Unityエンジニアではあるが、Boltを使って1つのゲームが作れるほどのデザイナー兼プランナーがいたので思いきってBoltを採用してみることにしました。 ただし、Boltのデメリットも感じていたので、動きのバリエーションが増えそうなゲーム内の敵のロジックだけに絞って採用しています。 エンジニア1名、デザイナー兼プランナーの合わせて2名だけでゲーム開発がスタートしており、それに伴った基盤システムの構築が急務だったため、Unityエンジニア以外のメンバーにもロジック作成をお願いすることにしました。

Boltでの作成の流れ

上述したとおり、今回のゲームは敵のロジックに絞ってBoltを使っています。それ以外の処理はすべてスクリプトで記述しています。Boltを部分的に制限して採用した理由は以下です。

  • ノードベースの処理とスクリプト処理とを混在させないため
  • 複雑な処理の場合はノードベースでは可読性が低くなりやすいため
  • Boltを使う箇所=オブジェクトで閉じた処理に制限したかったため

Mirrativアプリ本体やエモモの動きなどはスクリプトで書かれており、ライブラリや設計などの基盤もすでにあるため、Unityエンジニアにとってはスクリプトのほうが書きやすいです。自由にBolt化してしまうとノードベースの処理とスクリプトの処理が絡み合うため影響範囲が追いにくくなってしまいます。また、ノードベースではIDEでの補間ができなくなったり、参照が取れないため保守性が下がります。今回はプランナー・デザイナー目線から調整してもらいたいゲームバランスやエフェクトなどの見た目といった繰り返し変わりやすいポイントに対して高速なイテレーションを回すためにBoltを採用しています。エンジニアで担当するような変わりにくい処理はスクリプトで書いたほうが制御しやすいです。

また、ゲーム開発ではゲームのロジック以外にも通信機能などのMonoBehaviourに依存しない処理や、アセットバンドル管理、オブジェクトプーリングなどロジックが複雑化しやすい処理の実装も必要です。これらまでBoltで行おうとするとBoltのコードが肥大化し可読性が下がり処理が追えなくなってしまいます。

Boltは単一オブジェクトに対する振る舞いを作ることは得意ですが、他のオブジェクトと連携する場合はCustomEventでのメッセージングやグローバルな変数を利用するため、処理が追いにくくなりやすいです。そのため今回は敵オブジェクトという単一オブジェクトの振る舞いに対してだけBoltを使い、処理を閉じ込めることで保守性を高めるようにしました。 ただし、いくつかの処理はスクリプトと連携する必要がありました。

Boltとスクリプトの連携

スクリプト→Boltの例

例えば敵のHPやプレイヤーの位置などをBolt内でパラメータとして使いたい場合はVariablesの仕組みやCustomEventをスクリプト側から制御するようにしています。 Variablesはオブジェクトごとに自由にパラメータをもたせられる機能ですが、そのパラメータをスクリプトからも変更することができます。 また、CustomEventはイベントの形でBolt内にメッセージングを行えます。 これらを使ってBoltで扱うパラメータを更新したりBolt内のイベントを実行したりといった連携をしています。

public class MiniGameBolt : MonoBehaviour
{
    public void UpdateHp(int hp, DamageType damageType)
    {
        // Bolt内のHPを更新する
        Variables.Object(gameObject).Set("HP", hp);

        // Bolt内の特定のイベントを実行する
        CustomEvent.Trigger(gameObject, "DamageEnemy", (int)damageType);
    }
}

MiniGameBoltをオブジェクトにアタッチしておき、UpdateHpの関数をスクリプトから呼ぶとBoltのコードに通知されます。それによりBolt内のHPパラメータが更新され、CustomEventとして設定していたDamageEnemyイベントが実行されます。

Bolt→スクリプトの例

例えば敵やオブジェクトの生成はすべてスクリプトで制御されたオブジェクトプールを通しています。そのため敵の攻撃によって生成する新たな敵などはオブジェクトプールを通して生成する必要があり、Boltだけでは制御しきれないため専用に作成したスクリプトの関数を通して生成するようにしています。 Boltではオブジェクトにアタッチされているpublicメソッドを呼び出すことができるため、Boltから呼び出していい関数は専用のクラスで使っています。

public class MiniGameBolt : MonoBehaviour {

  // 購読側でオブジェクトプーリングにより新たな敵を生成する
  public IObservable<(int, Vector3)> OnRequestSpawn => subRequestSpawn;
  readonly Subject<(int, Vector3)> subRequestSpawn = new Subject<(int, Vector3)>();

  // Boltからよぶ
  // vは敵のIDに相当
  public void SpawnEnemy(int v, Vector3 position)
  {
    subRequestSpawn.OnNext((v, position));
  }
}

敵のBolt内で新たな敵を生成しています。生成タイミングやルールはBoltで制御し実際のインスタンス生成はスクリプトで行います。

連携が多いほどBoltとスクリプトが行ったり来たりして複雑化していくため、スクリプトでできることはスクリプトで、Bolt内でできることはBolt内でというルールを徹底し、できるだけ連携による影響は小さく無駄がないようにしています。

Boltを使った開発の課題とその解決法

初めからBoltやノードベースのプログラミングに精通してわけではないので開発おける困難や不明瞭な点も多くありました。いくつかの課題と知見を紹介します。

①AOTプレビルド

モバイルで動かす際に更新が必要になります。Unityエディタで動いていても実機で動かないパターンがあればほとんどの原因はこれでした。 メニューのAOT Pre-Buildを実行しAotStubsのファイルを更新することで解決します。

docs.unity3d.com

②コンソールにunit options failed to load and were skippedの警告が出る

スクリプトを更新するとクラスや関数がBoltで使えるかの判定が行われます。これはLidiq Background Workerの処理として観測でき、都度10秒程度かかります。警告はメニューのBuild Unit Optionを行うと消えますが、判定自体は常に行われます。また、スクリプトを変更していなくてもIDEからUnityエディタにフォーカスを変更しただけでこの処理が行われていまい、都度待ち時間が発するため開発におけるネックになってしまいました。この課題はReload Domain を無効にすることでフォーカスの変更による更新は削減できました。ただし、少しでもスクリプト更新が伴うといまだに発生しています。これはBoltに関係のない部分の変更まで検知し更新対象としてしまうためと考えており、Assembly DefinitionをMirrativ本体のコードとBoltと連携している部分のコードとに適切に分けることで起きにくくなると予想しています。Boltのオプションからこの更新自体を無効にする設定が見つけられず、また、MirrativではAssembly Definitionを設定できていないため現在もこの課題は残っています。

docs.unity3d.com

③UnitOptionの肥大化

上記のBuild Unit Optionを行うとUnitOption.dbというファイルにBoltで使えるクラスや関数のデータが登録され、UnitOption.dbがプロジェクト内に生成されます。Mirrativでは全クラス分登録されてしまうため、このファイルが50MB以上になっていました。調査したところエディタ上でのみ使われるファイルで、ビルド後のアプリには含まれておらずアプリサイズが肥大化することはありませんでした。しかしながら都度更新をし続けるとリポジトリが肥大化してしまうため、このファイルはGit LFSの対象として更新しています。

④スクリプトとノードベースで処理の読み方が異なる

スクリプトとノードベースでは同じ処理を実現していても使用する頭の使い方やコードの読み方が思っていた以上に違いました。 また、2つの仕組みが独立していることからも設計の統一も難しくなります。

Unityエンジニア以外が作成するBoltの処理はUpdateを中心とした更新処理で作成されています。一方MirrativのUnityエンジニアはイベント駆動で実装することが多いです。この2つの処理を混在して考えると理解の難易度が上がってしまいました。実際に開発の途中でサンプルとしてノードベースでUIの演出を組んでもらい、それをスクリプトで書き直す作業をしてみましたが、自身で作成する何倍もの難しさを感じました。また、視覚的に認識できるノードの数も多くないため一つ一つのノードをたどりながら理解していく必要がありました。

他にもスクリプトとノードベースのコードは認識の仕方が違うと感じています。スクリプトでは定義や変更する変数を最初に書きその後に内容を書きます。また、条件式であることが最初に分かります。

int a = 0;
if (a == b) ~~

「intのaは0である。もしaとbが同じなら〜」と捉えます。

ノードベースのコードでは内容が最初に来てその後手続き的に変数の更新を行ったり、計算の結果を条件式に渡したりします。

「intの0をaに設定し、aとbを比較して、その結果が同じものだったら〜」と捉えます。

そのためスクリプトの考え方だとBoltの処理を矢印に逆らって後ろから読むことが多くなり、見た目反してしまうため理解のしにくさがありました。 Bolt単体を使用する脳に切り替えられるとそこまで問題はないのですが、スクリプトと並行して理解しようとすると脳の切り替えが大変でした。 今回はUnityエンジニアがスクリプトを管理し、プランナーなどの非エンジニアがBoltを管理すると割り切って作っていたためそこまで問題は発生せず苦労しませんでしたが、同じエンジニアがスクリプトとBoltの両方を管理しようとすると大変だっただろうと予想しています。

まとめ

今回のゲーム開発ではBoltを利用してみました。ゲームの開発規模が数ヶ月と短かったことや、エンジニアではないがBoltを使えるメンバーがいたことなど、複数の幸運が重なったおかげでBoltに挑戦できました。結果、当初考えていたメリットを活かせており満足しています。また、発生した課題一つ一つに向き合うことでBoltのメリット・デメリットを理解しながら開発が進められ、効率の良い開発に繋がりました。ノードベースのプログラミングはUnity Technologiesも開発やアピールに力を入れている機能なので今後ますます使われていくと思っています。 ゲームはリリースされたばかりでこれから更新や運用を開始していくフェーズに入るため、また新たな課題が出てくるかもしれません。都度解決し知見として広めていきたいですね。

We are hiring!

ミラティブは積極的に挑戦していく文化があります。また、開発サイクルが短いこともあり多くの挑戦がしやすいです。Boltの導入もそのうちの一つで無事成果に繋がりました。良いサービスを作るためには同じやり方に固執せず技術の進歩に合わせながら常にアップデートしていくアンテナや柔軟性が必要です。ミラティブでは一緒に変化を楽しみながら作っていく仲間を募集しています! www.mirrativ.co.jp