ほげたつブログ

UE4とアニメーションをかじって生きてる

量産されたアセットのプロパティ仕様を変更する

UE4ではC++で定義されたクラスをBlueprintとして継承し、プロパティの調整やロジックの拡張等を行ったものをアセットとして量産することが可能です。

しかし開発が進み、アセットが増えてくるとアセットを仕様変更する際の修正コストが大きくなってきます。今回はそのような場合にラクをする方法を紹介します。


問題となるケース

例えば技クラスを作り、プロパティでヒットストップ時間を秒単位(float)で指定できるようにしたとします。

#pragma once

#include "CoreMinimal.h"
#include "Components/SceneComponent.h"
#include "AbilityComponent.generated.h"

UCLASS(Blueprintable, BlueprintType, meta=(BlueprintSpawnableComponent))
class MYPROJECT1_API UAbilityComponent : public USceneComponent
{
    GENERATED_BODY()

public:
    /** ダメージ */
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Damage;

    /** ヒットストップ時間(秒) */
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float HitStopTime;

public:    
    UAbilityComponent();
};


これを Blueprint で継承することで、以下のようなアセットを量産することが可能です。

f:id:hogetatu:20180221002400p:plain


この状態で開発を進めていたところ、やはり秒単位での設定は作業者に余計な負担をかけるため、フレーム数単位(int)での設定に切り替えたい という話が出たとします。しかし既にアセットは量産されており、単純にプロパティ仕様を変更すると大量のアセットに対してヒットストップ時間の再設定が必要になります。そういった場面で便利なのが アセット単位でカスタムバージョンを定義する という手法です。


カスタムバージョン対応方法

Actor や Component 等の UObject を継承したクラスは、読み込みタイミングでシリアライズ(UObject::Serialize)が実行されます。デフォルト定義のシリアライズだと UPROPERTY 宣言したプロパティをセーブ / ロードするのみの動作となりますが、Serialize 関数を override することで拡張して利用することも可能です。カスタムバージョン対応ではシリアライズ時にバージョン情報をアセットに埋め込みます。

バージョン情報の定義

struct FAbilityComponentCustomVersion
{
    enum Type
    {
        // ヒットストップを時間指定からフレーム数指定に変更
        ChangeFromHitStopTimeToHitStopFrame = 0,

        // -----<バージョン定義はこのラインより上に定義>-------------------------------------------------
        VersionPlusOne,
        LatestVersion = VersionPlusOne - 1
    };

    // The GUID for this custom version number
    const static FGuid GUID;
};

/** カスタムバージョンを管理するための GUID */
const FGuid FAbilityComponentCustomVersion::GUID(0xbbd7d3c8, 0x272e51bf, 0xf9f327ea, 0x686c53ae);

/** カスタムバージョンクラス */
FCustomVersionRegistration GRegisterAbilityComponentCustomVersion(FAbilityComponentCustomVersion::GUID, FAbilityComponentCustomVersion::LatestVersion, TEXT("AbilityComponent"));

シリアライズ

void UAbilityComponent::Serialize(FArchive& Ar)
{
    Super::Serialize(Ar);

    // バージョン情報を記録
    Ar.UsingCustomVersion(FAbilityComponentCustomVersion::GUID);
}


これでアセット保存時にバージョン情報が記録されるようになるので、次はアセットの読み込みが完了したタイミングでバージョン情報を元に変換処理を実行します。

読み込み完了時

void UAbilityComponent::PostLoad()
{
    Super::PostLoad();

    // 読み込んだアセットのカスタムバージョンが低ければヒットストップ時間をフレーム数に変換
    if (GetLinkerCustomVersion(FAbilityComponentCustomVersion::GUID) < FAbilityComponentCustomVersion::ChangeFromHitStopTimeToHitStopFrame)
    {
        HitStopFrameCount = FMath::RoundToInt(HitStopTime_DEPRECATED * 60.f);
    }
}

※ HitStopTime は今後利用しないため、プロパティ名に "_DEPRECATED" を足しています。DEPRECATED キーワードが付いたプロパティはシリアライズ時に読み込みはされますが、保存はされません。


まとめると以下のような構成になります。

header

#pragma once

#include "CoreMinimal.h"
#include "Components/SceneComponent.h"
#include "AbilityComponent.generated.h"

UCLASS(Blueprintable, BlueprintType, meta=(BlueprintSpawnableComponent))
class MYPROJECT1_API UAbilityComponent : public USceneComponent
{
    GENERATED_BODY()

public:
    /** ダメージ */
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Damage;

    /** ヒットストップ時間(秒) ※旧仕様 */
    UPROPERTY()
    float HitStopTime_DEPRECATED;

    /** ヒットストップ時間(フレーム数) */
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    int32 HitStopFrameCount;

public:    
    UAbilityComponent();

    // Begin UObject Interface.
    virtual void Serialize(FArchive& Ar) override;
    virtual void PostLoad() override;
    // End UObject Interface.
};

cpp

#include "AbilityComponent.h"
#include "Serialization/CustomVersion.h"

struct FAbilityComponentCustomVersion
{
    enum Type
    {
        // ヒットストップを時間指定からフレーム数指定に変更
        ChangeFromHitStopTimeToHitStopFrame = 0,

        // -----<バージョン定義はこのラインより上に定義>-------------------------------------------------
        VersionPlusOne,
        LatestVersion = VersionPlusOne - 1
    };

    /** カスタムバージョンを管理するための GUID */
    const static FGuid GUID;
};

/** カスタムバージョンを管理するための GUID */
const FGuid FAbilityComponentCustomVersion::GUID(0xbbd7d3c8, 0x272e51bf, 0xf9f327ea, 0x686c53ae);

/** カスタムバージョンクラス */
FCustomVersionRegistration GRegisterAbilityComponentCustomVersion(FAbilityComponentCustomVersion::GUID, FAbilityComponentCustomVersion::LatestVersion, TEXT("AbilityComponent"));

///////////////////////////////////////////////////////////////////////////////////////////////////////////

UAbilityComponent::UAbilityComponent()
    : Damage(0.f)
    , HitStopFrameCount(0)
{
}

/** シリアライズ */
void UAbilityComponent::Serialize(FArchive& Ar)
{
    Super::Serialize(Ar);

    // バージョン情報を記録
    Ar.UsingCustomVersion(FAbilityComponentCustomVersion::GUID);
}

/** 読み込み完了時 */
void UAbilityComponent::PostLoad()
{
    Super::PostLoad();

    // 読み込んだアセットのカスタムバージョンが低ければヒットストップ時間をフレーム数に変換
    if (GetLinkerCustomVersion(FAbilityComponentCustomVersion::GUID) < FAbilityComponentCustomVersion::ChangeFromHitStopTimeToHitStopFrame)
    {
        HitStopFrameCount = FMath::RoundToInt(HitStopTime_DEPRECATED * 60.f);
    }
}


この状態でエディタを起動し、作成済みのアセットを開くと以下のような結果となります。

f:id:hogetatu:20180221020339p:plain


HitStopTime として 0.4 秒を指定していた部分が HitStopFrameCount : 24 フレームに自動で置き換わっています。これで開発終盤での急な仕様変更にも耐えられますね()