LoginSignup
12
5

More than 3 years have passed since last update.

UnrealC++で自作アニメーションノードを作る

Last updated at Posted at 2019-12-06

はじめに

最近、アニメーションノードを自作する機会がありいくつか作ってみたのですが、自作アニメーションノードについての情報が少なくて困ったので、自作アニメーションノードについて記事を書いてみようと思います。
UE4のバージョンは4.22です。

この記事を読む前にこちらの記事に目を通すことをおすすめします。

[UE4] モジュールについて|株式会社ヒストリア

受け取ったポースを返すだけのアニメーションノードを作ってみた

アニメーションノードをまとめたアニメーションノードを作ってみた

つくるもの

1.PNG
2.PNG
今回作成するノードは、Animation Assetの項目で指定した3つのアニメーションを列挙型を用いて切り替え、BoneToModifyで指定したボーン以下のみに適用します。また、アニメーションの切り替えは滑らかに遷移するようにし、Stateの項目のInterp Speedで補間速度を指定できる(~Interpノードと同じ)ようにします。

記事が長くなってしまったので要点を「おわりに」の「まとめ」の項目にまとめました。忙しい方はそちらをご参照ください。

つくってみる

まず、アニメーションノードを作成するにはランタイムモジュールにFAnimNode_Baseを継承した実装部分のクラス、エディタモジュールにUAnimGraphNode_Baseを継承したエディタ上でのノードの見た目を担当するクラスを定義する必要があります。また、今回作成するノードでは内部でのみ使用しブループリントに公開しない実装クラスを定義します。
AnimNode_MyBlendAnim.h/cppとAnimNode_MyBlendAnimInternal.h/cppはエディタから作成できないので、VisualStudio上でファイルを作成します。AnimGraphNode_MyBlendAnim.h/cppはエディタから作成できるのでAnimGraphNode_Baseを継承したクラスをエディタモジュールに作成します。
また、アニメーションノードの開発する際にはコードエディタのSolution configurationをDevelopment Editorにしておく必要があります。
Visual Studioでは下の画像で囲ってある部分で変更できます。
6.PNG

FAnimNode_MyBlendAnimInternal

まずは内部で使用するノードを作っていきます。このノードでは指定された割合で3つのアニメーションをブレンドする部分を実装します。アニメーションBPで作ると以下のような処理になります。
3.png

AnimNode_MyBlendAnimInternal.h
#pragma once

#include "Animation/AnimNodeBase.h"
#include "Animation/AnimNode_SequencePlayer.h"
#include "AnimationRuntime.h"
#include "AnimNode_MyBlendAnimInternal.generated.h"

USTRUCT(BlueprintInternalUseOnly)
struct CUSTOMANIMNODE_API FAnimNode_MyBlendAnimInternal : public FAnimNode_Base
{
    GENERATED_USTRUCT_BODY()

public:
    UAnimSequence* Animation_1;
    UAnimSequence* Animation_2;
    UAnimSequence* Animation_3;

    float IsState2 = 0.f;
    float IsState3 = 0.f;

private:
    FAnimNode_SequencePlayer AnimNode_1;
    FAnimNode_SequencePlayer AnimNode_2;
    FAnimNode_SequencePlayer AnimNode_3;

public:
    FAnimNode_MyBlendAnimInternal();

    virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override;
    virtual void Update_AnyThread(const FAnimationUpdateContext& Context) override;
    virtual void Evaluate_AnyThread(FPoseContext& Output) override;
};

まず最初にPlay~ノードに当たるFAnimNode_SequencePlayerを使うためにAnimNode_SequencePlayer.hを、Blendノードに当たるBlendTwoPosesTogether関数を使うためにAnimationRuntime.hをインクルードします。
アニメーションアセットのUAnimSequence型で3つ変数を用意します。IsState2とIsState3はアニメーションのブレンド比率を表す変数でこれらの変数は外部から使用するのでpublicにしておきます。
そして、はじめにでご紹介した記事にあるとおりFAnimNode_Baseの関数をオーバーライドします。CacheBones_AnyThread関数は使用しません。
ここで注意点ですが、FAnimNode_Baseを継承して作成するクラスはclassではなくstructで定義します。また、モジュールAPI指定子を書くのを忘れやすいと思うので気を付けましょう。

次に、実装ファイルを見ていきましょう。

AnimNode_MyBlendAnimInternal.cpp
#include "AnimNode_MyBlendAnimInternal.h"
#include "Animation/AnimInstanceProxy.h"
#include "Animation/AnimSequence.h"

まず、AnimNode_MyBlendAnimInternal.hとAnimInstanceProxy.hをインクルードします。AnimInstanceProxy.hはだいたい使うので分からなかったらインクルードしておいてもいいかもしれません。また、後でAnimSequenceの関数を使用するのでAnimSequence.hもインクルードします。

AnimNode_MyBlendAnimInternal.cpp
void FAnimNode_MyBlendAnimInternal::Initialize_AnyThread(const FAnimationInitializeContext& Context)
{
    FAnimNode_Base::Initialize_AnyThread(Context);
    AnimNode_1.Initialize_AnyThread(Context);
    AnimNode_2.Initialize_AnyThread(Context);
    AnimNode_3.Initialize_AnyThread(Context);
    AnimNode_1.Sequence = Animation_1;
    AnimNode_2.Sequence = Animation_2;
    AnimNode_3.Sequence = Animation_3;
}

まずは初期化を行うInitialize_AnyThread関数です。ここではFAnimNode_SequencePlayerを初期化し、プロパティで指定されたアニメーションアセットをセットします。

AnimNode_MyBlendAnimInternal.cpp
void FAnimNode_MyBlendAnimInternal::(const FAnimationUpdateContext& Context)
{
    AnimNode_1.UpdateAssetPlayer(Context);
    AnimNode_2.UpdateAssetPlayer(Context);
    AnimNode_3.UpdateAssetPlayer(Context);
}

次に更新を行うUpdate_AnyThread関数です。ここではFAnimNode_SequencePlayerのUpdateAssetPlayer関数を呼び出しノードを更新します。これを忘れるとアニメーションが再生されず、最初のフレームのみブレンドされることになります。

AnimNode_MyBlendAnimInternal.cpp
void FAnimNode_MyBlendAnimInternal::Evaluate_AnyThread(FPoseContext& Output)
{
    if (Animation_1 == nullptr || Animation_2 == nullptr || Animation_3 == nullptr)
    {
        UE_LOG(LogTemp, Warning, TEXT("Please specify all animation assets in ApplyHandState."));
        return;
    }

    const FBoneContainer* RequiredBones = &Output.AnimInstanceProxy->GetRequiredBones();

    //入力するポーズ用の変数を初期化
    FCompactPose pose_1;
    FCompactPose pose_2;
    FCompactPose pose_3;
    pose_1.SetBoneContainer(RequiredBones);
    pose_2.SetBoneContainer(RequiredBones);
    pose_3.SetBoneContainer(RequiredBones);

    //入力するカーブ用の変数を初期化
    FBlendedCurve curve_1;
    FBlendedCurve curve_2;
    FBlendedCurve curve_3;
    curve_1.InitFrom(*RequiredBones);
    curve_2.InitFrom(*RequiredBones);
    curve_3.InitFrom(*RequiredBones);

    //アニメーションの再生時間やルートモーションを使うかの変数を初期化
    FAnimExtractContext context_1(AnimNode_1.GetAccumulatedTime(), Animation_1->bEnableRootMotion);
    FAnimExtractContext context_2(AnimNode_2.GetAccumulatedTime(), Animation_2->bEnableRootMotion);
    FAnimExtractContext context_3(AnimNode_3.GetAccumulatedTime(), Animation_3->bEnableRootMotion);

    //出力用のポーズとカーブ用の変数を初期化
    FCompactPose pose_1_2;
    FBlendedCurve curve_1_2;
    pose_1_2.SetBoneContainer(RequiredBones);
    curve_1_2.InitFrom(*RequiredBones);

    //アニメーションの再生時間からポーズとカーブを取得
    Animation_1->GetBonePose(pose_1, curve_1, context_1);
    Animation_2->GetBonePose(pose_2, curve_2, context_2);
    Animation_3->GetBonePose(pose_3, curve_3, context_3);

    //二つのアニメーションをブレンド
    FAnimationRuntime::BlendTwoPosesTogether(
        pose_1, pose_2, curve_1, curve_2, 1 - IsState2, pose_1_2, curve_1_2
    );

    //1回目の結果と三つ目のアニメーションをブレンド
    FAnimationRuntime::BlendTwoPosesTogether(
        pose_1_2, pose_3, curve_1_2, curve_3, 1 - IsState3, Output.Pose, Output.Curve
    );
}

次に最終的なポーズを決定するEvaluate_AnyThread関数です。やっていることはBlendTwoPosesTogether関数を用いてアニメーションをブレンドするだけですがこれが思いのほかめんどくさいです。というのもBlendTwoPosesTogether関数に渡すのがFPoseLinkではなくFCompactPoseだからです。この二つの型の変換ですが、直接変換する方法がなくAnimInstanceProxyからFBoneContainerを取得しそれを用いて初期化することで変換ではないですが、使えるようになります。同じく引数にあるFBlendedCurveも同様にFBoneContainerを用いて初期化します。ここまでで使用する二つの変数を初期化しましたが、まだポーズの情報が含まれていません。そこでUAnimSequenceのGetBonePoseを使用して指定した時間のポーズを取得します。そしてこの関数の引数にはFAnimExtractContextというものが必要で、これは再生時間やルートモーションをするかなどの情報をまとめた構造体です。FAnimNode_SequencePlayerのGetAccumulatedTime関数で再生時間を、UAnimSequenceのbEnableRootMotion変数でそれぞれの値を取得することができます。これでやっとBlendTwoPosesTogether関数を実行してアニメーションをブレンドできます。今回は3つのアニメーションをブレンドするのでこれを2回行います。
ここでの注意点ですが、FBoneContatinerを使ってFCompactPoseとFBlendedCurveを初期化しますがFCompactPoseのSetBoneContainer関数の引数はconst FBoneContainer*でFBlendedCurveのInitFrom関数の引数はconst FBoneContatiner&ということで、片方はポインタ、もう片方は参照なので気を付けましょう。

FAnimNode_MyBlendAnim

先程作成したFAnimNode_MyBlendAnimInternalとLayered blend per boneノードに当たるFAnimNode_LayeredBoneBlendを用いて目的の機能を作ります。アニメーションBPで作ると以下のような処理なります。
4.PNG

AnimNode_MyBlendAnim.h
#pragma once

#include "Animation/AnimNodeBase.h"
#include "AnimNode_LayeredBoneBlend.h"
#include "AnimNode_MyBlendAnimInternal.h"
#include "AnimNode_MyBlendAnim.generated.h"

UENUM(BlueprintType)
enum class EAnimState : uint8
{
    AS_State1   UMETA(DisplayName = "State1"),
    AS_State2   UMETA(DisplayName = "State2"),
    AS_State3   UMETA(DisplayName = "State3"),
    AS_Unknown  UMETA(DisplayName = "Unknown")
};

USTRUCT(BlueprintInternalUseOnly)
struct CUSTOMANIMNODE_API FAnimNode_MyBlendAnim : public FAnimNode_Base
{
    GENERATED_USTRUCT_BODY()

public:
    UPROPERTY(EditAnywhere, Category = "Links")
        FPoseLink SourcePose;

    UPROPERTY(EditAnywhere, Category = "Modify Target")
        FBoneReference BoneToModify;

    UPROPERTY(EditAnywhere, Category = "State", meta = (PinShownByDefault))
        EAnimState AnimState = EAnimState::AS_Unknown;

    UPROPERTY(EditAnywhere, Category = "State")
        float InterpSpeed = 0.f;

    UPROPERTY(EditAnywhere, Category = "Animation Asset")
        UAnimSequence* Animation_1;

    UPROPERTY(EditAnywhere, Category = "Animation Asset")
        UAnimSequence* Animation_2;

    UPROPERTY(EditAnywhere, Category = "Animation Asset")
        UAnimSequence* Animation_3;

private:
    FAnimNode_MyBlendAnimInternal InternalNode;
    FAnimNode_LayeredBoneBlend LayeredBoneBlendNode;
    float BeforeFrameIsState2 = 0.f;
    float BeforeFrameIsState3 = 0.f;
    float BeforeFrameAlpha = 0.f;

public:
    FAnimNode_MyBlendAnim();

    virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override;
    virtual void CacheBones_AnyThread(const FAnimationCacheBonesContext& Context) override;
    virtual void Update_AnyThread(const FAnimationUpdateContext& Context) override;
    virtual void Evaluate_AnyThread(FPoseContext& Output) override;

private:
    void GetHandState(float DeltaTime, float& IsState2, float& IsState3, float& Alpha);
};

まず、アニメーションの切り替えに使う列挙型をここで定義しておきます。AS_State1からAS_State3の時は対応するアニメーションに遷移しますが、AS_Unknownの時はアニメーションのブレンドを行わないようにします。
BeforeFrame~の変数はFInterpを使用して補間する際に使用する1つ前のフレームのデータを保持する変数です。また、GetHandState関数はアニメーションの指定をするAnimState変数をもとに現在のアニメーションから目的のアニメーションへ補間する処理を行います。
また、入力ピンの出し方ですがUPROPERTYで"meta = (PinShownByDefault)"と指定すればできます。この場合ノードを置いた時には入力ピンの状態になっていますが、"meta = (PinHidenByDefault)"とすることでプロパティから目のマークを押さないと入力ピンにならないようにできます。

ちなみにFBoneReference型のプロパティはアニメーションノードでよく見るスケルトンのボーン構造がツリー形式で表示されるやつになります。

次に、実装ファイルを見ていきましょう。

AnimNode_MyBlendAnim.cpp
void FAnimNode_MyBlendAnim::Initialize_AnyThread(const FAnimationInitializeContext& Context)
{
    FAnimNode_Base::Initialize_AnyThread(Context);

    BoneToModify.Initialize(Context.AnimInstanceProxy->GetRequiredBones());

    InternalNode.Animation_1 = Animation_1;
    InternalNode.Animation_2 = Animation_2;
    InternalNode.Animation_3 = Animation_3;

    InternalNode.Initialize_AnyThread(Context);

    //適用するボーンを設定
    FBranchFilter branchFilter;
    branchFilter.BoneName = BoneToModify.BoneName;
    branchFilter.BlendDepth = 0;
    FInputBlendPose blendPose;
    blendPose.BranchFilters.Add(branchFilter);
    LayeredBoneBlendNode.LayerSetup.Add(blendPose);

    LayeredBoneBlendNode.BasePose = SourcePose;

    //MyBlendAnimInternalノードとlayeredBoneBlendノードを接続
    FPoseLink internalPose;
    internalPose.SetLinkNode(&InternalNode);
    internalPose.Initialize(Context);
    LayeredBoneBlendNode.BlendPoses.Add(internalPose);

    LayeredBoneBlendNode.BlendWeights.Add(0.f);

    LayeredBoneBlendNode.Initialize_AnyThread(Context);
}

初期化処理ではFBoneReferenceやFAnimNode_MyBlendAnimInternalやFAnimNode_LayeredBoneBlendを初期化します。
5.PNG
エディタのプロパティを見てもわかりますが、FAnimNode_LayeredBoneBlendの適用するボーンを設定する項目は少しめんどうくさそうです。しかし、特に難しい処理などは挟まないので順に初期化していけば問題ありません。
次にMyBlendAnimInternalノードとLayered blend per boneノードを接続するところですが、Layered blend per boneノードでははじめにでご紹介した記事で解説されている方法のように接続するノードの参照を渡して接続することができず、接続するにはFPoseLink型で渡す必要があります。方法は簡単でFPoseLink型の変数を作りそれを接続したいノードをセットして初期化し、それをFAnimNode_LayeredBoneBlendのBlendPosesに追加するだけです。

AnimNode_MyBlendAnim.cpp
void FAnimNode_MyBlendAnim::CacheBones_AnyThread(const FAnimationCacheBonesContext& Context)
{
    LayeredBoneBlendNode.CacheBones_AnyThread(Context);
}

次にノードが参照するボーン情報をキャッシュする処理を書くCacheBones_AnyThread関数です。
中身はシンプルでFAnimNode_LayeredBoneBlendのCacheBones_AnyThread関数を呼び出しているだけです。

AnimNode_MyBlendAnim.cpp
void FAnimNode_MyBlendAnim::Update_AnyThread(const FAnimationUpdateContext& Context)
{
    //入力ピンを更新
    GetEvaluateGraphExposedInputs().Execute(Context);

    float isState2 = 0.f;
    float isState3 = 0.f;
    float alpha = 0.f;

    GetHandState(Context.GetDeltaTime(), isState2, isState3, alpha);

    InternalNode.IsState2 = isState2;
    InternalNode.IsState3 = isState3;
    LayeredBoneBlendNode.BlendWeights[0] = alpha;

    LayeredBoneBlendNode.Update_AnyThread(Context);
}

次に更新処理ですが、ここでは後で説明するGetHandState関数を使ってそれぞれのブレンド比率を求め、MyBlendAnimInternalノードとLayered blend per boneノードの値を更新します。
ここでの注意点ですが、

GetEvaluateGraphExposedInputs().Execute(Context);

この行を書かないとノードの入力ピンに公開された変数の値が初期化の際にしか更新されなくなります。ですので、入力ピンが1つでもあるノードではかならずUpdate_AnyThread関数でこの処理を呼び出してください。

こちらの情報は@seiko_devさんから教えて頂きました。ありがとうございました!

AnimNode_MyBlendAnim.cpp
void FAnimNode_MyBlendAnim::Evaluate_AnyThread(FPoseContext& Output)
{
    LayeredBoneBlendNode.Evaluate_AnyThread(Output);
}

次にEvaluate_AnyThread関数ですが、Update_AnyThread関数でアニメーションのブレンドやプロパティの更新は済んでいてあとはLayered blend per boneノードから結果を得るだけなのでシンプルになっています。

AnimNode_MyBlendAnim.cpp
void FAnimNode_MyBlendAnim::GetHandState(float DeltaTime, float& IsState2, float& IsState3, float& Alpha)
{
    float targetIsState2 = (AnimState == EAnimState::AS_State2 ? 1.f : 0.f);
    float targetIsState3 = (AnimState == EAnimState::AS_State3 ? 1.f : 0.f);
    float targetAlpha = (AnimState == EAnimState::AS_Unknown ? 0.f : 1.f);

    IsState2 = FMath::FInterpTo(BeforeFrameIsState2, targetIsState2, DeltaTime, InterpSpeed);
    IsState3 = FMath::FInterpTo(BeforeFrameIsState3, targetIsState3, DeltaTime, InterpSpeed);
    Alpha = FMath::FInterpTo(BeforeFrameAlpha, targetAlpha, DeltaTime, InterpSpeed);

    BeforeFrameIsState2 = IsState2;
    BeforeFrameIsState3 = IsState3;
    BeforeFrameAlpha = Alpha;
}

最後にGetHandState関数です。ここでは、列挙型からそれぞれのブレンド比率を求めてFMathのFInterpTo関数を用いて補間しています。次のフレームで今のフレームの情報を使用するので値を更新しておきます。また、FInterpTo関数ではDeltaTimeを使用しますがこれはUpdate_AnyThread関数の引数のFAnimationUpdateContextのGetDeltaTime関数で取得しています。

これで実装部分のコードは終わりです。次にエディタ上でのノードの見た目に関する部分のコードを見ていきましょう。

UAnimGraphNode_MyBlendAnim

ここからはエディタモジュールでの作業になります。

AnimGraphNode_MyBlendAnim.h
#pragma once

#include "CoreMinimal.h"
#include "AnimGraphNode_Base.h"
#include "AnimNode_MyBlendAnim.h"
#include "EdGraphNodeUtils.h"
#include "AnimGraphNode_MyBlendAnim.generated.h"

UCLASS()
class CUSTOMANIMNODEED_API UAnimGraphNode_MyBlendAnim : public UAnimGraphNode_Base
{
    GENERATED_UCLASS_BODY()

public:
    UPROPERTY(EditAnywhere, Category = "Settings")
        FAnimNode_MyBlendAnim Node;

public:
    // UK2Node_FunctionEntry interface
    virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
    // End of UK2Node_FunctionEntry interface

    // UEdGraphNode interface
    virtual FText GetTooltipText() const override;
    virtual FLinearColor GetNodeTitleColor() const override;
    // End of UEdGraphNode interface

    // UObject interface
    virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
    // End of UObject interface

private:
    /** Constructing FText strings can be constly, so we cache the node's title **/
    FNodeTitleTextTable CachedNodeTitles;
};

まず、実装部分のコードが書かれたAnimNode_MyBlendAnim.hとメンバ変数のFNodeTitleTextTable型が定義されたEdGraphNodeUtils.hをインクルードします。また、必ず実装部分のクラスをメンバ変数に持つようにします。

ちなみに、ノードを検索する際に特定のワードで検索に引っかかるようにするには

UCLASS(meta = (Keywords = "キーワード"))

のように指定します。

色々な関数がありますが、実装部分を見ながら説明していきます。

AnimGraphNode_MyBlendAnim.cpp
//ヘッダーファイルのインクルードした後
#define LOCTEXT_NAMESPACE "MyBlendAnim"

/*
関数の実装など...
*/

//ソースコードの最後
#undef LOCTEXT_NAMESPACE

まず、実装ファイルでは上記のマクロを定義します。対応する文字列はノードの名前にしておくとよいと思います。

AnimGraphNode_MyBlendAnim.cpp
FText UAnimGraphNode_MyBlendAnim::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
    if ((TitleType == ENodeTitleType::ListView || TitleType == ENodeTitleType::MenuTitle) && (Node.BoneToModify.BoneName == NAME_None))
    {
        return LOCTEXT("MyBlendAnim", "My Blend Anim");
    }
    else
    {
        FFormatNamedArguments Args;
        Args.Add(TEXT("ControllerDescription"), LOCTEXT("MyBlendAnim", "My Blend Anim"));
        Args.Add(TEXT("BoneName"), FText::FromName(Node.BoneToModify.BoneName));

        if (TitleType == ENodeTitleType::ListView || TitleType == ENodeTitleType::MenuTitle)
        {
            CachedNodeTitles.SetCachedTitle(TitleType, FText::Format(LOCTEXT("AnimGraphNode_MyBlendAnim_ListTitle", "{ControllerDescription} - Bone: {BoneName}"), Args), this);
        }
        else
        {
            CachedNodeTitles.SetCachedTitle(TitleType, FText::Format(LOCTEXT("AnimGraphNode_MyBlendAnim_Title", "{ControllerDescription}\nBone: {BoneName}"), Args), this);
        }
    }
    return CachedNodeTitles[TitleType];
}

ノードの名前や使用しているボーンの名前などのノードのタイトルを返すGetNodeTitle関数です。

FText UAnimGraphNode_MyBlendAnim::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
    return LOCTEXT("AnimGraphNode_MyBlendAnim_Title", "My Blend Anim");
}

このようにしても問題ないのですが、このノードではボーンを指定するので標準ノードのようにプロパティを見なくても選択されているボーンの名前がわかるようにします。
この部分は標準のノードのコードをコピーしたようなものなので、これをコピペしてノードの名前などのところを変えて使うのがよいかと思います。
簡単に説明するとCacheNodeTitlesのSetCachedTitle関数の引数のFTextのFormat関数で渡した文字列がそのまま表示されます。改行コードを入れることで複数行の表示もできます。

AnimGraphNode_MyBlendAnim.cpp
FLinearColor UAnimGraphNode_MyBlendAnim::GetNodeTitleColor() const
{
    return FLinearColor(0.f, 1.f, 0.f);
}

次にノードの色を返すGetNodeTitleColor関数ですが、これは単純にFLinearColor型で色を指定します。

AnimGraphNode_MyBlendAnim.cpp
FText UAnimGraphNode_MyBlendAnim::GetTooltipText() const
{
    return LOCTEXT("AnimGraphNode_MyBlendAnim_Tooltip",
        "Blends hand animation based on AnimState.\n"
        "All child bones of specified bone will be blended."
    );
}

次にノードにマウスカーソルを重ねた時に表示される文字列を返すGetTooltipText関数です。こちらも文字列を返すだけなのでシンプルです。また、改行コードを入れることで複数行に分けて表示することもできます。

AnimGraphNode_MyBlendAnim.cpp
void UAnimGraphNode_MyBlendAnim::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent)
{
    Super::PostEditChangeProperty(PropertyChangedEvent);
    ReconstructNode();
}

最後にプロパティのデフォルト値が変更された時に呼ばれるPostEditChangeProperty関数です。UAnimGraphNode_Baseの親のUEdGraphNodeで定義されているReconstructNode関数を呼び出して実装部分のクラスのInitialize_AnyThread関数を再度呼び出して初期化処理を行っています。

これで全ての実装が終わりました。おつかれさまでした!
アニメーションBPで実際に動かしてみましょう。

おわりに

書きたいことを書いていたら長い記事になってしまったので、忙しい人用に以下に要点をまとめます。

まとめ

 
・ Solution configurationをDevelopment Editorにする
・ 実装部分のクラスはFAnimNode_Baseを継承して作成するクラスはclassではなくstructで定義する
・ 手動で作成したクラスにはモジュールAPI指定子を忘れずつける
・ スケルトンツリーからボーンを指定するプルダウンメニューはFBoneReference型のプロパティ
・ 入力ピンにするにはUPROPERTYに"meta = (PinShownByDefault)"か"meta = (PinHidenByDefault)"を書く
・ 入力ピンのあるノードでは"GetEvaluateGraphExposedInputs().Execute(Context);"をUpdate_AnyThread関数に書く
・ ノードをあるキーワードで検索できるようにするにはUCLASSにmeta = (Keywords = "キーワード")を書く
・ エディタモジュールの方は処理にはあまり関係しないので、とりあえずエンジンコード丸写しでもOK
 

こんなところでしょうか。
自作アニメーションノードについての情報があまりなく、結局エンジンコードやおかずさん(@EGJ-Kaz_Okada)のKawwaiiPhysicsを参考に作りました。この記事がこれから自作アニメーションノードを作る方の参考になればうれしいです!

この記事で作成したノードや「アニメーションノードをまとめたアニメーションノードを作ってみた」で作成したノード、TwoBoneIKノードのようにComponent Spaceで動作するノードが含まれたプロジェクトを用意しました。
以下でダウンロードできます。
https://github.com/Naotsun19B/CustomAnimNode

明日は、なんさん(@NotANumber)の「アーティストのためのマテリアル式にゅうもん」です。お楽しみに!

12
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
5