概要
UnrealEngine5のアニメーションでのマルチスレッド処理についてのメモ書きです。
BPと(PropertyAccess
)と、C++の両方を試してみます。
更新履歴
日付 | 内容 |
---|---|
2023/04/04 | 初版 |
参考
以下の記事を参考にいたしました、ありがとうございます。
UE公式:アニメーションの最適化
UE公式:UAnimInstance
猫でも分かる UE5.0, 5.1 におけるアニメーションの新機能について
UE5 Property Accessを使ってAnimBPをマルチスレッドに更新しよう
スレッドセーフという幻想と現実
関連する過去記事
UE4 アニメ―ションBPをC++クラス継承にしてアクセスする
UE4 非同期処理についてのメモ
環境
Windows10
Visual Studio 2022
UnrealEngine 5.1.1
関連ソース
"\Engine\Source\Runtime\Engine\Classes\Animation\AnimInstance.h"
"\Engine\Source\Runtime\Engine\Private\Animation\AnimInstance.cpp"
"\Engine\Source\Runtime\Engine\Public\Animation\AnimInstanceProxy.h"
ブループリントでの実装方法
UE5からアニメーションBPでマルチスレッド処理ができるようになったようです。
BlueprintThreadSafeUpdateAnimation を使う
この関数はワーカースレッドで実行されアニムグラフの更新直前に実行されるようです。
ただしこの関数でスレッドセーフ以外の関数ノードを使用すると以下のようにエラーになります。
TryGetOwner や GetMovementComponent がスレッドセーフではないと警告がでています。
UE5にて追加された PropertyAccess という機能にて外部オブジェクトをローカル変数にコピーしてくれていることによりスレッドセーフを実現しているようです。
また自作関数の場合は、[詳細設定]より[スレッドセーフ]にチェックをいれることで BlueprintThreadSafeUpdateAnimation
での使用が可能となりますが、実際にスレッドセーフであるかの責任は作成者が負う事になります。
C++での自作関数の場合はメタ情報に BlueprintThreadSafe
の付与が必要です。これも実際にスレッドセーフ実装の責任は作成者が負うことになります。
UFUNCTION(BlueprintCallable, meta=(BlueprintThreadSafe))
void HeavyFunc(){ /* スレッドセーフな処理 */ }
C++ での実装方法
C++ではUE4時代からあるFAnimInstanceProxy
を使う方法と、UE5から追加されたNativeThreadSafeUpdateAnimation
を使う方法の2通りがあるようです。
NativeThreadSafeUpdateAnimation を使う
C++では UAnimInstance::NativeThreadSafeUpdateAnimation
でワーカースレッドにて実行することができます。スレッドセーフではない外部のオブジェクトにアクセスする場合などは排他処理を行うか、 UAnimInstance::NativeUpdateAnimation
にてローカルにコピーしておく等の対応が必要になります。
以下コード例。
UCLASS()
class MYPROJECT_API UMyAnimInstance : public UAnimInstance
{
GENERATED_BODY()
public:
// ネイティブ更新オーバーライドポイント。
virtual void NativeUpdateAnimation(float _DeltaSeconds) override;
// ネイティブスレッドセーフ更新オーバーライド ポイント。
virtual void NativeThreadSafeUpdateAnimation(float _DeltaSeconds) override;
// ネイティブ ポスト評価オーバーライドポイント
virtual void NativePostEvaluateAnimation() override;
private:
FVector LocalVelocity;
};
// ネイティブ更新
void UMyAnimInstance::NativeUpdateAnimation(float _DeltaSeconds)
{
// 基底処理
Super::NativeUpdateAnimation(_DeltaSeconds);
// スレッドセーフではない処理
const APawn* _Pawn = Cast<APawn>(TryGetPawnOwner());
if (!_Pawn){
return;
}
UCharacterMovementComponent* MoveComp = Cast<UCharacterMovementComponent>(_Pawn->GetMovementComponent());
LocalVelocity = MoveComp->Velocity;
}
// ネイティブスレッドセーフ更新
void UMyAnimInstance::NativeThreadSafeUpdateAnimation(float _DeltaSeconds)
{
// 基底処理
Super::NativeThreadSafeUpdateAnimation(_DeltaSeconds);
// ワーカースレッドでの処理
}
// ネイティブアニメーション評価後
void UMyAnimInstance::NativePostEvaluateAnimation()
{
// 基底処理
Super::NativePostEvaluateAnimation();
// 外部オブジェクトへの更新など
}
AnimInstanceProxy を使う
UE4時代からあった方法として、FAnimInstanceProxy
を使った方法があります。FAnimInstanceProxy
は UAnimInstance の代わりにアニメーション更新中に渡されるプロキシオブジェクトです。
FAnimInstanceProxy
を継承して差し替え、スレッドセーフ処理のために外部オブジェクトからのデータを PreUpdate
で保持し、Update
でワーカースレッドでの処理を書きます。
以下コード例。
// アニムインスタンスプロクシ
USTRUCT()
struct MYPROJECT_API FMyAnimInstanceProxy : public FAnimInstanceProxy
{
GENERATED_USTRUCT_BODY()
public:
virtual void InitializeObjects(UAnimInstance* _InAnimInstance) override;
virtual void PreUpdate(UAnimInstance* _InAnimInstance, float _DeltaSeconds) override;
virtual void Update(float _DeltaSeconds) override;
virtual void PostUpdate(UAnimInstance* _InAnimInstance) const override;
// 収集データ
UPROPERTY(Transient)
TObjectPtr<APawn> Owner;
UPROPERTY(Transient)
FVector ActorLocation;
UPROPERTY(Transient)
TObjectPtr<UCharacterMovementComponent> MovementComponent;
UPROPERTY(Transient)
FVector Velocity;;
UPROPERTY(Transient)
bool bIsAnyMontagePlaying;
};
// アニメーションインスタンスクラス
UCLASS()
class MYPROJECT_API UMyAnimInstance : public UAnimInstance
{
GENERATED_BODY()
private:
UPROPERTY(Transient)
FMyAnimInstanceProxy Proxy;
virtual FAnimInstanceProxy* CreateAnimInstanceProxy() override { return &Proxy; }
virtual void DestroyAnimInstanceProxy(FAnimInstanceProxy* InProxy) override {}
friend struct FMyAnimInstanceProxy;
};
void FMyAnimInstanceProxy::InitializeObjects(UAnimInstance* _InAnimInstance)
{
FAnimInstanceProxy::InitializeObjects(_InAnimInstance);
Owner = _InAnimInstance->TryGetPawnOwner();
if (Owner == nullptr) { return; }
MovementComponent = Cast<UCharacterMovementComponent>(Owner->GetMovementComponent());
}
void FMyAnimInstanceProxy::PreUpdate(UAnimInstance* _InAnimInstance, float _DeltaSeconds)
{
FAnimInstanceProxy::PreUpdate(_InAnimInstance, _DeltaSeconds);
// 外部からデータ収集
if (::IsValid(Owner)){
ActorLocation = Owner->GetActorLocation();
}
if (::IsValid(MovementComponent)){
Velocity = MovementComponent->Velocity;
}
if (_InAnimInstance){
bIsAnyMontagePlaying = _InAnimInstance->IsAnyMontagePlaying();
}
}
void FMyAnimInstanceProxy::Update(float _DeltaSeconds)
{
FAnimInstanceProxy::Update(_DeltaSeconds);
// ワーカースレッドの処理
}
void FMyAnimInstanceProxy::PostUpdate(UAnimInstance* _InAnimInstance) const
{
FAnimInstanceProxy::PostUpdate(_InAnimInstance);
// 外部オブジェクトへの更新
}
Proxyも継承する必要があるので少し煩雑です。あと、UE5から追加された NativeThreadSafeUpdateAnimation
を使う方法が推奨されているようです。
まとめ
BPのみでマルチスレッド処理の実装ができるのはC++での実装にかかる難易度を考えるとかなり凄い機能だと思います。
スレッドセーフ処理については、アニメーション処理上では外部オブジェクトの参照がほとんどだと思われるので排他処理ではなくコピーしておくパターンが多いのではないかと思います。