UE4/C++: NewObject で生成したオブジェクトの Outer を変更したい場合のメモ
↓こういうのが居たとする。
// UObject 派生だがシングルトンファクトリーで生成されるオブジェクトがあったとする UA* UA::GetInstance() { if ( ! my_static_instance ) my_static_instance = NewObject< UA >(); return my_static_instance; }
このようなファクトリーで得られる仕組みの UA
のオブジェクトは Outer
を持たないため GetWorld()
しても nullptr
が帰るなど不便な事もある。幾つか無理やり気味な方法は思いつくものの、 Outer
を外から設定できてしまえば何かと解決する。この悩み事が生じた際は UObject::Rename
が最も簡単な解決方法になる事がある。
// UGameInstance など World に絆付けされた何かから void UMyGameInstance::Something() { auto a = UA::GetInstance(); a.Rename( nullptr, this ); }
こうすると、以降は UA
のオブジェクト内部やそこからさらに NewObject<T>( this )
で生成したオブジェクトは Outer
の絆により GetWorld()
も期待動作するようになる。もちろん、もっと簡単な用途で単純に Outer
を変更したい場合も UObject::Rename
で同様に Outer
を変更できる。
UObject::Rename
は関数名からはオブジェクトの名前を変更するだけに見えるが、シグニチャーを見れば第2引数に UObject * NewOuter
とある。実装では第1引数に NULL
がデフォルトで与えられているが省略して呼ぶ意味があるかはさておき、名前を変更する必要の無い場合には nullptr
を与えれば良い事を推量できる。(実際に第1引数は nullptr
でそのように動作させられる)
// Runtime/CoreUObject/Public/UObject/Object.h /** Rename this object to a unique name.*/ virtual bool Rename( const TCHAR* NewName=NULL, UObject* NewOuter=NULL, ERenameFlags Flags=REN_None );
例によって事実上アンドキュメントなので使う場合はソースを読んだ方が良い。
// Runtime/CoreUObject/Private/UObject/Obj.cpp bool UObject::Rename( const TCHAR* InName, UObject* NewOuter, ERenameFlags Flags )
ERenameFlags
は RF_ClassDefaultObject
, REN_ForceGlobalUnique
, REN_Test
, REN_ForceNoResetLoaders
, REN_NonTransactional
, REN_DoNotDirty
, RF_Public
など指定すると意味があるようだが、さしあたり Outer
を設定、あるいは挿げ替えたいだけの場合には実引数は省略して REN_None
を与えれば良い。実装詳細的には LowLevelRename
と PostRename
も呼んでいるので思いのほか複雑な事をしている。
References
C#: Windows 青魔法により別プロセスのコマンドラインを掠め取ってくる方法、または WMI の使用方法
残念な事に別プロセスの起動時に与えられたコマンドライン情報を System.Diagnostics.Process
の StartInfo
プロパティーの FileName
や Arguments
から取得はできない。それらはプロセスの起動を自身のプロセスから行う際に自身が設定した場合にのみ意味がある。別プロセスを相手にするのなら、そのアプローチは諦めよう。
ではどうするか?黒魔法は惜しいアイデアだ。 powershell
でも cmd
でも良いが、
wmic process where "processid=32096" get commandline /format:value
CommandLine="C:\Program Files\Epic Games\UE_4.19\Engine\Binaries\Win64\UE4Editor.exe" C:\Users\usagi\tmp\Experimental\Experimental.uproject -debug -game
などと取得できる。この実行例は UE4 プロジェクトを開発環境からデバッグ実行した状態で実行された Experimental プロジェクトのコマンドライン。プロセスIDはどうやって確認したのか?それは 1つ前の記事「UE4/C++: ジェネリック・プラットフォーム・罠ビリティー」 を眺めて欲しい。
wmic
を System.Diagnostics.Process
で StartInfo
の Arguments
に適当な引数を与えて標準出力から結果を読み出してパースすれば良い。そういうやり方は生きている世界にもよるが今回の記事のレベルとしては黒魔法という事にしておく。このような黒魔法的な方法も C++ アプリから WMI でさっくりと情報を取りたい場合には C++ネイティブコードで素直にDLLをリンクしてCOMを扱う よりもよほど現実的に便利だったりする事もある。
今回は C# なので WMI をあっさり使える。 "黒魔法" 的に実装するよりもせいぜい "青魔法" 的な実装で済む。 C# プロジェクトの References
に System.Management
を追加し、次のような実装を行えば先の wmic
と同様の事を "一応" C# だけで実装できる。
using System.Management; static private string GetProcessCommandline( Int32 process_id ) { using ( var s = new ManagementObjectSearcher() ) { s.Query.QueryString = "select * from win32_process"; using ( var c = s.Get() ) { var pid_string = process_id.ToString(); foreach ( var o in c ) if ( o[ "processid" ].ToString() == pid_string ) return o[ "commandline" ].ToString(); } } return null; }
C# のセンセイ級の諸姉諸兄におかれましてはにゃーんか納得できないお気持ちでしょうか。
「クエリーを文字列で書くのか👹」
はい、そういうわけで "青魔法" っぽいじゃないですか。あはは😅
「
foreach
使っていいのはISO/IEC 23270:2006 C# (2.0)
までだから👹」
ManagementObjectSearcher
系に Where
とか LINQ
対応を実装してくれていない Microsoft に言って下さい。
「その失敗
null
じゃなくて例外使えよ👹」、「WMI 原理主義者としてCommandLine
と書くべきと要求する👹」、「文字列じゃなくてInt32
に合わせて比較しろよ👹」…
自分で何か作る時には自分へ要求して下さい。本当に効率が良いか、必要な事なのかはさておき。
「にゃーん」
『にゃあああああああああああああああん♥』
と、いうわけで WMI "青魔法" 実装でした😃 C++ と違ってまだ必要ならちょろりと実装を書こうかという気がしますが、そもそも OS の API としてfjkfjk (゜∀。)
にゃーーーーん。
References
- wmic | Microsoft Docs
- ManagementObjectSearcher Class (System.Management) | Microsoft Docs
- ManagementObjectCollection Class (System.Management) | Microsoft Docs
- ManagementObject Class (System.Management) | Microsoft Docs
- ManagementObjectCollection.ManagementObjectEnumerator Class (System.Management) | Microsoft Docs
- WMI C++ Application Examples | Microsoft Docs
- ProcessStartInfo.FileName Property (System.Diagnostics) | Microsoft Docs
UE4/C++: ジェネリック・プラットフォーム・罠ビリティー
諸事情により UE4 でゲームではない何かを開発したりユカイなマルチプロセスのゲームを作ったりすると プロセスID
を取得したくなるかもしれない。わたしはなった。
UE4 にはそんな時に便利そうな FGenericPlatform::GetCurrentProcessId
がある😃 プロセスIDを取得するためにクロスプラットフォームコードをちまちまOSごとに書く必要の無い理想郷がここにある。
さっそく実装しよう。
const auto pid = FGenericPlatform::GetCurrentProcessId(); UE_LOG( LogTemp, Log, TEXT( "pid=%d" ), pid )
しかしこの結果は常に、
0
となる。
驚きだろうか。私は一瞬驚いてソースコードを読みに行ってしまった。
// Runtime/Core/Private/GenericPlatform/GenericPlatformProcess.cpp uint32 FGenericPlatformProcess::GetCurrentProcessId() { // for single-process platforms (consoles, etc), just use 0 return 0; }
( ゚д゚)...
で、3秒くらいこの顔のまま考えて気づいた。『こっちじゃない』って。
// 『― ごめんね、ここはあなた( `Generic` )じゃダメなの ―』 // const auto pid = FGenericPlatform::GetCurrentProcessId(); // 『― あなたと、結合(リンク)したい! ―』 const auto pid = FPlatformProcess::GetCurrentProcessId(); UE_LOG( LogTemp, Log, TEXT( "pid=%d" ), pid )
FPlatform
は実質的に Unreal Engine API Reference でもアンドキュメントになってしまっているのだけど、
// Runtime/Core/Public/HAL/PlatformProcess.h #if PLATFORM_WINDOWS #include "Windows/WindowsPlatformProcess.h" #elif PLATFORM_PS4 #include "PS4/PS4Process.h" #elif PLATFORM_XBOXONE #include "XboxOne/XboxOneProcess.h" #elif PLATFORM_MAC #include "Mac/MacPlatformProcess.h" #elif PLATFORM_IOS #include "IOS/IOSPlatformProcess.h" #elif PLATFORM_ANDROID #include "Android/AndroidProcess.h" #elif PLATFORM_HTML5 #include "HTML5/HTML5PlatformProcess.h" #elif PLATFORM_LINUX #include "Linux/LinuxPlatformProcess.h" #elif PLATFORM_SWITCH #include "Switch/SwitchPlatformProcess.h" #endif
こんな具合に実装されていて、例えば Windows では
// Runtime/Core/Public/Windows/WindowsPlatformProcess.h struct CORE_API FWindowsPlatformProcess : public FGenericPlatformProcess // 中略 typedef FWindowsPlatformProcess FPlatformProcess;
と、なっていて、もちろんユーザープロジェクトからも他のエンジン実装のライブラリー群と同様に使用できる。
API Reference ドキュメントから機能を探してほぼ無意識にホイホイと実装してしまうと、うっかり FPlatformGenericProcess
ほか Generic 系を叩いてしまうのだけど、プラットフォームに依存性の高い機能は今回の GetCurrentProcessId
のように期待動作せず「( ゚д゚)」の3秒が訪れる事がある。
意識をしっかり保ちましょう。わたし、プログラマー( ・`ω・´)
C#/JSON/HTTP: .net で JSON-RPC-2.0 のクライアントを使ったら WebException が飛び ServerProtocolViolation で蹴られた件
Visual Studio で C# プロジェクトをにゃんにゃんしている土曜日だった。JSON-RPC-2.0
のクライアントを実装する必要があったので NuGet
を見ると導入が簡単そうなライブラリーが幾つも見つかる。
このへんが使うの簡単で使ったらいいんじゃないかと思ったライブラリー:
ダウンロード数とアップデートを見ると Nethereum.JsonRpc.RpcClient とか EdjCase.JsonRpc.Client が良いかもしれないと思ったが、依存ライブラリーが多く、そもそも大きなライブラリーの小さな部品の1つとして設計されていて JSON-RPC-2.0 クライアントをさっくり簡単に使いたい場合には無駄に複雑な上にドキュメントも難解でした。
ライブラリー選びによらず、たいていの .net の HTTP クライアントは System.Net.Http
をライブラリーの実装詳細に用いている。先に挙げた2つもそのタイプ。
使ってみると System.Net.WebException
例外が飛んできた。catch
してみると
Status
: ServerProtocolViolationMessage
: The server committed a protocol violation. Section=ResponseStatusLine
だそうだ。
WebException 例外氏「オレは悪くない。なんかサーバーがおかしい。RFC違反のヘッダー返してくる。だからオレは知らん。」
みたいな。
さて、この例外が帰ってくるサーバーアプリは、 Chrome
や curl
で目視確認する限りではヘッダー含め異常は無く、人間の目には正常に JSON-RPC-2.0 のレスポンスを返してくれていた。
curl --dump-header - http://127.0.0.1:50080/API/JSON-RPC-2.0/
HTTP/1.1 200 OK Content-Length: 97 Content-Type: application/json Access-Control-Allow-Origin: * {"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error; an invalid JSON data"},"id":null}
どう見ても正常に JSON-RPC-2.0 のエラーレスポンスを 200 OK している。少なくとも HTTP レスポンスのレベルでは問題無さそうに見える。(本当は問題があるのだが😋)
この状況でもクライアントアプリのプロジェクトの App.config
に <httpWebRequest>
要素を追加し、 useUnsafeHeaderParsing = true
を設定すると JSON-RPC-2.0 ライブラリーたちは取得結果のパースも正常に行え、期待動作してしまう。
<configuration> <!--他の要素は省略--> <system.net> <settings> <httpWebRequest useUnsafeHeaderParsing="true"/> </settings> </system.net> </configuration>
しかし、キモい。
さて、見えない原因は何だろうか?
幸い WebException
が問題があると言うバイナリーは現にそこにあるのだ。解析すればよい。私、プログラマー。バイナリー、トモダチ。
curl --trace - http://127.0.0.1:50080/API/JSON-RPC-2.0/
== Info: Trying 127.0.0.1... == Info: TCP_NODELAY set == Info: Connected to localhost (127.0.0.1) port 50080 (#0) => Send header, 96 bytes (0x60) 0000: 47 45 54 20 2f 41 50 49 2f 4a 53 4f 4e 2d 52 50 GET /API/JSON-RP 0010: 43 2d 32 2e 30 2f 20 48 54 54 50 2f 31 2e 31 0d C-2.0/ HTTP/1.1. 0020: 0a 48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 .Host: localhost 0030: 3a 35 30 30 38 30 0d 0a 55 73 65 72 2d 41 67 65 :50080..User-Age 0040: 6e 74 3a 20 63 75 72 6c 2f 37 2e 35 38 2e 30 0d nt: curl/7.58.0. 0050: 0a 41 63 63 65 70 74 3a 20 2a 2f 2a 0d 0a 0d 0a .Accept: */*.... <= Recv header, 16 bytes (0x10)
ここまで送信。問題はここから先、帰ってくるレスポンスのヘッダー領域にある事になっている。
0000: 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0a HTTP/1.1 200 OK. <= Recv header, 19 bytes (0x13) 0000: 43 6f 6e 74 65 6e 74 2d 4c 65 6e 67 74 68 3a 20 Content-Length: 0010: 39 37 0a 97. <= Recv header, 31 bytes (0x1f)
…む??
0000: 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 61 70 Content-Type: ap 0010: 70 6c 69 63 61 74 69 6f 6e 2f 6a 73 6f 6e 0a plication/json. <= Recv header, 31 bytes (0x1f)
…これは・w・
<= Recv header, 31 bytes (0x1f) 0000: 41 63 63 65 73 73 2d 43 6f 6e 74 72 6f 6c 2d 41 Access-Control-A 0010: 6c 6c 6f 77 2d 4f 72 69 67 69 6e 3a 20 2a 0a llow-Origin: *. <= Recv header, 1 bytes (0x1)
なるほど。
0000: 0a . <= Recv data, 97 bytes (0x61)
今回は原因が「見え」ました😃 よくバイナリーを見ると、レスポンスヘッダーのセパレーターの改行文字が 0x0a
だけになっています。 0x0d
どこ行った。と、言うわけで今回の原因はサーバーアプリが HTTP レスポンスヘッダーのセパレーターの改行文字を本来ならば RFC2616 §4 HTTP Message に準拠し "CR LF"
( 0x0D 0x0A
) を使わなければなりませんところを "LF"
( 0x0A
) のみで返している事らしいとわかる。
で、サーバーアプリの HTTP レスポンスヘッダーの生成箇所の実装ソースコードを確認してみると、
constexpr auto ret = TEXT( "\n" );
などと書いてありました。
constexpr auto ret = TEXT( "\r\n" );
このように修正すると、 System.Net.Http
は useUnsafeHeaderParsing = false
でも期待動作するようになり WebException
も飛ばなくなりました。こわいですね💀
References
.net./C#/UE4: UE4 製のアプリを C# から起動したり終了したりさせたい場合のメモ、あるいは閉じても閉じても殺しても死なないアプリを殺すメモ。
// Process using System.Diagnostics;
// C# アプリから UE4 製のアプリ(に限らず)を起動してプロセスをハンドルする var p = Process.Start( "your-executable-path.exe" );
// 既に動作している UE4 製のアプリ(に限らず)を探してプロセスをハンドルする // note: hoge.uproject で作った hoge.exe なら hoge で探せる。 var ps = Process.GetProcessesByName( "your-ue4-project-name" ); if ( ps.Length > 0 ) var p = ps.First();
// ハンドルしている UE4 製のアプリ(に限らず)のプロセスを殺す(つもりだが死なない) p.Close(); // <-- 残念でした。死にません。 // メインウィンドウを殺す(つもりだが死なない) p.CloseMainWindow(); // <-- 残念でした。死にません。 // お前を…殺す(つもりだが死なない) p.Kill(); // <-- 残念でした。死にません。
実は死んでいるのだが死んでいない。UE4製のアプリは元気に何事も無く動作している上に、 GetProcessByName
に発見されないステルス状態になっている…かのような挙動が観測されるようになる。
ここで UE4 製のアプリのプロセスがどうなっているのか Process Monitor とかそういう何かでプロセスツリーを確認しよう。(Windows の PowerShell
でも ps
がエイリアスとして動作するようだけど、ツリー表示はできないらしい。 WSL
からでは Windows のプロセスは見えないし。不便じゃのぅ…😅)
今回は G5
という UE4 製のアプリです。 UE4 のパッケージ機能を使い Windows 向けにプロダクトを出力すると、出力ディレクトリー直下に <project-name>.exe
が生成され、一般的には配布先のユーザーはこれを起動してアプリを使います。
しかし、実際にはこの出力ディレクトリー直下の <project-name>.exe
は Binaries/Win64/<project-name>-Win64-Shipping.exe
などを子プロセスとして起動させる事で UE4 製のアプリの"本体"(というか"実体")を間接的に起動させるプロセスのラッパー。だから C# の実装で <project-name>
で起動したり探したりしたプロセスを Close
しても CloseMainWindow
しても Kill
してもユーザーの目に映る動作中の UE4 製のアプリの "本体" は死なずに動作し続けます。プロセス名も尾びれが付いていて異なるため、目に見える UE4 製のアプリは動き続けているのに GetProcessesByName
で探せない一見すると謎のステルス状態のように観測されるようになります。
// UE4 製のアプリを起動してプロセスをハンドル var p = Process.Start( "your-executable-path.exe" ); // ぶち殺す using ( var killer = new Process() ) { killer.StartInfo.FileName = System.IO.Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.System ), "taskkill.exe" ); killer.StartInfo.Arguments = string.Format( "/PID {0} /T /F", p.Id ); killer.StartInfo.CreateNoWindow = true; killer.StartInfo.UseShellExecute = false; killer.Start(); killer.WaitForExit(); } // ↑ ref. https://qiita.com/yohhoy/items/b6e32e17c9d568f927d8 // @yohhoy さんありがとう♥(2014年の記事なので最新のピカピカマイクロソフトAPIではこんなことしなくて済むのか知らんけど。)
率直に言ってウケル…笑っていいと思います。でも、 Windows ってたまにこーゆーことした方が楽だったり高速だったりって事あるのよね…。例えば真面目に WMI を使うよりコマンド呼んで文字列の結果をパースした方が早かったり速かったり、なんてこともあった。
UE4 アプリの場合は子プロセスで起動される事に気づいてしまえば子プロセスを確定する方法はもうちょっと丁寧だったり、あるいはファイルから泥臭く拾ってくるにせよ Close
を呼んであげる事もできるけれど、汎用性もあるのでブログにこの方法もメモしておく事にしました。
そもそも終了に限らず別プロセスからにゃんにゃんするなら適当なプロセス間通信の手段を用意した方がいいけどね😅
References
C#/WPF: 10年ぶり?5年ぶり? Visual Studio で C# で XAML で WPF で
Note: 今回は技術的内容はたぶん無いよう・w・ たぶんただのエッセイだよう・w・
Visual Studio
で C#
を書くのはお気持ち的にはたぶん10年ぶりくらい(実際はそんなに長くないよ😋)。 C#
でアプリを書くのは5年ちょっと振りくらい(たぶんほんと)。 C#
や .net
の概況や言語仕様のアップデートは毎年1回くらいは気にかけていたけどね(メテオにはMPが足りないテラおじいちゃんが「おもいだす」できる程度には)。
私が C#
を使わなくなった理由は幾つかあるけれど、
Managed DirectX
の緩やかな衰弱、そして事実上の死XNA
の緩やかな衰弱、そして死Silverlight
の緩やかな衰弱、そして死Microsoft
に振り回されたくない想いの強まりMono
黎明期とGNU/Linux
プラットフォーム主環境化とASP.net
によるサーバーサイドエンジニア化- そもそもわたしが
C#
を使う理由が消失した
そんな事が10年くらい昔から5年くらい前までに私が C#
を直接は使わなくなった流れ。言語としては好きなんだよ😃
で、最近ね、仕事のボスが
「伊藤さん、技術力すごい評価するけどビジネスアプリ的なUIとか作ってくれないですよね(だからボーナス評価は並に落としますね」
とか言うの。まあ、私の会社じゃないし、お世話になってるだけだからね、要求には答えてあげないとね♥
『べ、べつに作れないわけじゃ・・ないんだから!(ぁーだりぃーょぉー、そんなダサいもの作りたくないでござるよー・・・うぐぅー』
と、いうわけで伊藤さんは中略 WPF
と Visual Studio
と C#
で Windows
向けのビジネスアプリ的なUIというやつをぺぺっと作ってやることにしたのでした。
先ず、 C++
しか使わないので入れていなかった C#
を WPF
でデスクトップアプリつくれるっぽいキットを Visual Studio Installer
を起動してぽちぽちインストールします。 Win Form
そんな存在すら危うさを感じるものいまさら使いません💀 UWP
そんな制約で疲れそうなものぺぺっと作りたいだけなのに使いません(たぶん勝手な思い込みです)。
既に C++
環境のため Visual Studio
の本体的な何かはインストールされているため、1分もかからずあっさりダウンロードとインストールがおわった。再起動も不要だったのでそんなことに無駄に小さな幸せを感じてしまった私に悔しさを感じた。
起動して WPF
デスクトップアプリのプロジェクトを新規作成。なんかフォーム(WPF
なのでそうじゃない)のヴィジュアルなデザイナーの下にコードが同時表示されている。
お久しぶりデース、XAML
=サン。ぁぁ、懐かしいですね、わたし好きですよ、こういう GUI
をヴィジュアルプレビューと記述用のテキストな言語で作れる何かそういうの。(最近は UMG
( UE4
)を使っていたのでテキストソースでは簡単にはイジれないんだよね。その前は imgui
で、あれはソースでしかイジれないしデザインのプレビューとか完全脳内コンパイラー必要だし。)
わたしの脳内のわたしじゃない誰か「ハァィ、うさぎー、それじゃあ今回はどうして
QML
は使わなかったんダーイ?」
わたしの脳内のそれじゃない誰か『うるせえな、あんなもん使ったら
GPL
汚染(厳密には使うコンポーネントやソースのバージョンによってLGPL3
だったりGPL3
だったりGPL2
だったりする )を受けるしクロスプラットフォームなんてどうでもよく Windows でだけぺぺっと動けばいいなら Qt 開発環境もめんどくせーんだよ・w・』
脳内で誰かがおしゃべりしていた。私にはどうでもよいことだが。
わたしらしき何者か『さて、とりあえずボタンでも付けてみようか。』
Visual Studio
のコンポーネントがアイコンで表示されてくれていてマウスでどーっとするとばーんと GUI
ができるあのアレは…どこだ…。
わたしのなかのかわいいかもしれないやつ『ぉぉ~なるほどぉ~!こんなところに縮こまっておったかーー、気付かんところだったではないかぁ~~😂』
などと思った気がしたが、ぺぺっとアプリを作るためにこの子には脳内の宇宙の扉の先のどこかへお帰り頂いた。
「押せ。」と一応書いてから気づいたが、引いたり、さすったり、殺したり、おしゃべりしたりはできないのでデザイン工学に一杯取られた気がした。実は殺すことはたぶんできるのだが、それは実行時にウィンドウハンドルを(中略)そのはなしではなかった。ぺぺっと忘れて続けよう。
それで、ただボタンを付けてイベントを束縛しても面白くないので Visual Studio
のヴィジュアルな力とやらを見てやろうと思った。グループ化してテキストボックスも入れてやろう、グフフ…。…これを…こうして…ここへ…。
たぶんわたしに接近してきたわたし「私の…わたしの…「かわいいボタン。押せ。」が消えてしまったではないかぁ~~~~~うわぁああああああん」
こういう時、かわいいやつを出すと得をするのだが、それは相手が居る場合だ。残念だったな、帰れ、かわいいわたし。
わたし、プログラマー。ソース、信じる。(ごにょごにょ
わたし『ぐぬぬ…。なーにがグループじゃぁ~~\(^o^)/ うっかり騙されたではないかぁ~~~!(設計に基づく正しい挙動です)』
なんて遊びながらでもぺぺっと GUI
アプリ作ってさっさと動かせちゃうんだから、便利でいいよね。
感覚をハンドリングできはじめてきた気がするので、落書きエッセイはこのくらいにします。こんなの読んでくれて、ありがとうね😋
UE4/C++: C++er がUE4型のシングルトンを作る際に気をつける事
「UE4 にはガベージコレクションがある。しかし特に C++er は正しい実装を過剰に気に掛ける必要がある。」
UE4Editor から GUI でも設定できる GameSingletonClass
, GameInstance
を使う場合は気にする必要は無い。今回はその仕組みを使わない場合の話。
UObject
派生型のシングルトン・オブジェクトを C++ の実装側だけで NewObject
し意図しないガベージコレクションを行わせない方法:
// static なシングルトンのインスタンスを取得するメンバー関数など UMySingletonType* GetInstance() { if ( instance = nullptr ) { instance = NewObject< UMySingletonType >(); check( instance ) // ↓ これ: UObject の基底型の UObjectBaseUtility のメンバー関数 instance->AddToRoot(); } return instance; }
たまに忘れる。そして初回動作に安心してしばらく、1分以上経過してからインスタンスが事故を起こす。この時、直接インスタンスを自身のコードで扱っていればすぐに気づくが、このクラスを介して保持しているはずオブジェクトをクローズドソースのサードパーティー製のライブラリーに弱参照させている場合など原因へ辿り着くには時間と経験が必要になり困難なバグとなる。
なお、 UObjectBaseUtility::AddToRoot
の対は UObjectBaseUtility::RemoveFromRoot
。UObjectBaseUtility::AddToRoot
は UObjectBaseUtility::SetFlags( RF_MarkAsRootSet )
と等しい。RF_MarkAsRootSet
は EObjectFlags
で定義される enum
値。