【UE5】小ネタ集【C++】

C++ を使うときに知っていると便利な機能のまとめです。
ひとつひとつは記事にするほどの情報量がないので、まとめました。
思いついたら追記して行きます。

動作環境
Unreal Engine 5.5.1
Windows 11 Home
Visual Studio 2022

更新履歴
25/01/14 記事を公開

もくじ

オブジェクト名
UObject::GetName() で取得した “オブジェクト名”_1 の末尾の _1 を取り除く
スポーンするアクターの名前の末尾にユニークな番号を付ける

ガベコレ
UObject のポインタがガベコレ中かどうかを調べる
UPROPERTY を使わずにガベコレを防ぐ

ゲーム起動、シミュレーション
エディタから起動した Standalone かどうかを調べる

インターフェース
UINTERFACE の UFUNCTION をオーバーライドする
ブループリントに追加したインターフェース関数を C++ で呼び出す

ログ
UE_LOG で関数名を出力する
ログカテゴリーを .cpp のみに定義する
ログタイプを変更する(Log Verbose など)

乱数
重み付きランダムの計算をしてくれる関数

プロジェクト設定
プロジェクト設定に自前の項目を追加する

コンソール、デバッグコマンド
コンソール変数を追加する
コンソールコマンド(デバコマ)を追加する

UObject::GetName() で取得した “オブジェクト名”_1 の末尾の _1 を取り除く
FString ObjectName = MyObject->GetName();
if (ObjectName.Len() > 2)
{
  int32 LastUnderscoreIndex;
  if (ObjectName.FindLastChar('_', LastUnderscoreIndex))
  {
    ObjectName = ObjectName.Mid(0, LastUnderscoreIndex);
  }
}

UObject のポインタがガベコレ中かどうかを調べる

ガベコレ中でもポインタは有効ですが、アクセスするとクラッシュするので、削除フラグが立っているかどうかをチェックして、フラグが立っているならアクセスしないように対応できます。
解放後のポインタはアクセスしただけでクラッシュする状態になっている場合もあるので、確保したポインタの状態を調べるのには限界があります。
どんな状態でもクラッシュしないように対応するには、自分でアロケータとガベコレの仕組みを作る必要があります…。

bool IsSetGCFlag(const UObject* const Object)
{
  if (!IsValid(Object)) { return false; } // 既に無効になっている(trueにしても良いかも)
  return Object->HasAnyFlags(EObjectFlags::RF_TagGarbageTemp | EObjectFlags::RF_BeginDestroyed | EObjectFlags::RF_FinishDestroyed);
}
PIE 中かどうかを調べる
bool IsInPIE()
{
  //return GEditor->bIsSimulatingInEditor || GEditor->PlayWorld;
  return GIsPlayInEditorWorld;
}

エディタから起動した Standalone かどうかを調べる

エディタなしのスタンドアローン(パッケージ起動)と、エディタから起動したスタンドアローンの識別方法。

bool IsInEditorStandalone()
{
#if WITH_EDITOR
  return GIsEditor && !GIsPlayInEditorWorld && IsRunningGame();
#else
  return false;
#endif
}

UINTERFACE の UFUNCTION をオーバーライドする

標準 C++ に慣れ親しんでいると奇妙にしか見えない UE の独自仕様。

UINTERFACE() class UHogeInterface : public UInterface {
  GENERATED_UINTERFACE_BODY()
};

// class での宣言は不要だが、実装コードは必要
UHogeInterface::UHogeInterface(const FObjectInitializer& _)
  : Super(_) {}

class IHogeInterface {
  GENERATED_IINTERFACE_BODY()
public:
  // IInterface 側に実装コードを書いてはいけない
  // 宣言のみ書く
  UFUNCTION(BlueprintNativeEvent)
  int32 GetFusaFusa() const;
};

UCLASS() class UMyClass : public AActor, public IHogeInterface {
  GENERATED_BODY()
public:
  // _Implementation を付けると、IInterface に宣言した
  // UFUNCTION をオーバーライドできる。
  virtual int32 GetFusaFusa_Implementation() const { return 2323; }
};

ブループリントに追加したインターフェース関数を C++ で呼び出す

動作確認までの流れ

  1. 下記コードの MyClass を親にしたブループリントアクターを UE エディタで新規作成。
  2. 1. の Class Settings > Details > Interfaces > Implemented Interfaces > Add で Hoge Interface を追加。
  3. ブループリント側で GetFusaFusa を実装。
  4. プレイ時に 1. のブループリントアクターをスポーン。
UINTERFACE() class UHogeInterface : public UInterface {
  GENERATED_UINTERFACE_BODY()
};

// class での宣言は不要だが、実装コードは必要
UHogeInterface::UHogeInterface(const FObjectInitializer& _)
  : Super(_) {}

class IHogeInterface {
  GENERATED_IINTERFACE_BODY()
public:
  // IInterface 側に実装コードを書いてはいけない
  // 宣言のみ書く
  UFUNCTION(BlueprintNativeEvent)
  int32 GetFusaFusa() const;
};

// このクラスを親にしたブループリントアクターを作成したら
// 詳細のインターフェース>実装インターフェースに Hoge  Interface を追加し
// 「ブループリントアクター」を UWorld::SpawnActor() などでスポーンさせる。
// ブループリント側にインターフェースを追加しているので、↓ の C++ のアクターを
// 直接スポーンするとインターフェースが見つからず GetFusaFusa() は呼ばれない。
// なので、必ずブループリントアクターをスポーンするよう注意する。
UCLASS(Blueprintable) class UMyClass : public AActor {
  GENERATED_BODY()
public:
  virtual void BeginPlay() override {
    UClass* MyClass = this->GetClass();
    const bool bImplemented = MyClass->ImplementsInterface(UHogeInterface::StaticClass());
    if (bImplemented) {
      // UFUNCTION の関数名に Execute_ を付けることで、ブループリント側に
      // 実装した関数を C++ 側で呼び出せる。
      // UHogeInterface を継承している MyClass のポインタを渡すことで
      // IHogeInterface のメンバ関数を static 関数のように呼び出すことができる。
      const int32 AmountOfFusaFusa = IHogeInterface::Execute_GetFusaFusa(this);
    }
  }
};

UPROPERTY を使わずにガベコレを防ぐ

これも UE 独自仕様。

UCLASS() class UMotiMoti : public UObject {
  GENERATED_BODY()

  // この TMap の UObject* がガベコレで勝手に回収されないようにしたい
  TMap<int32, UObject*> ObjectMap;
public:
  // static 関数なのにオーバーライドできる。
  // override キーワードは不要。
  static void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector) {
    UMotiMoti* MotiMoti = CastChecked<UMotiMoti>(InThis);
    if (!MotiMoti->ObjectMap.IsEmpty()) {
      Collector.AddReferencedObjects(MotiMoti->ObjectMap, InThis);
    }
  }
};

UObject 以外では FGCObject を使うことで同じことができます。

スポーンするアクターの名前の末尾にユニークな番号を付ける

#include “Runtime/Engine/Classes/Engine/World.h”

FName FActorSpawnUtils::MakeUniqueActorName(ULevel* Level, const UClass* Class, FName BaseName, bool bGloballyUnique)

const FName LocalUniqueName = FActorSpawnUtils::MakeUniqueActorName(
  nullptr, this->GetClass(), FName(TEXT("MyClass")), /* bGloballyUnique */false);

const FName GlobalUniqueName = FActorSpawnUtils::MakeUniqueActorName(
  nullptr, this->GetClass(), FName(TEXT("MyClass")), /* bGloballyUnique */true);

UE_LOG(LogTemp, Log, TEXT("LocalUniqueName=%s"), *LocalUniqueName.ToString());
UE_LOG(LogTemp, Log, TEXT("GlobalUniqueName=%s"), *GlobalUniqueName.ToString());

実行結果
LocalUniqueName=MyClass_0
GlobalUniqueName=MyClass_UAID_A8A1596CCE720A4202_1376017953

LocalUniqueName=MyClass_1
GlobalUniqueName=MyClass_UAID_A8A1596CCE720A4202_1392997954

UE_LOG で関数名を出力する

UE_LOG(LogTemp, Log, TEXT(“%hs”), __FUNCTION__);

ログカテゴリーを .cpp のみに定義する

DECLARE_LOG_CATEGORY_CLASS(Logなんとかかんとか, Log, All);

ログタイプを変更する(Log Verbose など)

cmd に以下を入力する

log ログカテゴリー ログタイプ

ログタイプのデフォルトはほとんどが Log ですが、Verbose に変更することがよくあります。

重み付きランダムの計算をしてくれる関数

GameplayStatics あたりにあっても良さそうなんですけどね…。
見当たらないので処理を作ります。

// 重みを設定した TArray<T> を用意しておき、以下の関数に引き渡すと
// 選んだ配列のインデックスを返す。エラー時は -1
// T は float, double, int32, uint8 などを想定。
template<class T>
int32 SelectIndexRandomlyWithConsideringWeight(const TArray<T>& InWeights) {
  // 重みの合計値を取得
  T TotalWeight = T();
  for (const T Weight : InWeights) { TotalWeight += Weight; }
  // 重みを考慮した乱数値を取得
  T SelectedWeight = FMath::RandRange(T(), TotalWeight);
  // 乱数値に対応する InWeights のインデックスを重みを考慮して返す
  int32 SelectedIndex = -1;
  T CurrentWeight = T();
  for (int32 Index = 0; Index < Weights.Num(); Index++) {
    CurrentWeight += Weights[Index];
    if (CurrentWeight > SelectedWeight) {
      SelectedIndex = Index;
      break;
    }
  }
  return SelectedIndex;
}

使用例

TArray<float> Weights;
Weights.Add(0.1f);
Weights.Add(0.5f);
Weights.Add(0.2f);
Weights.Add(0.3f);
const int32 SelectedIndex = SelectIndexRandomlyWithConsideringWeight<float>(Weights);
if (SelectedIndex < 0) { /* エラー */ }
// Weights[SelectedIndex] に対応するデータを取得
const なにかのでーたー& Data = なにかのでーたー配列[SelectedIndex];

※注意
Weights と「なにかのでーたー配列」の要素数は同じで、要素は1対1で対応している必要があります。
Weight[n] に設定した重みは、なにかのでーたー配列[n] のものである…ということです。

実際の運用では以下のような構造体を定義して、Weight とデータをひとつの構造体で管理する形になると思います。
関数を呼び出す際は、構造体から重みだけ抽出したものを関数に渡すことになります。

struct なにかのでーたー構造体 {
  float Weight{0.0f};
  なにかのでーたー Data{};
};
TArray<なにかのでーたー構造体> なにかのでーたー配列;

// なにかのでーたー構造体の Weight を Weights に追加
TArray<float> Weights;
Weights.Reserve(なにかのでーたー配列.Num());
for (const なにかのでーたー構造体& Nani : なにかのでーたー配列) {
  Weights.Add(Nani.Weight);
}

const int32 SelectedIndex = SelectIndexRandomlyWithConsideringWeight<float>(Weights);
const なにかのでーたー構造体& Nanika = なにかのでーたー配列[SelectedIndex];
// Nanika.Data

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です