UE5でBlueprintを作っていると、
「気づいたらCast Toだらけになっている……」
ということがあります。
PlayerにCastして、EnemyにCastして、WeaponにCastして、GameModeにCastして……。
Castについて調べてみると、
- Castはなるべく使わない方がいい
- Blueprint Interfaceを使った方がいい
- Event Dispatcherを使った方がいい
- なるべく疎結合にした方がいい
といった情報もよく見かけます。
では、Cast Toはできるだけ使わない方がいいのでしょうか?
結論から言うと、Cast自体が悪いわけではありません。
むしろCastが大量に必要になっているなら、
「このCastを何に置き換えるか?」ではなく、「なぜこの具体的な型を知る必要があるのか?」
から考えた方がいい場合があります。
この記事では、Castが増えすぎたBlueprintを題材に、
- Castはなぜ悪いと言われるのか
- CastをInterfaceに置き換えればいいのか
- Castが適しているのはどんなケースか
- 親クラスや継承をどう考えるか
- Parameter / Interface / Component / Event Dispatcherとの使い分け
- Castが増えたときに確認したい判断フロー
を整理します。
Cast Toは本当に悪なのか?
まず押さえておきたいのが、
Castそのものが主な問題とは限らない
ということです。
例えば、Playerが次のような具体的なBlueprintを大量に参照しているとします。

この状態では、BP_Playerが大量の具体的なBlueprintを知っています。
問題になりやすいのは、単純に「Cast Toノードが存在すること」だけではありません。
具体的なBlueprintへの参照が増えることで、Blueprint同士の依存関係まで広がっていくことがあります。
具体的なBlueprintへの参照が増える
Blueprintから具体的なBlueprintをハード参照すると、その参照先がさらに別のアセットを参照している場合、依存関係が連鎖していくことがあります。
その結果、
- メモリ使用量
- ロード時間
- Blueprint同士の依存関係
などに影響する可能性があります。
そのため、
Cast Toは重いから使ってはいけない
と単純に考えるより、
具体的なBlueprintへの参照が必要以上に広がっていないか?
を見ることが重要です。
Castが多いと変更箇所も増えやすい
もう一つ問題になりやすいのが、変更の影響範囲です。
例えばProjectileが、当たった銃の種類によって処理を変えるために、次のような実装を持っていたとします。

この時点でBP_Projectileは、
- BP_AssaultRifle
- BP_SniperRifle
- BP_Shotgun
という3種類の具体的なクラスを知っています。
では、新しくLaser Rifleを追加したらどうなるでしょうか。

Projectile側にもLaser Rifle用の処理を追加しなければならないかもしれません。
銃を一種類追加しただけなのに、Projectileまで変更する必要がある。
こうした依存関係がプロジェクト全体に増えていくと、一つの変更によって複数のBlueprintを修正しなければならなくなります。
Castが増えてきたときは、
具体クラス同士の依存関係まで増えていないか?
を確認してみるといいと思います。
Castを禁止すればいい?
では、Castを使わなければいいのでしょうか?
個人的には、それも少し乱暴だと思っています。
Castを減らす方法として、
- Blueprint Interface
- Event Dispatcher
- Component
などが紹介されることがあります。
もちろん、どれも便利な機能です。
しかし、
CastをInterfaceやEvent Dispatcherに置き換えれば、必ず良い設計になるわけではありません。
Interfaceに置き換えれば解決する?
例えば、複数の銃に共通して、
- OnFire()
- OnReload()
という処理が必要だとします。
Blueprint Interfaceを使えば、次のような形にできます。

呼び出し側は、
「Assault Rifleなのか?」
「Sniper Rifleなのか?」
と具体的な種類を意識する必要がありません。
「Fireできるオブジェクトなのか?」
だけを知ればよくなります。
これはInterfaceが非常に活きるケースです。
一方で、Interfaceそのものが共通実装を持ってくれるわけではありません。
例えば3種類の銃でOnFire()の中身がほとんど同じなのに、それぞれ同じような実装を書いているなら、
依存関係は減ったけれど、実装が複数のBlueprintへ分散した
という状態になる可能性があります。
もちろん、それぞれの銃でOnFire()の処理が本当に異なるのであれば問題ありません。
重要なのは、
Castを消すことだけを目的にInterfaceへ置き換えない
ということです。
Event Dispatcherに置き換えれば解決する?
Event Dispatcherは、
「何かが起きたことを外部へ知らせる」
ときに便利です。
例えばProjectileが何かにHitしたとします。

発行側は、誰がそのイベントを受け取るのかを直接知る必要がありません。
そのため、
「死亡した」
「HPが変化した」
「アイテムを取得した」
といった通知に向いています。
一方で、何でもEvent Dispatcherにすると、
- このイベントはどこから発行された?
- どこでBindされている?
- 誰が受信している?
- 最終的にどこで処理されている?
と、処理の流れを追うことが難しくなる場合があります。
つまり、
疎結合にすればするほど、必ず分かりやすくなるわけでもありません。
そもそも「違い」をどう表現するのか?
ここで一度、Castから離れてクラスそのものを考えてみます。
Blueprintも基本的にはオブジェクト指向です。
例えば、次のようなBP_BaseCharacterがあるとします。

そして、このクラスを継承してPlayerとEnemyを作ります。

PlayerとEnemyは異なるクラスです。
しかし、どちらも、
Characterとして共通するデータや振る舞い
を持っています。
ここで重要なのが、
「違いがある」=「別クラスにする」ではない
ということです。
何かに違いがあったとき、その違いが、
- 種類なのか
- 値なのか
- 能力なのか
- 機能なのか
によって、表現方法を変えます。
例えば、
種類の違い → 親クラス・継承
値の違い → Parameter / Data
能力 → Interface
機能 → Component
出来事 → Event Dispatcher
といった形です。
これを整理すると、Castが必要な場所も整理しやすくなります。
Castが活きるケース
Castは使わない方がいい機能ではありません。
具体的な型として扱う必要があるなら、Castを使うのは自然です。
具体的なクラスを知る必要がある
例えばBossGateにPlayerが入ったときだけ、Player固有の処理を呼びたいとします。

この場合、
OverlapしたActorがPlayerなのかを知ること自体に意味があります。
そのため、BP_PlayerへCastしてPlayer固有の処理を呼ぶのは自然です。
無理にInterfaceを作る必要はありません。
親クラスとして共通処理を使う
PlayerとEnemyがどちらもBP_BaseCharacterを継承している場合を考えてみます。

Projectile側が必要としているのが、
「Playerなのか?」
「Enemyなのか?」
ではなく、
「CharacterとしてTakeHit()を呼びたい」
だけなら、具体的なPlayerやEnemyまで知る必要はありません。
BP_Projectile
│
└ OnHit()
│
└ Cast To BP_BaseCharacter
│
└ TakeHit()
これなら実体がPlayerでもEnemyでも、
Player / Enemyを意識せず、BaseCharacterとして共通のTakeHit()を呼び出せます。
具体クラスごとにCastする必要はありません。
「でも継承って良くないんじゃない?」
ここまで読んで、
「継承はなるべく使わない方がいいのでは?」
と思った人もいるかもしれません。
確かに、
- 継承よりComposition
- 継承は密結合になる
といった話はあります。
そして、深すぎる継承階層がメンテナンスを難しくすることもあります。
例えば、
Enemy
↓
HumanoidEnemy
↓
MeleeEnemy
↓
SwordEnemy
↓
FireSwordEnemy
という構造です。
階層が深くなるほど、
- 上位クラスの変更がどこまで影響するのか
- どのクラスに処理を書くべきなのか
- 実際の挙動がどこで決まっているのか
が分かりにくくなりやすくなります。
個人的には継承を基本1段までにしている
筆者の場合、継承は基本的に1段までというルールで作ることが多いです。
例えば、
BaseCharacter
├ Player
└ Enemy
程度です。
これは「継承は絶対に1段までにするべき」という一般的なルールではありません。
あくまで個人的な判断基準です。
2段目が欲しくなった時点で、
そもそも、このクラス分類は正しいのか?
を一度考えるようにしています。
例えば、
Enemy
↓
Melee
↓
Sword
↓
Fire
という分類を考えてみます。
しかし、よく見ると、
- Enemy → キャラクターの分類
- Melee → 戦闘方式
- Sword → 装備
- Fire → 属性
です。
それぞれ別の分類軸です。
これらを一本の継承ツリーに押し込めば、規模が大きくなったときに無理が出てくる可能性があります。
継承そのものを避けるのではなく、
本当に「is-a」の関係なのか?
を考えることが重要です。
Castを使わない方がいいケース
では、Castが増えてきたときに検討したい代表的なケースを見ていきます。
1. 値が違うだけならParameter / Data
例えば、

という3種類のEnemyを作ったとします。
しかし違いが、
パラメータ値だけなら、
本当に別々のBlueprintが必要でしょうか?
例えば一つのBP_Enemyを作り、

として、スポーン時などに値を変更するだけで表現できるかもしれません。
つまり、
値が違うだけなら、まずParameterやDataで表現できないか考える
ということです。
具体クラスそのものを増やさなければ、それらを判別するためのCastも必要なくなります。
2. 能力だけ必要ならInterface
次は、
「何者なのか」ではなく「何ができるのか」だけ知りたい場合
です。
例えば、

には共通の親クラスがないとします。
しかし、すべてがDamageを受けられる。
呼び出し側が必要としているのは、
「Playerなのか?」
「Enemyなのか?」
ではありません。
「Damageを受けられるのか?」
だけです。
この場合、
BPI_Damageable
└ TakeDamage()
というInterfaceを作り、それぞれに実装できます。

このように、
具体的な種類ではなく、共通の能力だけを要求したい
場合はInterfaceが適しています。
3. 共通の機能ならComponent
複数のActorに共通の機能を持たせたい場合は、Componentが使えます。
例えば、
- HP管理
- Inventory管理
- Status管理
などです。
HealthComponent
InventoryComponent
StatusComponent
として切り出せば、複数のActorで再利用できます。

ただし、
何でもComponentへ移せばいいわけではありません。
例えばHPそのものの管理はHealthComponentへ任せたとしても、
Damage
↓
HealthComponentでHP更新
↓
死亡した?
├ YES → Death()
└ NO → HitReaction()
というCharacterとしての一連の流れは、BaseCharacter側に置いた方が分かりやすい場合もあります。
ここでは、
機能を再利用すること
と、
処理の流れを共通化すること
を分けて考えるのがポイントです。
4. 出来事を知らせたいならEvent Dispatcher
例えばEnemyが死亡したとします。
Enemyから、
Cast To GameMode
Cast To AIController
Cast To ResultHUD
として、それぞれの処理を直接呼び出すこともできます。
しかし、これではEnemy自身が、
「自分が死んだ後に誰が何をするのか」
まで知ることになります。
Enemy側がやりたいことが、
「自分が死んだ」
と外部へ知らせるだけなら、Event Dispatcherを検討できます。

発行側であるEnemyは、受信者を直接知る必要がありません。
そのため、
何かが起きたことを外部へ通知したい
というケースではEvent Dispatcherが適しています。
Cast / 親クラス / Interface / Component / Event Dispatcherの違い
ここまでをまとめます。
| 機能 | 何を考える? | 主な用途 |
|---|---|---|
| Cast | この型として扱える? | 具体型・親型として扱う |
| 親クラス | 何者なのか? | 共通データ・共通実装 |
| Parameter / Data | 値がどう違う? | 同じ仕組みの中で差を作る |
| Interface | 何ができる? | 共通の能力・契約 |
| Component | 何を持っている? | 再利用可能な機能 |
| Event Dispatcher | 何が起きた? | 外部へのイベント通知 |
初心者向けにかなりざっくり言えば、
Cast = 型として扱う
親クラス = 共通の種類
Parameter / Data = 値の違い
Interface = できること
Component = 持っている機能
Event Dispatcher = 起きたことを知らせる
と考えると整理しやすいと思います。
これは絶対的なルールではなく、設計を考えるための判断軸です。
実際のゲームでは、これらを組み合わせて使うことになります。
Castが増えてきたときの確認フロー
BlueprintでCastが増えてきたら、次の順番で考えてみてください。
| 順番 | 機能 | 確認ポイント |
|---|---|---|
| 1 | Parameter / Data | 値が違うだけ? |
| 2 | 親クラス | 同じ種類? |
| 3 | Interface | 能力だけ必要? |
| 4 | Component | 機能を持たせたい? |
| 5 | Event Dispatcher | 知らせたいだけ? |
| 6 | Cast | 具体型を本当に知る必要がある? |
1. 値が違うだけ?
YESなら、
Parameter / Data
で表現できないか考える。
2. 同じ種類として扱える?
YESなら、
親クラス
として共通化できないか考える。
3. 共通の能力だけ必要?
YESなら、
Interface
を検討する。
4. 共通の機能を持たせたい?
YESなら、
Component
を検討する。
5. 出来事を知らせたいだけ?
YESなら、
Event Dispatcher
を検討する。
6. 具体的な型を本当に知る必要がある?
YESなら、
Castを使えばOKです。
疎結合にすればいいわけでも、共通化すればいいわけでもない
Castを減らす話をすると、
「なるべく疎結合にした方がいい」
という結論になりがちです。
もちろん、依存関係を必要以上に増やさないことは重要です。
一方で、疎結合にするためにInterfaceやEvent Dispatcherを大量に使えば、
- 実装が分散する
- 通信経路を追いにくくなる
- 同じ処理を書く場所が増える
といった問題が起こる可能性もあります。
逆に親クラスへ共通処理をまとめれば、
- 共通処理を一か所で管理できる
- 変更箇所を減らせる
- 共通した振る舞いを保証しやすい
というメリットがあります。
その一方で、深い継承階層を作れば、今度は親子関係が複雑になります。
つまり、
疎結合にすればするほど良いわけでも、共通化すればするほど良いわけでもありません。
大切なのは、
変更しやすく、後から見ても理解しやすい場所に依存関係を作ること
だと思います。
Castを減らすためにCastを消す必要はない
今回は、UE5のBlueprintでCast Toが増えてきたときの考え方を整理しました。
ポイントをまとめると、
- Cast自体が悪いわけではない
- 具体Blueprintへの参照や依存関係が増えていないかを見る
- CastをInterfaceへ機械的に置き換えればいいわけではない
- 具体型を知る必要があるならCastを使っていい
- 同じ種類なら親クラスとして扱えないか考える
- 値の違いならParameter / Dataで表現できないか考える
- 共通の能力ならInterfaceを検討する
- 共通の機能ならComponentを検討する
- 出来事の通知ならEvent Dispatcherを検討する
- 継承を使うなら、異なる分類軸を一つの階層に押し込んでいないか確認する
Castが増えてきたとき、
「このCastを何に置き換えればいい?」
と考える前に、
「そもそも、なぜこの具体的な型を知る必要がある?」
と考えてみてください。
それでも具体的な型を知る必要があるなら、Castを使えばいいです。
Castを減らすために、Castを消す必要はありません。
Blueprintが複雑になってきたときの設計整理に、今回の考え方を使ってみてください。
動画でも解説しています
この記事の内容は、YouTubeでも実際のBlueprint例や図を使って解説しています。
Cast、Interface、Component、Event Dispatcherなどの違いを図で確認したい方は、動画もあわせてどうぞ。
コメント