🔷はじめに
UnrealEngine5でオトナ向けのゲーム開発をしているNishiです🔞
"オブジェクト指向"をUE5に適用した場合、どのような実装になるのか考えてみました。
この記事では、オブジェクト指向の3原則の中でも、"カプセル化"の部分にフォーカスして、考えたことをざっくり公開します。
🔷対象者
この記事は下記のような人を対象にしています。
- 駆け出しUnrealEngineエンジニア
- プログラミング初学者
筆者は記事執筆時点で、UE5歴2年 ≒ プログラマー歴ですので、その点も踏まえてご覧いただけるとmm
🔷要約
今回は、オブジェクト指向のカプセル化をUEで実践する方法を考えました。
カプセル化は、機能ごとの処理を分けて、コンポーネントを自作することで実現できます。
その際、機能の呼び出し元アクターがコンポーネントに依存しないように設計し、インターフェースで機能を呼び出すようにしました。
🔷本編
🔸そもそもカプセル化とは?
オブジェクト指向の3原則は、
- 継承
- 多態性
- カプセル化
の三つです。
そのうち、継承と多態性はUnrealEngineを使っていれば自然と使いまくっています。
"継承"は、「Actorクラスを親クラスにして子クラスを作る」などで実践済み。
"多態性"は関数のオーバーライドなどで自然と実践しています。
(^._.^) ニャン
そしてカプセル化はザックリ言うと、
「ある機能を別のクラスに分離して、中身はそっちで実装しようゼ。機能を使う側はその機能の中身がどうなってるのかを知る必要はないぜ!」
...みたいなことです。
いわゆるコンポーネント指向ってやつですね。
...正しく詳しい説明はネットで調べるなりしてもらえると...!
🔸実装のルール
では実際にコンポーネント指向をUE5で実践するにあたって、要件を整理しました。
カプセル化に必要な要件
- コンポーネントを所有するアクターをコンポーネントに依存させない(疎結合にする)
- 簡単に取り外しできるようにする
- コンポーネントに実装した機能はインターフェースを使って呼び出す
開発での使い勝手を良くするための条件
- コンポーネントの形式に依らず、アクターからの機能の呼び出し方が同じである(人為的ミスを減らす)
- 複数人での開発を想定して、機能ごとにアセットが分かれているようにする
コンポーネントに実装する機能のルール
- ひとつのコンポーネントにはシンプルな機能をひとつ
- 引数で状態を受け取り、状態変更せず、何かをアウトプットするだけの機能にする
ex.) 呼び出し元の変数をコンポーネントが変更するような機能は避ける。どこでその変数が変更されるのか訳が分からなくなるから。
その他考えたこと
- エディタ画面上でプレビューしやすいか
- 単体テストがしやすいか
- アセットの数が増えすぎないか
🔸最終的な実装
機能ごとにBlueprintInterfaceとコンポーネントを作成し、呼び出し元は、インターフェースを介して機能を呼び出せるようにしました。
機能ごとに新しく作成するもの
- 呼び出す機能を実装するBlueprint Interface
- その機能を有する、以下のいづれかのクラス
- Actor Component
- Scene Component
- 特定の既存のコンポーネントクラスの子クラス
- Actor (Child Actorとして女の子キャラに実装)
1. BlueprintInterface
ひとつの機能に、ひとつのBlueprintInterfaceを作成する。
呼び出したい機能の関数をひとつだけAddする。
2. コンポーネント
クラスの選定
どういったクラスでコンポーネントを作成するか、機能に合わせて決める。
以下の採用優先度の高いものから選ぶほうがよりシンプル。
採用優先度 | クラス | 用途 | 例 |
---|---|---|---|
高 | Actor Component | それ自身が座標を持たない。特定のクラスを継承する必要がない。 | キャラの表情を変える |
中 | Scene Component | 座標情報が必要。女の子キャラが持つCompにアタッチする。 | 距離減衰するSoundを手から発する |
中 | 特定の既存のコンポーネントクラスの子クラス | 特定のコンポーネントクラスを継承する。 | 女の子に帽子をかぶせる(StaticMeshComponentの子クラス) |
低 | Actor(ChildActor) | 既存のコンポーネントの継承だけでは賄えない場合 | ⓵ 複数Compを組み合わせたもの ⓶コンポーネントではないもの(NiagaraSystemなど) |
コンポーネントの実装
選んだクラスに機能を実装する
- ひとつのコンポーネントにはシンプルな機能をひとつ
- 引数で状態を受け取り、状態変更せず、何かをアウトプットするだけの機能にする
→ 引数以外の値を呼び出し元から取得しない
コンポーネントにインターフェースで呼び出せる機能を実装する。
女の子キャラへの実装
使う機能のコンポーネントを配置する。
(BPで動的にアタッチするとかでももちろんOK)
GetAllComponentsByInterfaceという自作の関数でインターフェースクラスを指定して、Getしたコンポーネントに対してインターフェースで機能を呼び出す。
このノードを作っておけば、どの形式のコンポーネントに対しても、インターフェースで機能の呼び出しができる。
🔷実践してみて
🔸気に入っている点
元々考えていた、カプセル化のルール的なものを満たせていることに満足しています。
① 女の子キャラがコンポーネントに依存しない
カプセル化の基本となる疎結合な実装が実現できている!
「こういう機能をこういう引数・戻り値で作る」って最初に決めて、BlueprintInterfaceだけ用意しておけば、コンポーネントの作成自体は分業で進めることも可能。
② 簡単に取り外しできる
女の子キャラがコンポーネントを持っていなくてもBlueprintはコンパイルエラーを起こさない。
コンポーネントのアタッチデタッチだけで機能のつけ外しも可能。
③ コンポーネントの形式に依らず、キャラクターからの機能の呼び出し方が同じである
機能の開発担当者が、コンポーネントをいづれの形式で実装したのかを、機能の利用者が知らなくても良い!
脳死で、事前に用意したインターフェースで機能を呼び出せば、あとからコンポーネントはがっちゃんこできる。
単独開発でも全然ありがたい。過去の自分がどう実装したかなんてすぐ忘れるからです(笑)。
④ 機能ごとにアセットが分かれているようにする
機能ごとに開発者を分けやすい。単独開発だとしても、機能の再利用性が高い点は良い。
🔞ゲームだと、女の子の機能の共通項はめっちゃ多いので、特に活躍しそう。
⑤ エディタ画面上でプレビューしやすい
今回、ChildActorコンポーネントでも共通の方法で機能を呼び出せるようにしたことで、プレビューしづらいコンポーネントの作り方は回避しやすくなったんじゃないかと思います。
⑥単体テストがしやすい
ちゃんとカプセル化ができていれば、単体テストはもちろんやりやすい!
🔸気になっている点
① アセットの数が多くなる
機能ごとにインターフェースとコンポーネントを作っているとアセットの数が膨大になりそう。
これは仕方ないんですかねぇ?
インターフェースは事前に作成する想定なので、機能をなんとなくジャンル分けして、ジャンルごとにまとめて一つのインターフェースに関数を実装してもいいかもな~、とも思った。
でも、そのジャンル分けの仕方とかでまた悩むのも阿保らしいから、アセットが増えちゃうのは容認かなぁ。
② 継承との使い分けは?
コンポーネント指向で作った女の子キャラの親クラス的なものを作り、それを継承して個性のある女の子を作る...っていう使い分けが良いかな、と思っています!
🔷おわりに
カプセル化をUnrealEngineで実践するにはどうしたら?ということで色々考えてみたことを書かせてもらいました。
自分は大規模ゲーム開発の現場にいたことがなく完全に手探り、かつC++も使ってないので最適化は不十分な方法ではあると思うのですが、自分なりに納得のいくコンポーネント指向の実践方法になったのではないかと思っています。
ご意見やアドバイス等あれば、ぜひコメントやXのポストなどお待ちしております...!
読んでいただきありがとうございました!
.
.
.
🔷【おまけ】アドバイス・コメントを受けて
記事の公開後に、読んでくださったUEツヨイな方にコメントをいただいたので、補足情報を書かせてもらいます!
🔸Pure関数×配列Getの罠
実はもともと、GetAllComponentsByInterfaceは、Pure関数で実装していました。
記事公開後に「Pure関数だと配列の要素のたびに関数の中身が実行されてしまう」というご指摘を受けて非pure関数に変更しました。
(参考:恒吉星光さんのXのポスト) < アリガトウゴザイマス!
(^._.^) ワン
Pure関数での配列Getの話は、アンチパターンとして以下の記事でも紹介されているので、ぜひ読んでみてください。
🔸"カプセル化"じゃなくて"多態性"の話であった説?
今回は、オブジェクト指向の"カプセル化"の話として記事を進めましたが、実は"多態性"の話としても捉えられるのではないか?というご意見をいただきました。
"多態性"とは何なのか。
「多態性(Polymorphism)」とは、オブジェクト指向における「実行される処理の実体が、コールされたメッセージではなく、メッセージを受けたオブジェクトによって決定される性質」のことです。また、この性質を使って「同一のメッセージを使って、オブジェクトごとに異なった処理を行わせること」を指して、「多態性」という言葉が使われる場合もあります。
(参照:CodeZine 「Cでわかるオブジェクト指向【第4回】多態性」)
確かに、呼び出しに対して実際に処理される内容がコンポーネント側で決まるという点で、"多態性"の話でもあるとも考えられるんですかね...難しい。
オブジェクト指向の時点で近しい話なんでしょうか。
(^._.^) ワャン
一番イメージしやすい"多態性"の例は、
「WaterオブジェクトがOverlapしたオブジェクトに対して"SplashWater"というインターフェースの関数を呼び出すとき、Fireオブジェクトは自身をDistroyするけど、Flowerオブジェクトは喜ぶ。」
とかだと思います。
オブジェクト指向...まだまだ勉強しがいがありそうです!
🔷参考
.
.
.
.
.
🔷告知
現在、女の子のキャラクターとボイスチャットで会話することのできる🔞ゲームを開発中です。
DLsiteにて11月中旬発売予定ですので、興味があれば手に取っていただけるとうれしいです!
Xでは開発の様子もポストしてますんで、よければフォローお願いしマッス!
では!