シーケンサーに公開可能なプロパティの条件

[UE4][C++]シーケンサーで非対応のパラメータを制御可能にする方法|株式会社ヒストリア

ヒストリアさんの記事に便乗して補足記事でも。
(いずれ書こうと思っていたネタだったが先を越されてしまった・・・)

C++ での実装箇所

ヒストリアさんの記事でも書いてあるが、公開プロパティ判定は FSequencerObjectChangeListener::FindPropertySetter で処理されている。
その関数をより具体的に見ていく(ソースコードは UE4.18.3 時点)

const FOnAnimatablePropertyChanged* FSequencerObjectChangeListener::FindPropertySetter(const UStruct& PropertyStructure, FAnimatedPropertyKey PropertyKey, const UProperty& Property) const
{
const FOnAnimatablePropertyChanged* DelegatePtr = PropertyChangedEventMap.Find(PropertyKey);
if (DelegatePtr != nullptr)
{
FString PropertyVarName = Property.GetName();

// If this is a bool property, strip off the 'b' so that the "Set" functions to be
// found are, for example, "SetHidden" instead of "SetbHidden"
if (PropertyKey.PropertyTypeName == "BoolProperty")
{
PropertyVarName.RemoveFromStart("b", ESearchCase::CaseSensitive);
}

static const FString Set(TEXT("Set"));

const FString FunctionString = Set + PropertyVarName;

FName FunctionName = FName(*FunctionString);

static const FName DeprecatedFunctionName(TEXT("DeprecatedFunction"));
UFunction* Function = nullptr;
if (const UClass* Class = Cast<const UClass>(&PropertyStructure))
{
Function = Class->FindFunctionByName(FunctionName);
}
bool bFoundValidFunction = false;
if (Function && !Function->HasMetaData(DeprecatedFunctionName))
{
bFoundValidFunction = true;
}

bool bFoundValidInterp = false;
bool bFoundEditDefaultsOnly = false;
bool bFoundEdit = false;

if (Property.HasAnyPropertyFlags(CPF_Interp))
{
bFoundValidInterp = true;
}

// @TODO: should we early out of our property path iteration if we find an "edit defaults only" property?
if (Property.HasAnyPropertyFlags(CPF_DisableEditOnInstance))
{
bFoundEditDefaultsOnly = true;
}
if (Property.HasAnyPropertyFlags(CPF_Edit))
{
bFoundEdit = true;
}

const bool bIsHiddenFunction = IsHiddenFunction(PropertyStructure, FAnimatedPropertyKey::FromProperty(&Property), Property.GetName());

// Valid if there's a setter function and the property is editable. Also valid if there's an interp keyword.
if (((bFoundValidFunction && bFoundEdit && !bFoundEditDefaultsOnly) || bFoundValidInterp) && !bIsHiddenFunction)
{
return DelegatePtr;
}
}

return nullptr;
}

このコードから以下のことが読み取れる。

175-200 行目: FoundValidFunction

「”Set” + プロパティ名」がついた関数をそのプロパティの Setter とみなし、その対象となる UFUNCTION 関数が存在するかどうか。

  • このとき bool プロパティのみ、頭文字 “b” があったらそれをカットしたプロパティ名で検索される
    • (UE4 では bool プロパティは変数名に接頭語 “b” をつけることが通例)
  • また、メタに “DeprecatedFunction” がついている場合、つまり非推奨関数も検索対象外とされる

206-209 行目: FoundValidInterp

プロパティに CPF_Interp フラグがついているかどうか。

  • つまり、UPROPERTY 指定子 Interp が書かれていること
  • ブループリントでは、Expose to Cinematics にチェックが入っていること

212-214 行目: FoundEditDefaultOnly

プロパティに CPF_DisableEditOnInstance フラグがついているかどうか。

  • つまり、UPROPERTY 指定子 EditDefaultsOnly もしくは VisibleDefaultsOnly が書かれていること
CPF と UPROPERTY 指定子の関係は、HeaderParser.cpp の 3134 行目あたりを参考にするとよい。
また、CPF 自体はただの #define 文であり、その宣言は ObjectMacros.h に全て記載されている。

216-219 行目: FoundEdit

プロパティに CPF_Edit フラグがついているかどうか。

  • つまり、UPROPERTY 指定子に以下のいずれかが書かれていること
    • EditAnywhere
    • EditInstanceOnly
    • EditDefaultsOnly
    • VisibleAnywhere
    • VisibleInstanceOnly
    • VisibleDefaultsOnly

221 行目: IsHiddenFunction

IsHiddenFunction の具体的な実装はこんな感じ。
(内部で呼ばれてる GetFunctionName がさっきと同じコード書いていてすこぶるダサい・・・)

FName GetFunctionName(FAnimatedPropertyKey PropertyKey, const FString& InPropertyVarName)
{
FString PropertyVarName = InPropertyVarName;

// If this is a bool property, strip off the 'b' so that the "Set" functions to be
// found are, for example, "SetHidden" instead of "SetbHidden"
if (PropertyKey.PropertyTypeName == "BoolProperty")
{
PropertyVarName.RemoveFromStart("b", ESearchCase::CaseSensitive);
}

static const FString Set(TEXT("Set"));

const FString FunctionString = Set + PropertyVarName;

FName FunctionName = FName(*FunctionString);

return FunctionName;
}

bool IsHiddenFunction(const UStruct& PropertyStructure, FAnimatedPropertyKey PropertyKey, const FString& InPropertyVarName)
{
FName FunctionName = GetFunctionName(PropertyKey, InPropertyVarName);

static const FName HideFunctionsName(TEXT("HideFunctions"));
bool bIsHiddenFunction = false;
TArray<FString> HideFunctions;
if (const UClass* Class = Cast<const UClass>(&PropertyStructure))
{
Class->GetHideFunctions(HideFunctions);
}

return HideFunctions.Contains(FunctionName.ToString());
}

そのプロパティの Setter が、それを所有するクラスの UCLASS の HideFunctions に指定されているかどうかを確認している。

公開可能プロパティの条件

以上のフラグを使用した条件式

if (((bFoundValidFunction && bFoundEdit && !bFoundEditDefaultsOnly) || bFoundValidInterp) && !bIsHiddenFunction)

より、以下の公開可能プロパティの条件が導き出される。

Set + プロパティ名を持つ関数(bool プロパティは頭に “b” があった場合、それを取り除いた名前)を Setter とする。
このとき、以下の 2 つの条件を両方満たすとき、そのプロパティはシーケンサーで公開される。

【条件1】 以下の a, b のうちどちらかを満たす
a. 非推奨関数でない Setter が存在し、以下のいずれかの UPROPERTY 指定子を持つこと
EditAnywhere, EditInstanceOnly, VisibleAnywhere, VisibleInstanceOnly
b. UPROPERTY 指定子 Interp がついていること

【条件2】対象の Setter が存在する場合、そのプロパティを所有するクラスが HideFunctions に指定していないこと

具体例

UCLASS(HideFunctions=(SetEditAnywhere3, SetEditInstanceOnly3))
class ATestSequencerActor : public AActor
{
GENERATED_BODY()

public:

// CPF_Edit だが Setter がないので NG
UPROPERTY(EditAnywhere)
bool bEditAnywhere = false;

// CPF_Edit だが Setter がないので NG
UPROPERTY(EditInstanceOnly)
bool bEditInstanceOnly = false;

// CPF_Edit だが Setter がないので NG
UPROPERTY(VisibleAnywhere)
bool bVisibleAnywhere = false;

// CPF_Edit だが Setter がないので NG
UPROPERTY(VisibleInstanceOnly)
bool bVisibleInstanceOnly = false;

////

// CPF_Edit で Setter もあるので OK
UPROPERTY(EditAnywhere)
bool bEditAnywhere2 = false;
UFUNCTION(BlueprintCallable)
void SetEditAnywhere2(bool value)
{
bEditAnywhere2 = value;
}

// CPF_Edit で Setter もあるので OK
UPROPERTY(EditInstanceOnly)
bool bEditInstanceOnly2 = false;
UFUNCTION(BlueprintCallable)
void SetEditInstanceOnly2(bool value)
{
bEditInstanceOnly2 = value;
}

// CPF_Edit で Setter もあるので OK
UPROPERTY(VisibleAnywhere)
bool bVisibleAnywhere2 = false;
UFUNCTION(BlueprintCallable)
void SetVisibleAnywhere2(bool value)
{
bVisibleAnywhere2 = value;
}

// CPF_Edit で Setter もあるので OK
UPROPERTY(VisibleInstanceOnly)
bool bVisibleInstanceOnly2 = false;
UFUNCTION(BlueprintCallable)
void SetVisibleInstanceOnly2(bool value)
{
bVisibleInstanceOnly2 = value;
}

////

// CPF_Edit だが Setter が HideFunction なので NG
UPROPERTY(EditAnywhere)
bool bEditAnywhere3 = false;
UFUNCTION(BlueprintCallable)
void SetEditAnywhere3(bool value)
{
bEditAnywhere3 = value;
}

// CPF_Edit だが Setter が HideFunction なので NG
UPROPERTY(EditInstanceOnly)
bool bEditInstanceOnly3 = false;
UFUNCTION(BlueprintCallable)
void SetEditInstanceOnly3(bool value)
{
bEditInstanceOnly3 = value;
}

// CPF_Edit だが Setter が非推奨関数なので NG
UPROPERTY(VisibleAnywhere)
bool bVisibleAnywhere3 = false;
UFUNCTION(BlueprintCallable, meta = (DeprecatedFunction))
void SetVisibleAnywhere3(bool value)
{
bVisibleAnywhere3 = value;
}

// CPF_Edit だが Setter が非推奨関数なので NG
UPROPERTY(VisibleInstanceOnly)
bool bVisibleInstanceOnly3 = false;
UFUNCTION(BlueprintCallable, meta = (DeprecatedFunction))
void SetVisibleInstanceOnly3(bool value)
{
bVisibleInstanceOnly3 = value;
}

////

// CPF_Interp なので OK
UPROPERTY(Interp)
bool bInterp = false;

////

// CPF_Interp でも CPF_Edit でもないので NG
UPROPERTY(BlueprintReadOnly)
bool bBlueprintReadOnly = false;

// CPF_Interp なので OK
UPROPERTY(BlueprintReadOnly, Interp)
bool bBlueprintReadOnly_Interp = false;

// CPF_Edit だが Setter がないので NG
UPROPERTY(BlueprintReadOnly, EditAnywhere)
bool bBlueprintReadOnly_EditAnywhere = false;

// CPF_Edit で Setter もあるので OK
UPROPERTY(BlueprintReadOnly, EditAnywhere)
bool bBlueprintReadOnly_EditAnywhere2 = false;
UFUNCTION(BlueprintCallable)
void SetBlueprintReadOnly_EditAnywhere2(bool value)
{
bBlueprintReadOnly_EditAnywhere2 = value;
}

};

実行結果はこんな感じ。

最後の方に書いたように、 BlueprintReadOnly, Interp の組み合わせの場合、ブループリントでは Getter しか提供されていないが、シーケンサー上では上書き可能、という特殊なプロパティも設定ができる。
シーケンサーからの更新だけを保証した上で、ブループリントの内部処理を変更したい場合などではとても有用だと思う。