ほげたつブログ

プログラムとアニメーションをかじって生きてる

UE4でゲームを作る時に考えていること2選

こちらは「Unreal Engine 4 (UE4) Advent Calendar 2017」11日目の記事です。

Unreal Engine 4 (UE4) Advent Calendar 2017 - Qiita


冬コミが近くて絶賛炎上中です。(タスケテ)
技術的な記事を書く上で検証をする時間が取れないので、さらっと書きます。

UE4 でゲームを作る時の決め事

同人や会社で UE4 を使ったゲーム開発やプラグイン開発を始めて2年と数ヶ月になりました。大中小様々なプロジェクトで開発を行う中で、自分の中でなんとなく考えている事を言語化してみます。

■ Actor はそれ単体で動作及びデバッグができる状態を保つ

これは最も重要で、何かしらのモノを作る場合にはいつも頭の片隅に置いています。いくつか具体例を挙げてみます。

1.Actor は World に設定された GameMode や PlayerController に依存しない

GameMode や PlayerController 等は、それらを継承したクラスを各種レベルに設定できますが、Actor 内に書かれるロジックでは 特定のGameMode や PlayerController を継承したクラスへの Cast を一部の例外を除いて禁止 しています。この理由を説明するにあたり、自分がこれらのクラスの役割をどのように認識しているのかをざっくり説明します。


GameMode

ゲームルールの管理及び、全体のシーケンス管理を行います。場にどういった要素があるのか、どういう状態なのかを監視し、適切なタイミングで状態を移行させ、必要な情報をユーザーに与えます。


例えば戦車で戦い、敵を一掃することが勝利条件のゲームを作っているとします。戦車は走行し、砲塔を回転させ、砲撃することが可能です。GameMode は両陣営の残機を監視し、どちらかの残機が0になったタイミングでゲームを終了させます。

ここでアイデアが爆発してしまい、戦車でマリオカートみたいなレースゲームが作りたくなったとします。与えられたコースを戦車で走り、時には砲撃で敵を邪魔しながら、最終的に先頭でゴールすることが勝利条件です。戦車は走行し、砲塔を回転させ、砲撃することが可能です。GameMode はゴールラインを監視し、順位付けを行い、全員がゴールしたらゲームを終了させます。


お気付きの通り、ゲームのルールに関わらず、戦車にできることは変わりません。ただ、「できること」と「やること」は違うので、ゲームルールによって砲撃等が使われないケースはあります。

つまり理想の上では「GameMode を変えれば同じ要素(Actor)を使って別のゲームが遊べる」ということになります。そのため、ただの要素に過ぎない Actor 内で、特定の GameMode が World に設定されているという前提でロジックを組む(Cast を行う)のは良くないという事がわかります。


PlayerController

ユーザー入力の管理を行います。ここで注意したいのが、PlayerController は入力に対する振る舞いを決定しますが、実際の動作については定義しません。

ユーザーが「左スティックを倒した」のであれば、PlayerController が受け取って操作(所有)する Character に「歩く」という動作をリクエストします。ユーザーが「ポーズボタンを押した」のであれば、PlayerController が受け取って GameMode に「ポーズ状態への遷移」という動作をリクエストします。回りくどく思われるかもしれませんが、Character 等の所有する
Pawn で直接入力を受け取らないのは理由があります。


まず、ゲーム中に「キャラクターが車に乗った」等で操作対象が変わり、入力に対する振る舞いが変わることに対して PlayerController が受け皿になることで吸収できます。操作対象に同じインターフェースを実装しておくことで抽象化もできると思います。ただし、これだけの理由であれば Action Mappings により吸収することも可能です。

ここで最も大きな理由となりえるのが 操作対象の Pawn はゲームの状態を意識してはいけない という、前項で説明した部分になります。例えば「カットシーンが再生されている間はユーザーの入力を制限したい」といった場合に、操作対象の Character 自身がゲームの状態を監視して移動制限を行ってしまうと、ゲームルールに依存した実装となってしまいます。

もちろん前述の考え方をするのであれば PlayerController も複数の GameMode から利用されることが望ましいのですが、経験則の上では GameMode と PlayerController は 1:1 で作ることが多く、それであればと PlayerController は例外的に特定の GameMode に依存してもいい という方針で作っています。(例えば MissionGameMode を作ったら MissionPlayerController と名付けるなどとして依存関係にあることを明示しています)

2.Actorが動作を行うために必要な要素は外部から注入する

Actor が何かしらの動作を行う際には、場に存在する他の要素に依存することがほとんどです。自身だけで動作が完結する Actor というものは少ないと思います。

ここで悪手なのが 依存する対象を自身で SpawnActor すること です。これを行うと、動作テストをする際に依存対象の Actor が正常に動いているという前提が必要になったり、それが他のアセットと依存関係があった場合にイテレーションの度に無駄なロード等が発生し、積もり積もって効率を下げる要因になります。


ここで必要になってくるのが依存性の注入です。例えば弊サークルが同人で作っているゲームだと CharacterController というクラスがAIの思考部分を担っているのですが、センサーからの情報取得や実行する動作などは細かく AITask として定義されています。そして CharacterController からリクエストされた AITask を保持して実行するのは AITaskRunner というクラスで、これは CharacterController をセットアップする際に外部からインスタンスを渡すようにしています。これにより有利になっているのは以下の点です。

  • 実装が省略されていたり、テストコードが入っているカスタマイズされた AITaskRunner を使用することができる
  • 1フレーム中に実行可能な AITask の最大数等のプロパティをレベルによって変えることができる
  • 将来的に AITaskRunner が負荷のボトルネックになった場合に、元のクラスを維持しながら ParallelAITaskRunner のように拡張された実装を試してみることができる


依存性の注入については Dependency Injection (DI) で検索するともっと詳しい説明が出てくると思います。

■ Actorを単体でデバッグする

Actorが単体で動作するようになったら、それ専用のデバッグレベルを用意しています。Actorの実装に変更を加えた場合、このレベルを開いて一通りデバッグすることで、結合時に発生するバグや、機能を拡張した際に生じるエンバグを減らすことができます。

また、デバッグしたい Actor(以降 HogeActor と呼びます)を ChildActor として保有する、デバッグ専用の Actor(以降 DebugHogeActor と呼びます)を用意することも有効です。この理由を何点か説明します。

1.派生クラスのデバッグをすぐに行える

HogeActor から派生した HogeActorA 、HogeActorB 等が用意されている場合、ChildActorComponent に設定するクラスを変更することで派生クラスのデバッグも瞬時に可能です。DebugHogeActor のプロパティに TSubclassOf< HogeActor > HogeActorClass; といった感じに宣言することで、シーンに配置した後に Details からプルダウンで HogeActor から派生クラスを選択でき、DebugHogeActor の ConstructionScript で ChildActor->SetChildActorClass(HogeActorClass); のように呼べば、たとえ実行中でもデバッグするアクターを切り替えられます。

例えばアクションゲームで操作キャラが複数いる場合など、デバッグシーン上で動かしながら任意のタイミングでキャラを切り替えられるとデバッグが結構捗ります。

ChildActorComponent は動作が安定していないところがあってなるべく避けてきたのですが、デバッグ用途としては SetChildActorClass した直後にインスタンスが取得できるのは便利ですね。(デバッグ用途としては)

2.外部から注入する依存オブジェクトを切り替えられる

DI の有用性については前述の通りですが、この構成にすることで、外部から注入する依存オブジェクトを実行前もしくは動的に切り替えることが可能です。依存オブジェクトをDebugHogeActor のプロパティに設定できるようにして、ConstructionScript が実行された時に HogeActor が参照する依存オブジェクトを切り替えることで実現可能です。

弊サークルが作っている同人ゲームの場合、CharacterController が判断して AITask をリクエストする仕組みなので、Character に設定する CharacterController を「特定地点に移動するだけの CharacterController」や「ゾンビを視認するだけの CharacterController」等に変更し、リクエストする AITask を限定して小さくデバッグを行うといった事もできています。

3.外的要因でデバッグが困難になった場合に対処できる

Actor は拡張された GameMode 等に依存しないように作るべきと前述しましたが、そのような状態がキープされ続けるというのは非常に稀です。例えば複数人で作業する場合、守り続けてきた純潔が他人の実装によっていとも簡単に崩壊するなんて日常茶飯事です。そんな時に DebugHogeActor のような層が存在すると、例えば HogeActor に付けられた悪事を働く Component の Tick を BeginPlay で強制的に止めたり、適当にパラメータを与えてとりあえずデバッグに支障が無いようにできるので心の平穏が保たれます。

4.デバッグ用のロジックを Blueprint で記述できる

某アンリアルフェスで某社が 「Blueprint は恋人、結婚するなら C++ という歴史に残る名言を残しましたが、自分もロジックを組む際には基本的に C++ を書いています。もちろん全てが C++ ではなく、演出用で調整が多そうな部分や UI は Blueprint を使っていますが、「設計及びリファクタのしやすさ」「利用可能な API の多さ」「速度面」「ぶっちゃけノードをマウスで繋ぐのが面倒」などの理由で C++ で記述することが多いです。

とはいえ、デバッグ用のテンポラリなコードまで全て C++ で書くとイテレーションが悪くなるため、DebugHogeActor の層で Blueprint で記述できるようになると非常に便利です。自分の場合は DebugHogeActor 自体は HogeActor が公開している関数が全て BlueprintCallable とは限らないので C++ で作りますが、更にそれを継承した BP_DebugHogeActor を作って利用するようにしています。


現場からは以上です。火事場に戻ります。

明日のアドカレ

明日(12日目)は dgtanakaさん による「マテリアルエディターのカスタムノードあれこれ」です。神記事が予想されるので正座して待ちましょう。