【UE5】スマートポインタ
前置き
そもそもスマートなポインタもそうじゃないポインタも使ったことがないから、何がスマートなのか分からなかった。
普通のポインタについてゼロから最低限まで理解して、スマートポインタにありがたみを感じられるようになったので、自分用に軽くまとめる。
ポインタとは
プログラムは実行中、必要な値(オブジェクト)を全てメモリに置く。ポインタとは、値が置かれているメモリのアドレス(場所)を保持する変数である。
参照との違い
参照とは、既存変数のエイリアスである。よって初期化が必須であり、その際nullにすることはできない。後から他の変数を指すように変更することもできない。
ポインタよりも安全性が高いので、関数の引数や返り値として優秀である。(絶対安全ではない)
詳しくは以下を参照。 qiita.com
生ポインタの弱点
スマートじゃない普通の生ポインタは、プログラマが手動でメモリを確保(new)&解放(delete)する必要がある。
確保したメモリが解放されずに残り続けてしまうことをメモリリークと呼ぶ。メモリリークはメモリ使用量を増やし続けて、最終的にクラッシュを引き起こすヤバい奴である。
int* p = new int(10); // delete p; をしない
また、解放後もポインタにはアドレス値が残るが、これを再度使うと未定義動作を引き起こす。このような「寿命が尽きたオブジェクトを指し続けるポインタ」のことをダングリングポインタと呼ぶ。
int* p = new int(10); delete p; // 以下は全て未定義動作 int x = *p; // 参照 *p = 5; // 代入 delete p; // 二重delete
スマートポインタとは
スマートポインタとは、メモリ解放を勝手にしてくれるポインタの総称である。
C++のスマートポインタ
以下を参照。 zenn.dev
UEのスマートポインタ
以下を参照。
【UE5】UE独自のマクロが多機能すぎる話
UE独自のマクロたち
UCLASSUPROPERTYUFUNCTIONUSTRUCT
これらは全て、UnrealEngineのリフレクションシステムに登録するための宣言マクロである。どうやらUnreal Header Tool(UHT)という特殊なプリプロセッサが先にヘッダーをパースしてマクロを抽出し、メタ情報を含むコードをXXX.generated.hとして生成するらしい。UHTによって生成されたコードはGENERATED_BODY()に展開され、その後C++コンパイルが行われる。
ちなみにリフレクションとは、プログラム実行(ランタイム)中にプログラム自身が持つ構造やメタデータ(クラス名、メソッド名、プロパティ名、型情報など)を読み書きする機能のことである。UEのエディタからC++で書いた変数を読み書きしたり、BPでC++関数を呼び出したりできるのはリフレクションシステムのおかげである。C#にはあるけどC++にはない機能なので、UEが独自に実装しているっぽい。
UPROPERTY()
UPROPERTY() float Speed = 10.f;
変数に付けられるマクロ。あまりにも多機能すぎるので整理する。
UEエディタやBPへの公開
エディタからのアクセス制御
EditAnywhere- クラスデフォルトとレベルインスタンスの両方で変数の値を編集可能。
VisibleAnywhere- クラスデフォルトとレベルインスタンスの両方で変数の値を確認可能。
EditDefaultsOnly- クラスデフォルトでのみ編集可能。
VisibleDefaultsOnly- クラスデフォルトでのみ確認可能。
EditInstanceOnly- レベルインスタンスでのみ編集可能。
VisibleInstanceOnly- レベルインスタンスでのみ確認可能。
BPからのアクセス制御
BlueprintReadOnly- BPで読み取り可能。
BlueprintReadWrite- BPで読み書き可能。
- (指定なし)
- BPからアクセス不可。
AllowPrivateAccess = "true"- 変数がC++でprivateまたはprotectedとして宣言されていても、BPからのアクセスを明示的に許可
エディタ表示
詳細パネルにおける表示
Category = "Name"- プロパティを表示する詳細パネルのグループ名を指定。
AdvancedDisplay- 詳細パネルの「詳細設定」セクションに表示。
DisplayName = "Name"- エディタ上で表示されるプロパティ名を指定。(C++の変数名とは異なる名前にできる)
ToolTip = "Text"- プロパティにマウスオーバーしたときに表示されるツールチップ(説明文)を指定。
meta=キーワード
ClampMin="X", ClampMax="Y"- ユーザーが入力できる最小値と最大値を定義。
UIMin="X", UIMax="Y"- エディタで表示されるスライダーや入力ウィジェットの範囲を定義。
EditCondition = "BoolPropertyName"- 指定されたbool型プロパティがtrueの場合にのみ、このプロパティを編集可能。
ArrayClamp = "PropertyName"- 配列のサイズを、別のプロパティの値に制限。
MakeEditWidget- FVectorやFRotatorなどの構造体に対して、ビューポート内に編集用のギズモ(ウィジェット)を表示。
セーブ&ロード制御
Transient- このプロパティをシリアライゼーションの対象から除外。一時的なランタイムデータに使用推奨。
Config- この変数の値を、ゲームの.ini形式の設定ファイルにセーブ&ロード可能にする。実行ファイルの再コンパイルなしに値を調整したいゲーム設定に使用。
GlobalConfig- この変数の値を、エンジン全体のグローバルな設定ファイルにセーブ&ロード可能にする。
Localized- このプロパティの値がローカライズ(多言語対応)されることを明示。言語パックに応じて文字列が切り替わるようになる(?)。
ネットワークレプリケーション
割愛。
初期化と複製
InstancedDuplicateTransient- オブジェクトが複製されるとき、このプロパティの値をコピー対象から除外する。
AssetRegistrySearchable- このプロパティの値を、エディタのアセットレジストリ(コンテンツブラウザの検索インデックス)で検索可能にする。
ExposeOnSpawn- このプロパティを、オブジェクトがスポーンされる際に使用されるスポーンノードの入力ピンとして公開
あとがき
他のマクロについては今度書き足す…。
参考
Minecraft ForgeでMod作ってみた
Enum Flagsで入力を検知する
使用例
Enum Flagsを使って入力を検知する。
定義
[Flags] public enum KeyCodeFlag { None = 0, // ----- 移動 ----- // W = 1 << 0, A = 1 << 1, S = 1 << 2, D = 1 << 3, Right_Shift = 1 << 4, // 攻撃 E = 1 << 5, // 戻る Q = 1 << 6, // メニュー M = 1 << 7, // Ui決定 Return = 1 << 8, // スキル One = 1 << 9, Two = 1 << 10, Three = 1 << 11, }
検知
/// <summary> /// 入力中ずっと /// </summary> /// <returns></returns> private KeyCodeFlag CreateGetKeyFlag() { var flag = KeyCodeFlag.None; if (Input.GetKey(KeyCode.W)) flag |= KeyCodeFlag.W; if (Input.GetKey(KeyCode.A)) flag |= KeyCodeFlag.A; if (Input.GetKey(KeyCode.S)) flag |= KeyCodeFlag.S; if (Input.GetKey(KeyCode.D)) flag |= KeyCodeFlag.D; if (Input.GetKey(KeyCode.RightShift)) flag |= KeyCodeFlag.Right_Shift; if (Input.GetKey(KeyCode.E)) flag |= KeyCodeFlag.E; if (Input.GetKey(KeyCode.Q)) flag |= KeyCodeFlag.Q; if (Input.GetKey(KeyCode.M)) flag |= KeyCodeFlag.M; if (Input.GetKey(KeyCode.Return)) flag |= KeyCodeFlag.Return; if (Input.GetKey(KeyCode.Alpha1)) flag |= KeyCodeFlag.One; if (Input.GetKey(KeyCode.Alpha2)) flag |= KeyCodeFlag.Two; if (Input.GetKey(KeyCode.Alpha3)) flag |= KeyCodeFlag.Three; return flag; }
判定
// 拡張メソッド static class EnumExtensions { public static bool HasBitFlag(this KeyCodeFlag value, KeyCodeFlag flag) => (value & flag) == flag; }
ガベージコレクションとは
ガベージコレクションとは
ガベージコレクションとは、コンピュータプログラムの実行環境などが備える機能の一つで、実行中のプログラムが占有していたメモリ領域のうち不要になったものを自動的に解放し、空き領域として再利用できるようにするもの。そのような処理を実行するプログラムを「ガベージコレクタ」(garbage collector)という。
よくGCと略されて呼ばれる。
ちなみにGCで断片化したメモリを整理するコンパクションという処理もあるが、これは厳密にはGCとは区別される。
GCの種類
GCやコンパクションといったメモリに関わる処理はとても重いので、できる限り避けた方がいい。
UnityのGCはBoehm GCという名前で、これはGCが走るとそれ以外の処理を全て止めてしまうStop the World方式となっている。
またBoehm GCではコンパクションは行わない。
具体的にどうすればよいのか
以下の記事が分かりやすい。
light11.hatenadiary.com
また、以前にクロージャは良くないということを書いたが、これはGCを防ぐためである。
lougestudy.hatenablog.jp
仮想メソッドと脱仮想化
仮想メソッドとは
“virtual”が頭に付いた関数やインターフェイス内の関数のこと。継承先のクラスでoverrideすることで、名前は同じなのにクラスによって違う動きをするメソッドを作ることができる。このような性質を多態性という。
仮想メソッドの内部実装
仮想メソッドは仮想関数テーブル(virtual function table)という手法を使って実装されている。C++の知識が必要らしいが、一応知っておきたい気はする。でも今回はスルーで。
仮想メソッドの実行コスト
仮想メソッドの実行コストとなる要因は2つ。
- 仮想関数テーブルを引くための間接参照の増加
- インライン展開ができない
特に後者の方がパフォーマンスに大きく影響する。
インライン展開とは
インライン展開とは、関数として呼び出すよりも中身を展開してしまった方が確実に良いと判定できる関数に対して、呼び出し箇所に関数の中身を展開するコンパイラーの最適化処理のこと。
関数の呼び出しや戻り時のジャンプによって発生するコストを無くすことができる。
public class InlineSample : MonoBehaviour { private void Start() { string name = Name(); // Name()はインライン展開で"Louge"に置き換えられる } public string Name() => "Louge"; }
脱仮想化(devirtualization)とは
脱仮想化とは、メソッド内でさかのぼれば具体的な型がわかる場合に、仮装メソッドを通常のメソッド呼び出しに置き換えるコンパイラーの最適化処理のこと。
仮想関数の場合は多態的な動きをするのでインライン展開ができないが、脱仮想化を挟むことでインライン展開ができる場合がある。
public interface IPerson { string Name(); } public class Louge : IPerson { string IPerson.Name() => "Louge"; } public class DevirtualizationSample : MonoBehaviour { private void Start() { IPerson person = new Louge(); person.Name(); // IPerson.Name()の仮想呼び出しではなく、Louge.Name()を直接呼び出す。 } }
しかし実際問題、脱仮想化が入るような状況はかなりレアなので、基本的に仮想メソッドは普通のメソッドよりもコストが大きくなってしまうっぽい。
Unityの場合
Unityで用いられているコンパイラーであるIL2CPPでは脱仮想化が行われてない。つまり、仮装メソッドは仮装メソッドとしてでしか呼ばれないので、普通のメソッドよりもコストが必ず大きくなる。
但し.NET6では脱仮想化は行われるので、将来Unityが.NET6をサポートすれば高速化されるかもしれない。
クロージャに甘えない
クロージャとは
ラムダ式とかで関数の外にある変数や関数を使うと、実はそれらをキャプチャするために暗黙的にクラスがnewされる。この仕組みを使っている関数をクロージャと呼ぶ。引数を渡さなくても自由にラムダ式を書けるのはこの便利な仕組みのおかげ。
日本語だと関数閉包っていうらしい。
対策例
UniRxではステート付き関数が充実しているのでそれを使えば良い。
SubscribeWithState
protected override void Initialize() { base.Initialize(); // 入力購読 m_InputManager.InputEvent.SubscribeWithState(this, (input, self) => self.DetectInput(input.KeyCodeFlag)).AddTo(this); }
SubscribeWithState2
/// <summary> /// 敵ステータスセット /// </summary> /// <param name="enemyStatus"></param> private void SetEnemyStatus(EnemyStatus enemyStatus) { ..... 前略 // 死亡時に経験値を与える if (Owner.RequireEvent<ICharaBattleEvent>(out var battle) == true) { battle.OnDead.SubscribeWithState2(this, enemyStatus, (result, self, enemyStatus) => { foreach (var unit in self.m_UnitHolder.FriendList) { // 味方キャラによるキルなら経験値加算 if (result.Attacker == unit) { self.m_TeamLevelHandler.AddExperience(enemyStatus.Param.Ex); break; } } }).AddTo(CompositeDisposable); } }
Disposable.CreateWithState
2つ目の例ではタプルを使って複数の値を引き渡している。
/// <summary> /// カメラを追従させる /// </summary> /// <param name="parent"></param> /// <returns></returns> IDisposable ICameraHandler.SetParent(GameObject parent) { m_MainCamera.transform.SetParent(parent.transform); return Disposable.CreateWithState(this, self => self.m_MainCamera.transform.parent = null); }
/// <summary> /// バフを付与する /// </summary> /// <param name="buff"></param> /// <returns></returns> public IDisposable AddBuff(BuffTicket buff) { m_BuffList.Add(buff); return Disposable.CreateWithState((m_BuffList, buff), tuple => tuple.m_BuffList.Remove(tuple.buff)); }