初めに
私はUnityを長く使ってきました。
今いるプロジェクトでUnreal Engine とC++を久しぶりに使いました。
Unityに比べると、Unreal Engine はお作法を守らないと不具合が起きやすいエンジンです。
今回は、UEのC++における基本的なお作法について説明していきます。
コード規約
UEのコード規約があります。
コード規約に困ったら、大体これでいいと思います
https://dev.epicgames.com/documentation/ja-jp/unreal-engine/epic-cplusplus-coding-standard-for-unreal-engine
変数周り
int32を使おう
UEから整数型のint32が提供されています。
もちろんshortもありますが、正直あまり使いません。
整数型をメンバ変数にする場合は、int32を使いましょう。
関数内でも基本的にint32で良いと思います。
レジストリや最適化を理解しているならintでも良いかもしれないです。
構造体の初期化
構造体の変数は初期値を入れましょう
理由としては、初期値を入れないとシッピング時にエラーになるからです。
(シッピング==ROM作成)
USTRUCT(BlueprintType)
struct FCameraData
{
GENERATED_BODY()
// フェードするか?
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool IsFade = false;
// Fov
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float FieldOfView = 90;
// 座標
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector Location = FVector(0,0,0);
// 回転
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FRotator Rotation = FRotator(0.f, 0.f, 0.f);
};
UPROPERTYについて
TObjectPtrの前につけましょう
TObjectPtrがGCされるのを防ぎます
UPROPERTY()
TObjectPtr<UDialog> m_dialog = nullptr;
ポインター
基本的にUEは生のポインターをそのまま保持することはありません。
理由はGCが走った時に解放されてしまうからです。
TObjectPtrやTWeakObjectPtrを適宜つけていきましょう
UPROPERTY() もお忘れなく
基本メンバ変数のみに使います
関数内は遅延やコールバック関数が無いなら、生でも良い。
UDialog* m_dialog = nullptr
UPROPERTY()
TObjectPtr<UDialog> m_dialog = nullptr;
UPROPERTY()
TArray<TObjectPtr<AMapParts>> m_parts;
CPP周り
ポインターのNullチェック
ポインターのNullチェックもUE独自のを使います
UEはUnityと違い、NULLチェックを怠りエラーが起きるとEditorが落ちてしまいます。
(作業途中のも消える)
すべてにNullチェックをしましょう。
エラー文もお忘れなく、落ちた時に何が原因かわかるようになります。
// 何もチェックしてない
auto id = m_Actor->GetId();
// IsValidを使ってない。参照先のアドレスがないときに、評価段階でエラーになってしまう
if( m_Actor )
{
auto id = m_Actor->GetId();
}
// エラーが書かれてないので、エラーが起きたか分からない。
if (IsValid(m_Actor))
{
auto id = m_Actor->GetId();
}
if (IsValid(m_Actor))
{
// 適当な処理
}
else
{
UE_LOG(LogTemp, Error, TEXT("%s : m_Actor is not Valid"), UTF8_TO_TCHAR(__FUNCTION__));
}
2重の->はやめよう!
こんなコード見かけたことありませんか?
m_component->GetButton()->SetVisibility(true);
動作上は問題ありませんが、安全性の観点から
このような書き方は絶対にやめましょう!
Null チェックが抜けやすく、
途中のポインタが nullptr の可能性を見落としがちになります。
UE ではクラッシュに直結します!
もしやるなら、オブジェクト指向に従って、
Componentにpublicな関数を用意して実行しましょう!
if (IsValid(m_component))
{
m_component->SetVisibility(true);
}
else
{
UE_LOG(LogTemp, Error, TEXT("%s : m_component is not Valid"), UTF8_TO_TCHAR(__FUNCTION__));
}
Castの結果も必ずチェック
Castの結果も必ずチェックしましょう。
エラーになるとエンジンが落ちてしまいます。
auto* MyChar = Cast<AMyChara>(Actor);
MyChar->Update();
if (AMyChar* MyChar = Cast<AMyChar>(Actor))
{
MyChar->Update();
}
TMapについて
いわゆる連想配列です。
配列であるTArrayより参照が早いのが特徴です。
マスタデータとか膨大なデータで使うのがおすすめです。
使うときは、.Find()がおすすめです。
ポインターで返ってきて、データが無かったらNullで返ってくるからです。
参照するときは必ずNullチェックを行うようにしてください。
UPROPERTY(BlueprintReadWrite, EditAnywhere)
TMap<Enum, int> DirectionTarget;
auto nextTarget = DirectionTarget.Find(type);
if( nextTarget )
{
// 適当な処理
}
else
{
UE_LOG(LogTemp, Error, TEXT("%s : nextTarget is not Valid"), UTF8_TO_TCHAR(__FUNCTION__));
}
■余談
Map["Test"]で参照すると、対象が見つからない場合クラッシュします。
存在するかチェックするのもいいですが、Nullが返ってくる.Find()が無難です。
配列アクセスは IsValidIndex を使おう
配列の範囲もチェックしましょう。
TArray<int32> Items;
auto item = Items[0]
TArray<int32> Items;
auto item = 0
if (Items.IsValidIndex(0))
{
item = Items[0];
}
ログはUE_LOGを使おう
UE専用のLog表示があります。
UTF8_TO_TCHAR( _ _ FUNCTION _ _)でクラス名と関数名が表示できます
UE_LOG(LogTemp, Error, TEXT("%s : item is not Valid"), UTF8_TO_TCHAR(__FUNCTION__));
ensureを使おう
check()やensure()は、任意のタイミングで中断させることができます。
checkは原則不可が無難です。
ensure
警告を出すだけです。
IDEからUEを起動していると、デバッガでBreakさせて一時中断させることができます。
その後、処理を継続することができます。
エラーが起きても処理が継続するように設計しましょう。
check
shipping以外では停止してしまいます。
継続不可です。
開発への影響が大きいため、設計上あり得ない場合のみ書くようにしましょう
check は「コードのバグ検出」、
ensure は「実行時エラーの検知」に使い分けましょう。
非同期読み込み
UAssetManager::GetStreamableManager() を使おう
テクスチャやActorとかを非同期読み込みしたいときに使います。
void AsyncLoad(UObject* owner, FSoftObjectPath path, TFunction<void(UObject* asset)> callback)
{
TSoftObjectPtr ownerPtr = owner;
// 非同期読み込み
FStreamableManager& streamableManager = UAssetManager::GetStreamableManager();
streamableManager.RequestAsyncLoad(path, FStreamableDelegate::CreateLambda(
[=](){
if (!ownerPtr.IsValid()) {
return;
}
UObject* asset = path.ResolveObject();
if (callback)
{
callback(asset);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("%s : callback is not set."), UTF8_TO_TCHAR(__FUNCTION__));
}
}));
}
TSoftObjectPtr<UTexture2D> IconImage;
FSoftObjectPath IconImagePath = IconImage.ToSoftObjectPath();
AsyncLoad(
this,
IconImagePath,
[this](UObject* asset)
{
UTexture2D* loadedTexture = Cast<UTexture2D>(asset);
if (loadedTexture)
{
// テクスチャ関連の処理
}
else
{
UE_LOG(LogTemp, Error, TEXT("%s : Failed to load texture."), UTF8_TO_TCHAR(__FUNCTION__));
}
}
■余談
BPにも非同期ロードできる「async load asset」というのがあります。
こちらは、UKismetSystemLibrary::LoadAssetClassを呼び出しています。
FStreamableManagerを作成していて、競合してしまう可能性が高そうです。
ファイル参照
UEとUnityの違いで大きなもの1つは、ファイル参照だと思います。
Unityの動的ロードは、ファイルパス直書きですが、
UEはTSoftObjectPtrなどを使ってアセットをアタッチします。
なので、UEなのにファイルパス直書きをすることは絶対にやめましょう。
ソフト参照
ハード参照、ソフト参照というのがあります。
2つともUEのアセット読み込みに使われます。
ソフト参照とは、任意のタイミングで読み込みをする型です。
ハード参照は、参照元のアセットが読み込まれると自動的に読み込まれます。
基本的にソフト参照を使います。
// 画像などのアセット参照(ソフト参照)
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSoftObjectPtr<UTexture2D> SmallThumbTexture;
// Actor / UObject クラスのソフト参照
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSoftClassPtr<UDialog> DialogClass;
// クラス制約付きの参照(主に SpawnActor 用)
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TSubclassOf<UInputController> InputControllerClass;
TSoftObjectPtr
アセット参照・遅延ロード・インスタンス管理向け
TSoftClassPtr
Actor / UObject クラスのソフト参照
クラスを遅延ロードしたい場合に使用
TSubclassOf
主に SpawnActor 時に使用。
※基本的にハード参照になる点に注意
Blueprintとの正しい付き合い方
基本的にC++を中心に書いていきます。
BPは、プランナー調整用など後から変更したいパラメータを用意します。
UPROPERTY(EditAnywhere, Category="Move")
float SlowDistance = 200.f;
C++からBPの処理を呼ぶ場合の宣言です。
BPに処理が書いてあって、C++から呼びたいときに使われます。
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
void SetText(const FString& text);
BPからC++の関数を呼ぶ場合
BP側に処理が書いてあり、C++の関数を呼びたいときに使います
UFUNCTION(BlueprintCallable)
bool IsLanguageJapanese(){return true;}
最後に
UE問わず当たり前なことも書きました。
UEは「書けて動く」だけでは不十分で、
ライフサイクル・GC・非同期・Editor実行を理解していないと、
原因不明のクラッシュやEditor落ちに悩まされます。
お作法を守るのは、お堅いルールなどではなく、
安全に開発を進めるためです。
Unityと違い、UEはお作法を守らないとクラッシュになりやすいです。
安全を考慮した設計することで、
開発スピードと安定性の両立ができるようになります。
皆さんのUE開発が、少しでも安全で快適になる助けになればと思います。