ハード参照とソフト参照

勉強会とかでちらほらと、アセットのハード参照、ソフト参照って言葉が出てきますよね。
自分もぼや~っとは認識していたのですが、正確に違いや使い方を理解していなかったのでtwitterでポロっとつぶやいたところ、カニパンチさんとまめおさんが超絶丁寧に教えてくれました。多謝!
せっかくなので自分の備忘録と、これから同じ悩みを抱えるであろう方のため資料として、ブログを残しておくことにしました。
今回はキャラクターの武器付け替えを実装する際に発生するハード参照の問題を例にとり、ハード参照とソフト参照の違いについて説明していきたいと思います。

ハード参照

ハード参照による実装

まず武器の付け替えシステムですが、何も考えずに非常に簡単に組むとこうなります。
武器用にStaticMeshComponentを追加して
f:id:nca03132:20201030181854p:plain
1,2で武器メッシュを切り替えるようにする
f:id:nca03132:20201030182243p:plain
キャラクターの参照ビューアを確認すると、BPで参照している武器メッシュ(S_Sword,S_Katana)に対して白い線が結ばれているのがわかります。この白い線がアセットへのハード参照を表しています。
f:id:nca03132:20201030183347p:plain
この状態で1,2キーを押すと、きちんとキャラクターの武器メッシュが切り替わるのが確認できます。

ハード参照の問題点

何の問題もなく動いているように見えますが、この状態にはちょっとした落とし穴があります。
数字キーを押すことにより武器メッシュが切り替わるので、使用していない武器メッシュはアンロードされていそうに見えますが、ところがどっこいすべてオブジェクトとしてメモリにのった状態となっています。
これはUE4の仕様で、アセット読み込み時にはそのアセットがハード参照を持つアセットもすべて読み込むようになっているためです。
武器メッシュはさらにマテリアルやテクスチャのハード参照を持っているので、それらも全武器の分だけ丸っと全部読まれます。
付け替え武器が2~3種類なら全然許容範囲かもしれませんが、100とか200になってくると、キャラ生成時に全武器がメモリに乗ることになるので非常によろしくない状態になるのは容易に想像できると思います。

武器メッシュを要素に持つデータテーブルや配列を用意しても回避できない

いやいや100とか200の武器メッシュの切り替えをBPでべた書きするわけないでっしゃろ。
武器メッシュの参照を要素にもつデータテーブルを用意し、そこから読み込む武器メッシュを取り出してセットすればええやんけ……と思う方もいらっしゃると思います。
残念ながらこれもダメです。
試しに先ほどのS_Sword,S_Katanaを保持するデータテーブルを作ってみると
f:id:nca03132:20201030184500p:plain
データテーブルは要素にある全武器メッシュのハード参照を持つことになります。
f:id:nca03132:20201030184558p:plain
キャラクターはデータテーブルをメンバ変数に持つとデータテーブルのハード参照を持つようになります。
f:id:nca03132:20201030185013p:plain
従って、キャラクターはデータテーブルを介して全武器メッシュへのハード参照を持つことになり、やっぱりキャラクターロード時に全武器メッシュが読み込まれてしまうのです。
キャラクターにデータベースの代わりに全武器メッシュの配列やマップ変数を持たせても同じ結果になります。
とにかく!利用法はどうであれ参照ビューアで白い線(ハード参照)を持ったアセットは、全部芋ずる式にメモリに読み込まれてしまうことに注意してください。

ソフト参照

ソフト参照による実装

そこでソフト参照の登場となります。
ソフト参照はハード参照と同じように使用アセットを設定しておくことが出来ますが、親アセット生成時に自動的にロードはされません。
逆に言えば必要になった時には明示的にロードを行う必要があります。
まずは武器メッシュ用のソフト参照をキャラクターの変数として作成します。
キャラクターに新規変数を作成し、変数の型にStaticMeshのソフトオブジェクト参照変数を指定します。
f:id:nca03132:20201030185907p:plain
上記手順でSoftRef_Sword,SoftRef_Katanaという2つのソフトオブジェクト参照変数を作り一度コンパイルします。
f:id:nca03132:20201030190718p:plain
それぞれのソフトオブジェクト参照変数のデフォルト値にS_Sword、S_Katanaを設定しておきます。(※ここ忘れないように)
この状態で参照ビューアを確認すると武器メッシュへの線はピンクに変わっていることが確認できます。これがソフト参照です。
f:id:nca03132:20201030190822p:plain
あとは武器使用時にロードしてSetStaticMeshの引数に渡すようにします。
ソフト参照アセットのロードは同期、非同期の2通りの方法で行うことが出来きます。

同期読み込み

f:id:nca03132:20201030191348p:plain

非同期読み込み

f:id:nca03132:20201030191645p:plain

ソフト参照の問題点

なにより「使う前にはかならずロードする必要がある」という点でしょう。
ハード参照の説明の時に問題点として挙げた「武器メッシュを要素に持つデータテーブルや配列を用意しても回避できない」については、データテーブルや配列を武器メッシュへのソフト参照に変更し、使用前にロード処理を入れれば問題なくソフト参照で運用できます。
なお、ロードを書き忘れて以下のようにしてしまった場合でもエディタ上では動くことがあります。
f:id:nca03132:20201030193115p:plain
が、これはエディタが内部的にアセットを事前ロードしているためたまたま動いているだけであり、パッケージ化にしたときに動かずハマることになります。
アセットのロードが完了していることを保証できるならこれで問題ありませんが、そうでなければロード処理は必ず書きましょう。

意図せずハード参照を引き起こすケース

せっかく気を付けてソフト参照で書くようにしても、ふとしたことで意図せずハード参照を発生させてしまうことがあります。
以下に気を付けるべきケースをいくつが紹介します。

型の直接指定があるノード

冒頭で紹介したSetStaticMeshノードもそうなのですが、ノードの中にはダイレクトにアセットやクラスを指定できるものが存在します。
以下にいくつか例を挙げておきます。
f:id:nca03132:20201031083958p:plain
こういったものに直接アセットやクラス指定を入れるとハード参照が発生します。
クラス指定についてはブループリントではハード参照を避けて通れない(ソフト参照変数を用意してもハード参照になってしまう)ので設計を工夫するしかないのですが、データアセットについてはあとから付け替える可能性があるならソフト参照の導入を検討しましょう。
(※)C++を使えばClassを動的取得するノードを作ってクラス指定子につなぐなどで回避可能という情報をいただきました(カニパンチさんありがとうございます)

CastTo

CastToもハード参照を発生させてしまいます。
例えば敵に攻撃がヒットしたときに、敵の種類に応じてダメージを与える計算が変わる場合について考えてみましょう。
f:id:nca03132:20201030192017p:plain
アホの子みたいなBPですがサンプルとして勘弁していただくとして(汗)、ここではCastToがEnemy1,Enemy2,BossCharacterと3つ使われています。
純粋に処理を分けたいだけなのですが、実はこれだけでこのキャラクターはEnemy1、Enemy2、BossCharacterへのハード参照を持ってしまいます。
f:id:nca03132:20201030192403p:plain
ダンジョンのフロアごとにレベル分けして、1FにはEnemy1、2FにはEnemy2、3FにはBossCharacterしか登場しないとしても、プレイヤーがこのブループリントを使っている時点ですべてのフロアでEnemy1、Enemy2、BossCharacterのオブジェクトが生成されてしまいます。もちろんそれらに使われているリソースもロードされます。
さらにもし万が一、Enemy1がまた別のキャラクターへのハード参照で持っていると、それも数珠つなぎにロードされて・・と、この辺はかなり気を使わないと事故を引き起こす可能性があるので要注意です。
CastToは使った瞬間、問答無用にCast先クラスへのハード参照を作ってしまうのでインターフェースを切る・ディスパッチャを使う・不要なダウンキャストを避けるといったクラス間依存性を下げる設計を検討しましょう。

最後に

というわけで、得た知見をつらつらと書いてみましたがいかがでしたでしょうか。
今回は武器メッシュということでスタティックメッシュへの参照を扱いましたが、基本的にすべてのアセットは同じ理屈でロードされます。
作っているプロジェクトがある程度大きくなってきて多数のアイテム、装備、敵を扱うようになったら、各アセットを参照ビューアを確認して白で繋がってるアセットは全ロードされるということを念頭に正しい参照関係が構築できているか確認するとよいでしょう。
[2020/11/05追記]
ヘキサドライブさんが「BPの参照連鎖を断つ手法」というTIPSをブログにアップされました!
ハード参照を避ける手法について、より突っ込んで詳しく解説している神記事です。
直リンク張っていいのかわからなかったので紹介だけでもしておきます。
ヘキサドライブさんのHPから、研究室→プログラムTIPS→2020/11/5とたどれば見れると思いますので、ぜひ参考にしてみてください。