【UE5】Serialize関数を活用してUObjectプロパティのデータ移行を行う
はじめに
UnrealEngineでは、UObject のプロパティ名が変更された際に、古いプロパティの値を維持しつつ新しいプロパティへ移行できる CoreRedirect という便利な機能があります。
しかし、この機能は万能ではなく、対応できないケースが存在します。
よくある例として、仕様変更により、プロパティの型を 単一の値から配列や複雑な構造 に変更する必要が生じる場合が挙げられます。
CoreRedirect で対応できない例
protected:
// int32 -> TArray<int32> へと移行したいが...
// CoreRedirect の機能では対応できない!
/** 廃止予定のプロパティ */
UPROPERTY(EditAnywhere)
int32 OldProperty;
/** 移行先のプロパティ */
UPROPERTY(EditAnywhere)
TArray<int32> NewProperty;
このようなケースでは、単なる名前のリダイレクトに留まらず、プロパティ間でデータを変換する処理が必要となります。CoreRedirect だけでは対処できないため、他の手段を検討しなければなりません。
そこで本記事では、UObject::Seriazlie()
を利用して、より柔軟にプロパティの移行を行う方法について解説します。
解説
UObject::Serialize()
は UObject の派生クラスでデータの読み書きをカスタマイズするためにオーバーライド可能な関数です。
この関数は、データの保存や読み込みが行われる直前直後のタイミングで自動的に呼び出されるため、特殊な処理を挟むのに非常に都合が良いです。
そのため、今回のようなプロパティ間のデータ移行にも最適です。
以下は最もシンプルなプロパティ間のデータ移行の実装例です。
Serialize()
をオーバーライドして、読み込み時に古いプロパティの値を新しいプロパティに移行しています。
virtual void Serialize(FArchive& Ar) override
{
Super::Serialize(Ar);
if (Ar.IsLoading())
{
if (OldProperty != INDEX_NONE)
{
// int32 -> TArray<int32>
NewProperty = { OldProperty };
OldProperty = INDEX_NONE;
}
}
};
実践例: レベル上に配置されたActorのプロパティ移行
Serialize()
を活用したプロパティ移行の基本が分かったところで、本章ではより具体的な例を通じて、プロパティ移行の手順を解説します
ここでは、レベル上に配置された Actor のプロパティを移行する方法を詳しく見ていきます。
1. 準備
1-1. AActor の派生クラスの作成する
プロパティの移行をテストするための Actor の派生クラス、 MyActor を作成します。
ここではプロパティの定義のみを行い、データ移行の処理はまだ実装しません。
MyActor.h
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
AMyActor()
: OldProperty(INDEX_NONE)
{
}
protected:
/** 廃止予定のプロパティ */
UPROPERTY(EditAnywhere)
int32 OldProperty;
/** 移行先のプロパティ */
UPROPERTY(VisibleAnywhere)
TArray<int32> NewProperty;
public:
virtual void Serialize(FArchive& Ar) override
{
Super::Serialize(Ar);
}
};
1-2. レベル上に Actor を配置して、移行前のプロパティに値を設定する
次に、作成した MyActor をレベル上に配置します。配置した MyActor を アウトライナー から選択し、詳細パネルで OldProperty
の値を変更します。
例として今回は OldProperty
を 255
に設定します。
設定が完了したら、レベルを保存しておきます。これでプロパティ移行に向けた準備が整いました。
2. データ移行処理の実装
準備ができたところで、次は MyActor の Seriazlie()
をオーバーロードして、プロパティ移行処理を実装します。
以下のコードでは、前の章で示した実装例を軸に、より実践的な実装を行っています。
MyActor.h
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
AMyActor()
: OldProperty(INDEX_NONE)
{
}
protected:
/** 廃止予定のプロパティ */
UPROPERTY(VisibleAnywhere)
int32 OldProperty;
/** 移行先のプロパティ */
UPROPERTY(VisibleAnywhere)
TArray<int32> NewProperty;
public:
virtual void Serialize(FArchive& Ar) override
{
// 読み書き両対応する場合は, 親クラスの関数呼び出し位置が異なることに注意!!
if (Ar.IsCooking())
{
RedirectProperty();
}
if (Ar.IsSaving())
{
// OldProperty に Transient 属性を動的に付与して, 以降のシリアライズを抑制 (不要なデータを書き込みさせない)
// UPROPERTY(Transient)
FindFieldChecked<FIntProperty>(StaticClass(), TEXT("OldProperty"))
->SetPropertyFlags(CPF_Transient);
}
Super::Serialize(Ar);
if (Ar.IsLoading())
{
RedirectProperty();
}
}
protected:
/** プロパティ移行 */
void RedirectProperty()
{
if (OldProperty != INDEX_NONE)
{
// int32 -> TArray<int32>
NewProperty = { OldProperty };
OldProperty = INDEX_NONE;
// 警告ログをEditorのメッセージログウィンドウに出力
{
TObjectPtr<UPackage> Package = GetPackage();
FString Log = FString::Printf(
TEXT("%s - 廃止されたプロパティの移行が完了していません. レベルを再保存してください. (%s)"),
*GetActorLabel(), *Package.GetName());
FMessageLog MessageLog("MapCheck");
MessageLog.Warning(FText::FromString(Log));
MessageLog.Open();
}
}
}
};
差分
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
AMyActor()
: OldProperty(INDEX_NONE)
{
}
protected:
/** 廃止予定のプロパティ */
- UPROPERTY(EditAnywhere)
+ UPROPERTY(VisibleAnywhere)
int32 OldProperty;
/** 移行先のプロパティ */
UPROPERTY(VisibleAnywhere)
TArray<int32> NewProperty;
public:
virtual void Serialize(FArchive& Ar) override
{
+ // 読み書き両対応する場合は, 親クラスの関数呼び出し位置が異なることに注意!!
+
+ if (Ar.IsCooking())
+ {
+ RedirectProperty();
+ }
+
+ if (Ar.IsSaving())
+ {
+ // OldProperty に Transient 属性を動的に付与して, 以降のシリアライズを抑制 (不要なデータを書き込みさせない)
+ // UPROPERTY(Transient)
+ FindFieldChecked<FIntProperty>(StaticClass(), TEXT("OldProperty"))
+ ->SetPropertyFlags(CPF_Transient);
+ }
+
Super::Serialize(Ar);
+
+ if (Ar.IsLoading())
+ {
+ RedirectProperty();
+ }
}
+
+protected:
+ /** プロパティ移行 */
+ void RedirectProperty()
+ {
+ if (OldProperty != INDEX_NONE)
+ {
+ // int32 -> TArray<int32>
+ NewProperty = { OldProperty };
+ OldProperty = INDEX_NONE;
+
+ // 警告ログをEditorのメッセージログウィンドウに出力
+ {
+ TObjectPtr<UPackage> Package = GetPackage();
+ FString Log = FString::Printf(
+ TEXT("%s - 廃止されたプロパティの移行が完了していません. レベルを再保存してください. (%s)"),
+ *GetActorLabel(), *Package.GetName());
+
+ FMessageLog MessageLog("MapCheck");
+ MessageLog.Warning(FText::FromString(Log));
+ MessageLog.Open();
+ }
+ }
+ }
};
3. プロパティのデータ移行
3-1. 移行されたデータの確認
先ほど MyActor を配置したレベルを再び開きます。
詳細パネル から MyActor のプロパティを確認すると、OldProperty
の値がリセットされ、代わりに NewProperty
の要素に、先程設定した 255
が追加されていることが確認できました。
3-2. レベルの再保存
ところで、レベルを開いた際に、メッセージログに「廃止されたプロパティの移行が完了していません. レベルを再保存してください.
」という警告が表示されました。
このメッセージは、先ほど Serialize()
に仕込んでいたものです。警告に従い、レベルの再保存をします。
3-3. 古いプロパティがシリアライズされていないことを確認
レベルを再度開くと、先ほど表示されていた警告が消えたことが確認できました。
これにより、古いプロパティがシリアライズされておらず、アセットに残っていないことが確認できました。
以上で、プロパティのデータ移行処理が無事に完了しました。
おわりに
仕様変更や設計変更に伴うプロパティの移行は、残念ながらゲーム開発において避けて通れない作業の一つと言っても過言ではないでしょう。
適切なデータの移行方法を知っておくと、データの設定し直しによる作業の巻き戻りが発生することなく、スムーズに開発の進行できるはずです。
本記事がその助けになれば幸いです。
Discussion