25
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Unreal Fest Extreme '21 Summer アンリアルクエストに参加して得られたもの その2 解説編で疑問に思ったCastについて調べてみた編

Posted at

#UE4の"Castはあまり利用しない方が良い"という情報はよく見かけるが・・・
UE4では"Castはあまり利用しない方が良い"という情報をよく見かけます。
理由については以下のように書かれていることが多いかと思います。

1. Castは負荷がある為、特にTick(フレーム毎の処理)で呼び出すことはお勧めしない
2. Castすると参照が増える為、動作が遅くなる
3. Castを使う設計はオブジェクト指向的ではないので、Castを使わないような設計をお薦めする
4. ○○○○プロジェクトではCastの使用は許可していない(これは理由ではありませんが)

どの理由を見てもCastが嫌われていることがわかります。
しかし、UE4のBPのノードは"Castを前提にしている"と言わんばかりの戻り値(ノードの右側のピン)を返してくる物もあるかと思います。

例えば・・・

・コリジョンを使おうと思っても、On Component HitのOtherActorは、そのままだと何に当たったか分からないし・・・

・プレイヤーに定義したイベントを呼び出したくてプレイヤーを取得するノードであるGetPlayerPawn・GetPlayerCharacterを使っても、PawnやCharacterのオブジェクト参照を返却するので、現在のキャラクターのクラスにCastしないと使えないし・・・

・プロジェクト設定で設定したゲームインスタンスから値を取得しとうと思ってGetGameInstanceノードを使っても、プロジェクト設定で設定しているGameInstanceクラスにCastしないと使えないし・・・

UE4のノードの戻り値はCast前提で作ってあるんじゃないの?と、思ってしまう作りになっている印象があります。
(プログラム的に言えば、On Component Hitの戻り値がそれぞれ適した型で返ってくるなんて作れないので仕方ないといえば仕方ないのですが・・・)

#初心者がハマりやすいCastという便利な仕組み

上記例でも触れていますが、UE4初心者が必ず組むことになるであろう、コリジョン判定のノード(On Component Hit)を例に具体例を見てみましょう。
OnComponentHitから何が衝突したかを取得するピンはOtherActorというピンになります。

Sample0.png

プレイヤーが衝突しようが、敵キャラクターが衝突しようが、その他のActorが衝突しようが、全てこのOtherActorから情報を取得することになります。
(もちろん、コリジョンプリセットで設定した内容によって衝突判定が発生しない組み合わせもありますが、ここは話しを分かりやすくする為に全てに衝突判定があるという前提となります)

例えば、このコリジョンにプレイヤーが衝突した場合はダメージを受けるが、敵が衝突した場合は敵の色が変わるという処理を書きたかったとします。
単純に考えれば、OtherActorをプレイヤーにCastできた場合は、そのCastしたプレイヤーのダメージ処理を呼び出し、敵キャラクターにCastできた場合は、Castした敵キャラクターの色を変える処理を呼ぶという処理を考えるかと思います。
実際にBPにしてみるとこんな感じです。
Sample1.png
ところが、これを作った初心者は、ある日ネットの掲示板で__「Castはあまり利用しない方が良い」__という情報を見かけます。
そこで考えた末に出来上がったBPは以下の通りです。
Sample2.png
プレイヤーのダメージ処理はUE4がApplyDamageというノードを用意してくれているので、なんとか実装できましたが、衝突した敵の色を変えるには、衝突した"何か(OtherActor)"を”衝突した敵”に変換する必要があり、変換とは"Cast"の為、ここで諦めてしまうことでしょう。

一体どうすれば!?

#そもそも、Castってそんなに悪者なの!?
様々なプログラム言語でCastという仕組みはありますが、ここまで嫌われていることはないと思います。
まるで親の仇のような扱いをされるには、何等かの理由があるのでしょうが、私にはその理由がわかりませんでした。

確かにリアルタイム性を重視するゲームプログラミングの特徴として、Tick毎にCast処理を入れたら無駄が多いので、Castが必要になった時点で一度だけCastしておき、その結果を保持しておくことでCastの負荷を減らすという発想はできますが、単純にそれだけが原因ではないと思い、調べてみることにしました。

#ハード参照?
更に調査を進めていると、__"UE4のCastは、呼び出し元オブジェクトに、Castしたオブジェクトのハード参照が追加されていまう”__という点が問題視されているようでした。

ここで新しい言葉、「ハード参照」が登場しました。

一番最初に紹介したCastをお薦めしない理由の一つに「参照が増える為、動作が重くなる」という理由がありましたが、どうも、この「ハード参照」のことを指しているようです。

さらに調べてみるとUE4には「ハード参照」と「ソフト参照」という2種類の参照が存在することがわかりました。
「ハード参照」とは参照先のBPクラスを自動的にロードする参照のことらしく、これが今回の問題の原因となっているっぽいです。
(「ソフト参照」については調べてきれていません・・・)

ハード参照は上述の通り参照先のBPクラスを自動的にロードしますが、これは連鎖的に動作し、ハード参照された先のBPクラス内でハード参照されたBPクラスがある場合は、現在のBP→参照先BP→参照先BP→参照先BPのようにクラス(インスタンス)が自動的に生成されてしまい、ロード時間が増え、メモリを圧迫する結果となります。

Sample9.png

Castを使うことで意図しないハード参照が発生し、知らぬうちにロード時間が増えたり、メモリを圧迫することを懸念した結果、Castの使用をお薦めしていない事が分かりました。

ただし、ハード参照が絶対的に悪いというワケではなさそうです。
例えば、レベル上に扉があり、扉を開くにはボタンに触れないといけない場合、扉とボタンはセットで存在する必要があります。
扉とボタンは強い依存関係にあり、どちらかが一方が存在しないということはないので、この2つはハード参照でも問題ないはずです。

#参照ビューワ
参照周りを調べていると、UE4の参照ビューワ機能がよく紹介されていました。
参照ビューワとは、選択したBPクラスが他のオブジェクトとどれだけハード参照・ソフト参照しているか分かるツールです。

試しに、一番最初に提示したプレイヤーと敵へのキャストが入ったBPクラスのオブジェクト参照を見てみると、こんな感じになっていました。

Sample10.png

ちょっと分かり難いですが、この参照ビューワから分かることは、レベル上にプレイヤーや敵がいなくても、このBPクラスを配置するだけで、自動的にプレイヤーも敵もロードされてしまうことを意味しています。
この程度の規模なら問題ないかもしれませんが、大規模なプロジェクトでは問題になりそうな感じがしますよね・・・

#「Castを使わない」という言葉だけが独り歩きしている印象

ここまで調べてみると、Castが悪いというよりは、使いどころを誤った参照が悪いという感じに見えてきました。
確かにCastの処理コストというものはありますが、それよりもBPクラス間の参照が絡み合う事で、不要で制御できていないBPクラスをロードする事の方が悪いって感じがします。

言い方を変えると、Castに限らず、参照を知らないうちに作ってしまう機能すべてに注意しなければならないという事なのかもしれません。

例えば、BPクラス内にカスタムイベントを作成して、そこのインプット(引数)の型に他のBPクラスを指定したとします。
Sample39.png
実は、これでもハード参照が作られてしまいます。
Sample40.png

これはあくまでも一例ですが、Castだけが悪者というワケではなさそうです。

#では、どうしたらいいのか?
調べてみると、これらの問題に対する解決方法は以下のようなものがありました。(他にもあるはずですが・・・)

1.ブループリントインターフェースを利用する方法
2.ブループリントの親クラスにキャストする方法
3.C++のクラスにキャストする方法
4.C++ブリッジ実装を利用する方法

そして、上記解決方法のどれか1つが正解というワケではなく、オブジェクトの関連性や、クラスの設計など様々な要因から、適した方法を検討すべきです。
「ここはインターフェースを使った方が設計的に正しいな」とか「ここは常にアクセスしたい値があるから、取得処理をC++で記述してブリッジとして利用しよう」とか、様々な事を検討すべきです。

本記事では具体例として、「ブループリントインターフェースを利用する方法」と「ブループリントの親クラスにキャストする方法」を説明してみたいと思います。(C++を使った解決方法については、C++を使ってる人にとっては当たり前な内容なのかな?と思い、深堀はしませんでした。もし需要があるようでしたら、C++を使った解決方法も記事にしていければと思います。)

#ブループリントインターフェースを利用する方法(解決方法その1)
これを説明する為に、試しに一つゲームを作ってました。
ThirdPersonテンプレートを元に、グレイマンが胸から2種類の弾丸を発射して、3種類の敵を倒していくだけのゲームの設計をしてみましょう。

グレイマンが発射する弾丸は
・青い弾丸(BP_BulletBlue)
・赤い弾丸(BP_BulletRed)
の二種類。

倒す敵は
・青敵(BP_EnemyBlue)
・赤敵(BP_EnemyRed)
・紫敵(BP_EnemyPurple)
の三種類としましょう。

敵は自身と同じ色の弾丸が当たった場合は弾丸と共に消滅するというルールにします。
(紫敵は青い弾丸でも赤い弾丸でも倒せる)

青い弾丸(BP_BulletBlue) 赤い弾丸(BP_BulletRed)
青敵(BP_EnemyBlue) 倒せる 倒せない
赤敵(BP_EnemyRed) 倒せない 倒せる
紫敵(BP_EnemyPurple) 倒せる 倒せる

早速作成開始です。
まずは何も考えずに組んでみたBPがこちらになります。

青い弾丸(BP_BulletBlue)のイベントグラフ
青い弾丸のイベントグラフ

赤い弾丸(BP_BulletRed)のイベントグラフ
赤い弾丸のイベントグラフ

弾丸のOnComponentHitのOtherActorをそれぞれの敵にCastしてみて、Castに成功したら敵と弾丸の破壊処理を実行し、Castに失敗したら次の敵のCastを試すといったロジックです。
ゲームのルールとしてはこれで正しく動作しますが、Castを使った弊害がどのように表れるのでしょうか?

それでは、早速作成した弾丸の参照ビューワを見てみましょう。

青い弾丸(BP_BulletBlue)の参照ビューワ
Sample14.png

赤い弾丸(BP_BulletRed)の参照ビューワ
Sample15.png

上記で解説している通り、弾丸に全ての敵のハード参照がくっついています。
ここで気を付けなければならないのは、弾丸はグレイマンにハード参照されているということです。(弾丸を発射する度にグレイマンから弾丸が生成される為)
つまり、グレイマンを呼び出すだけで、弾丸がロードされ、弾丸経由で全ての敵もロードされるのです。例え、画面上にグレイマンしか居なくても、すべての敵のオブジェクトまで読み込むのは無駄といえます。

プレイヤー(グレイマン)の参照ビューワ
Sample16.png
それでは、次にCastを使わないロジックに変更してみます。
UE4にはTag機能があるっぽいので、これを使えばなんとかなるかもしれません。
青敵には"BLUE"、赤敵には"RED"、紫敵には"PURPLE"というタグを入れた状態で、以下のようなBPとしました。

青い弾丸(BP_BulletBlue)のイベントグラフ(Tag使用版)
Sample17.png

赤い弾丸(BP_BulletRed)のイベントグラフ(Tag使用版)
Sample18.png
元々Castを使ってOtherActorを判定していた部分をTagを使って判断するように切り替えただけです。
当たり前ですが、Castで実装していた時とゲームの挙動は変わりません。

Cast版との大きな違いは参照ビューワで見る事ができます。
早速、参照ビューワを確認すると、先ほどまで絡まっていた参照関係が必要最小限になっていることが確認できます。

青い弾丸(BP_BulletBlue)の参照ビューワ(Tag使用版)
Sample19.png
赤い弾丸(BP_BulletRed)の参照ビューワ(Tag使用版)
Sample20.png
良いじゃん!良いじゃん!

でも、よく考えてみてください。今回のゲームの仕様はとても単純(弾丸2種類で、敵3種類)だったので、これでも良さそうですが、拡張性の事を忘れていませんか?
弾丸の種類が増えたら、このロジックをコピペして増やしていかないといけないですし、敵の種類が増えたら、敵の判定ロジックを数珠繋ぎに増やしていかなければなりません。

弾丸も敵も増えたことを考えるとゾっとします。

これを解決する考え方が「オブジェクト指向」というワケです。

「オブジェクト指向」を説明し始めると、それはそれで膨大な説明になってしまううえに、様々な解釈があるので、詳しい話しは端折りますが、オブジェクト(ここで言うところの「弾丸」と「敵」)はそれぞれ独立した要素であり、オブジェクト間で何か情報を渡す必要がある場合はインターフェースを使うという方法で実装してみたいと思います。

簡単に言えば、弾丸と敵は下記のようなイベントのやり取りをするイメージです。

弾丸:何かに当たった場合は、当たった先("敵"とは言っていない)に「弾丸が当たったよ」と伝える
:弾丸が当たる可能性があることを伝え、弾丸が当たったらどうするか?という処理を書く

ここで重要なのは、弾丸が当たった事を伝える先は敵ではなくても良いし、敵は弾丸を知らなくても、弾丸に当たったことさえ知れれば良いのです。

そして、これを実現する方法が__ブループリントインターフェース__なのです。

早速、ブループリントインターフェースを使って、今まで作ってきたゲームをブラッシュアップしてみましょう。

ブループリントインターフェースは、コンテンツブラウザ上で右クリックから「ブループリント」「ブループリントインターフェース」で作成することができます。
Sample21.png
今回は弾丸が当たった場合の処理の実装ということで、「BPI_HitBullet」というブループリントインターフェースを作成しました。

ブループリントインターフェースをダブルクリックしてエディタを開くと、読み取り専用の関数を作成する画面になります。
今回は青い弾丸が当たった場合の処理と、赤い弾丸が当たった場合の処理を敵に実装したいので、ブループリントインターフェース上では、2つの関数を作成します。

これは何をしているかというと、各敵のBPクラスに「青い弾丸が当たった時の処理を書いてね」「赤い弾丸が当たった時の処理を書いてね」という依頼をする為の元ネタを書いているという感じです。(OnHitBUlletBlueで青い弾丸が当たった時の処理、OnHitBulletRedで赤い弾丸が当たった時の処理を書く為の規約(インターフェース)を用意している。)
Sample22.png
ここで用意したインターフェースを敵に使ってもらわないと意味がないので、これを敵のBPクラスに実装していきます。
敵のBPクラスのエディタを開き、画面上部にある「クラス設定」をクリックすると、詳細ウインドウに「インターフェース」が表示されますので、そこにある「追加」ボタンをクリックし、先ほど作成した「BPI_HitBullet」を選択します。
Sample24.png
すると「マイブループリント」の中に「インターフェース」にOnHitBulletBlueとOnHitBulletRedが表示されるので、それらを右クリックして「イベントを実装」を選択してください。
Sample25.png
この例の場合は、青敵の為、青い弾丸が当たった場合はDestroyActorを呼びますが、赤い弾丸が当たった場合は何もしません。
同様に、赤敵の場合、紫敵の場合もインターフェースを実装してみましょう。

次に、インターフェースを呼び出す側の処理を書く必要があります。
青い弾丸のBPクラス(BP_BulletBlue)をエディタで開き、OnComponentHitのOtherActorに対して「青い弾丸が当たりましたよ」というインターフェースを叩くようにロジックを実装します。

とは言っても、、OtherActorピンからOnHitBulletBlueを呼び出してあげるだけです。
Sample26.png
こちらも同様に、赤い弾丸の実装もしてみてください。

これでブループリントインターフェースに対応した修正は完成!!と言いたかったのですが、無事に敵は倒せるようになったのですが、敵を倒した際に弾丸も消えていたはずなのに、この変更で弾丸が消えなくなってしまいました。

ブループリントインターフェースで定義したOnHitBulletBlueとOnHitBulletRedにアウトプットパラメータを追加することで対応してみましょう。
BPI_HitBulletのエディタを再度開き、OnHitBulletBlueとOnHitBulletRedにそれぞれ、IsHitというBoolean型のアウトプットを追加します。
このように、ブループリントインターフェースの関数にアウトプットパラメータを設定することで、呼び出し側(つまり、今回で言えば弾丸側)に何等かの情報を返すことができるようになります。
青い弾丸が「OnHitBullreBlue」をOtherActorに渡すと、OtherActorにBPI_HitBulletのインターフェースが実装されている場合に、OnHitBulletBlueの実装が実行され、弾丸が当たって消滅したか否かをアウトプットパラメータを使って弾丸側に返却することで、弾丸は自身をDestroyActorすることができるようになるのです。

ということで、最終的には、このような感じとなります。

青い弾丸(BP_BulletBlue)の衝突処理
Sample27.png
青敵に対して、インターフェース経由で青い弾丸が当たったことを受け取った時の処理
Sample28.png
青敵に対して、インターフェース経由で赤い弾丸が当たったことを受け取った時の処理
Sample29.png
これで、弾丸と敵は、参照だけでなく、関連性も疎結合となります。

もちろん、この状態で参照ビューワを見ても余計な参照は存在しません。
この仕組みなら、弾丸の種類が増えても、新しい弾丸に対する敵の挙動を書けば良いだけだし、敵が増えても、弾丸側にはなんの変更も必要ありません。
さらに、このゲームから弾丸が消えても、敵が消えても、それぞれの依存関係がインターフェースだけの為、単純にインターフェースが利用されなくなるだけで、お互いのオブジェクトに何ら影響がない点にも注目してください。

ブループリントインターフェースを使うことで、敵の実装は「何色の弾丸」が当たる可能性があるのかを容易に把握することができ、更に弾丸が当たった時の処理を完結に記述することができました。

ブループリントインターフェースを使用することで、オブジェクト間の関係が疎結合になっていることが分かるかと思います。

#ブループリントの親クラスにCastする方法(解決方法その2)
これはどちらかというと、BPのCastはハード参照を作ってしまうという機能と、オブジェクト指向の「継承」「オーバーライド」をうまく組み合わせて、無駄な参照を回避している方法となります。

これは、とても簡単なサンプルで説明することができます。

プレイヤー(BP_Player)が衝突することで色が変わるアクター(BP_Collision)があるとします。
プレイヤー側でコリジョン判定をし、OtherActorがBP_Collisionにキャストできる場合、BP_CollisionのColorChangeイベントを呼ぶことで、BP_Collisionの色が変化するようになっています。

プレイヤー側のブループリントはこんな感じです。
Sample30.png
ただし、これには罠がありまして、BP_Collisionは必要かどうかも分からない参照が沢山紐づいているのです。(これはサンプルの為、手あたり次第参照を追加しています)
Sample31.png
ということは・・・プレイヤーの参照ビューワを見ると・・・
Sample33.png
予想通り、無駄な参照がいっぱいです。しかも、BP_Collisionが基点となっているのが原因です・・・

プレイヤーが衝突したらアクターの色を変えたいので、衝突処理の中でBP_CollisionにCastしたいけど、この参照は引き継ぎたくない!!

こんな場合の対策として、BP_Collisionの親BPクラスを作成し、その親クラスは何も参照していないけど、ColorChangeイベントだけを持っている状態を作りだすことで、参照の影響を受けずにColorChangeイベントを呼び出すことができます。

まずは、BP_Collisionの親BPクラスを作ります。
新規BPクラス(今のBP_Collisionの親クラスから新規に作成してください)としてBP_CollisionParentを作成し、その中にはColorChangeというカスタムイベントのみを定義しておきます。
Sample34.png
次に、BP_Collisionに今作成したBP_CollisionParentを親クラスとして設定します。

BP_Collisionを開き、画面上部のクラス設定をクリックし、詳細パネルの「クラスオプション」内の「親クラス」でBP_CollisionParentを選択してください。
Sample35.png
そして、BP_Collision内で定義していたカスタムイベント「ColorChange」を一旦削除し、"イベントを呼び出し"に分類分けされた中のColorChangeと置き換えてください。
Sample38.png
これは、親クラスで定義されたColorChangeをオーバーライド(機能の上書き)するという意味になります。(親クラス側ではColorChangeの処理は空でしたよね)

最後にプレイヤー側の衝突判定でCastしていたところを、BP_ColliionへCastするのではなく、BP_CollisionParentへCastするように修正します。
Sample36.png
これで修正が完了です。
これで、キャラクターがBP_Collisionに当たると、BP_CollisionのColorChangeを呼び出すことができます。

最後にプレイヤーの参照ビューワを見てみると・・・
Sample37.png
余計な参照が無くなっていることがわかります。

ただし、上記のやり方は、ハード参照を回避する為に親クラスを作成し使うという、本来の設計・製造とは逆のアプローチになってしまっています。
親クラスを設計してから子クラスを実装するのが正しい設計の手順なので、そのあたりは間違えないようにしてください。

#結局のところ、UE4のCastや参照とはどう付き合うべきか?
個人的な見解ですが、UE4のハード参照の挙動については、プログラマー以外でもBPを使って簡単に実装できるという機能とのトレードオフなのかな?と思っています。
デザイナーが思いついたものを作っていくときと、プログラマーがメモリやリソースを考えながら実装していく時の実装方法は大きく異なることを考えながら作成していく必要があるのではないかと思いました。

つまり、ある程度の規模のコンテンツを作る前には、事前の設計が重要になってくると思います。

#まとめ
「Castはお勧めしない」という言葉の中に、これだけの意味が含まれていたかと思うと驚きました。

しかも、Castの問題から、そこからハード参照の話しになって、更に参照使うくらいならオブジェクト指向的な設計を検討しましょうというところまで話しが広がっていくので、なかなか理解しにくい部分かと思います。

個人的には、Castもハード参照も悪くはないけど、使い方を間違えると大問題になるんだな・・・と感じました。
これは脱初心者には必要な知識と判断し、サンプルを作りながら検証してみたのは良い経験になったと思います。

今回は、UE4初心者の私が、疑問を抱き、疑問を解決していくまでの過程と、その検証内容をそのまま記事にさせて頂きました。職業プログラマーが本気で2日間調査した結果となりますが、UE4関連については知らないことが沢山ありますので、間違った説明をしている可能性があります。
その場合はお気軽にご指摘等頂ければと思います。

#参考にさせて頂いたサイト
UnrealEngineの情報は公式からの情報が多いので大変助かります。
また、下記リンクにある「アンリアルクエスト-グレイマンからの5つの挑戦状-」特別生放送!【前編】の3時間34分くらいのところで、私の質問を取り上げて頂きありがとうございました。この時はUE4でのCastの知識が無さすぎて頓珍漢な質問をしてしまっています。(まさに、この記事の最初の状態でしたから・・・)
ここで回答を頂けたことが、この記事を作成する力となりました。

#最後に・・・
UE5のアーリーアクセスが開始されたので、私もUE5をインストールしてみたのですが、うちの内蔵GPUマシンでは動作がつらかったので、UE5ネタではなくCast・参照ネタを記事にさせて頂きました。UE5で楽しんでいる皆さんがうらやましい・・・早くグラフィックボードを買わねば!!

25
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?