2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UEのC++のお作法

2
Posted at

初めに

私は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は、プランナー調整用など後から変更したいパラメータを用意します。

BPで後から変えたいパラメータ
UPROPERTY(EditAnywhere, Category="Move")
float SlowDistance = 200.f;

C++からBPの処理を呼ぶ場合の宣言です。
BPに処理が書いてあって、C++から呼びたいときに使われます。

C++からBPの処理を呼ぶ場合
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
void SetText(const FString& text);

BPからC++の関数を呼ぶ場合
BP側に処理が書いてあり、C++の関数を呼びたいときに使います

BPからC++の関数を呼ぶ場合
UFUNCTION(BlueprintCallable)
bool IsLanguageJapanese(){return true;}

最後に

UE問わず当たり前なことも書きました。

UEは「書けて動く」だけでは不十分で、
ライフサイクル・GC・非同期・Editor実行を理解していないと、
原因不明のクラッシュやEditor落ちに悩まされます。

お作法を守るのは、お堅いルールなどではなく、
安全に開発を進めるためです。

Unityと違い、UEはお作法を守らないとクラッシュになりやすいです。
安全を考慮した設計することで、
開発スピードと安定性の両立ができるようになります。

皆さんのUE開発が、少しでも安全で快適になる助けになればと思います。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?