UE5 C++におけるゲーム内アイテムデータ管理とDataTable連携の実装

Unreal Engine 5 でゲーム内のアイテムやリソースデータを効率的に管理するためには、C++ 側で反射システム(Reflection)を有効にしたデータ構造を設計し、エディタ上のデータテーブルと連携させる必要があります。本記事では、アイテム情報を保持する構造体から、それらをゲーム全体で共有するデータ管理クラスまでの実装手順を解説します。

1. 基底クラスとデータ構造体の定義

アイテムの元データはシーン内に配置する必要がないため、UObject を継承した C++ クラスを作成します。エディタのコンテンツブラウザから「新規 C++ クラス」を選択し、親クラスに Object、アクセス修飾子を「Public」に設定します。

生成されたヘッダーファイルでは、UCLASS マクロを記述してリフレクションシステムへクラスを登録します。GENERATED_BODY() は必須の初期化用マクロであり、クラス定義の先頭に配置します。

UCLASS(Blueprintable, BlueprintType)
class UItemRegistry : public UObject
{
    GENERATED_BODY()
public:
    // メンバ変数やメソッドの宣言
};

次に、個々のアイテムを構成するパラメータを格納するための構造体を定義します。データテーブルの行(Row)として使用するには、構造体が FTableRowBase を継承している必要があります。また、リフレクション対象の構造体名は F から始まる命名規則に従い、USTRUCT(BlueprintType) でマークアップします。

USTRUCT(BlueprintType)
struct FItemDataRow : public FTableRowBase
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Basic Info")
    FName ItemIdentifier = TEXT("None");

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Basic Info")
    FText LocalizedName = FText::GetEmpty();

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Properties")
    EItemAttribute ItemAttribute = EItemAttribute::Null;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Properties")
    int32 RarityLevel = 0;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory")
    bool bIsStackable = false;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory")
    int32 MaxStackSize = 1;

    FItemDataRow() = default;
};

UPROPERTY の指定子について補足すると、EditAnywhere はデフォルト値とインスタンス詳細の両方で編集可能、BlueprintReadWrite はブループリントでの読み書きを許可します。Category を利用すると、エディタ上の詳細パネルやブループリントノードで変数が整理されて表示されます。

2. Unreal Engine 固有のデータ型とコンテナ

文字列型の違いと相互変換

UE5 には FStringFTextFName の 3 種類が標準で提供されています。

  • FString: 汎用的な文字列管理。内部は wchar_t ベースで、演算子 + による連結が容易。
  • FText: ローカライズ対応用。プレイヤーへ直接表示するテキストに使用し、DataTable やローカライゼーションデータベースと連動させるのが基本。
  • FName: ハッシュ化された不変文字列。検索や TMap のキーとして高速に動作するため、ID やカテゴリ名の管理に適しています。

型変換は FString を中継点として行うのが一般的です。

FString TempString;
FText LocalizedDesc = FText::FromString(TEXT("Description"));
FName ItemID = TEXT("Sword_01");

// FText / FName → FString
TempString = LocalizedDesc.ToString();
TempString = ItemID.ToString();

// FString → FName / FText
ItemID = FName(*TempString);
LocalizedDesc = FText::FromString(TempString);

// FString ↔ std::string (C++11 標準ライブラリとの連携)
std::string StdStr = TCHAR_TO_UTF8(*TempString);
TempString = UTF8_TO_TCHAR(StdStr.c_str());

動的配列と連想コンテナ

メモリ管理や境界チェックが強化された UE 標準コンテナ TArrayTMap を活用します。

TArray<int32> ValueList;
ValueList.Add(10);
ValueList.Add(20);

// 条件に一致する要素を検索(ポインタで返却)
const int32* FoundValue = ValueList.FindByPredicate([](const int32& Val) {
    return Val > 15;
});
if (FoundValue) {
    UE_LOG(LogTemp, Warning, TEXT("Found: %d"), *FoundValue);
}

// TMap は FName をキーにすると検索性能が最適化される
TMap ItemStatsMap;
ItemStatsMap.Add(TEXT("AttackPower"), 150);
if (ItemStatsMap.Contains(TEXT("AttackPower"))) {
    ItemStatsMap.FindOrAdd(TEXT("AttackPower")) += 50;
}

3. 列挙型とスマートポインタの設計

ゲーム内のカテゴリや属性は UENUM マクロで定義し、ブループリントでも型として認識できるようにします。

UENUM(BlueprintType)
enum class EItemAttribute : uint8
{
    Null    UMETA(DisplayName = "未設定"),
    Fire    UMETA(DisplayName = "炎"),
    Ice     UMETA(DisplayName = "氷"),
    Lightning UMETA(DisplayName = "雷"),
    Nature  UMETA(DisplayName = "自然"),
};

オブジェクトの寿命管理には TSharedPtr<T> を採用します。参照カウント方式であり、コンテナや非 UPROPERTY なメンバ変数との親和性が高く、ポインタ演算(*->)も標準通り動作します。

TSharedPtr<FItemDataRow> ItemDataPtr = MakeShared<FItemDataRow>();
if (ItemDataPtr.IsValid()) {
    ItemDataPtr->RarityLevel = 3;
}

4. DataTable の動的読み込みとゲームインスタンスへの格納

エディタ上で定義したアイテム一覧は、実行時に UDataTable 経由で読み出し、ゲーム全体で参照可能な UGameInstance にキャッシュするのが効率的な構成です。まずは UGameInstance を継承したマネージャークラスを定義します。

UCLASS()
class UGameDataInstance : public UGameInstance
{
    GENERATED_BODY()
public:
    // アイテムデータの一覧を保持するマップ
    TMap ItemRegistry;

    // DataTable からデータを読み込む処理
    void LoadItemDatabase();
};

実装ファイル(.cpp)では、アセットパスを指定して DataTable を動的にロードし、各行のデータを取得してマップへ登録します。

void UGameDataInstance::LoadItemDatabase()
{
    // 指定パスの DataTable アセットをロード
    FString AssetPath = TEXT("DataTable'/Game/Data/Items_Master.Items_Master'");
    UDataTable* SourceTable = LoadObject<UDataTable>(nullptr, *AssetPath);

    if (!SourceTable) {
        UE_LOG(LogTemp, Error, TEXT("Failed to load Item Database DataTable."));
        return;
    }

    ItemRegistry.Empty();

    // テーブルの全行名(キー)を取得
    const TArray<FName>& RowKeys = SourceTable->GetRowNames();

    for (const FName& RowKey : RowKeys) {
        FString Context;
        // 行データとして構造体を取得(キャストは内部で自動処理)
        const FItemDataRow* FoundRow = SourceTable->FindRow<FItemDataRow>(RowKey, Context);

        if (FoundRow) {
            // データをコピーしてスマートポインタで管理し、マップへ格納
            TSharedPtr<FItemDataRow> DataCopy = MakeShared<FItemDataRow>(*FoundRow);
            ItemRegistry.Add(DataCopy->ItemIdentifier, DataCopy);
        }
    }

    UE_LOG(LogTemp, Warning, TEXT("Successfully loaded %d items into registry."), ItemRegistry.Num());
}

UGameInstance はゲームセッションの開始から終了まで存続するため、読み込んだデータをここでキャッシュしておけば、レベル遷移やシーンの切り替え時に都度アセットをディスクから読み込むオーバーヘッドを回避できます。これにより、インベントリシステムやクラフト処理などの後続コンポーネントが、高速なメモリアクセスを通じてアイテム情報を参照できるようになります。

タグ: UnrealEngine5 C++ UDataTable UGameInstance リフレクション

5月16日 03:51 投稿