动画通知(Anim Notify)问题小记

69 篇文章 6 订阅
63 篇文章 1 订阅

本文简单记录了使用 动画通知(Anim Notify) 过程中遇到的一些问题和可能的解决方法

基础

动画通知 是 UE 提供的一种逻辑触发机制,可以在播放动画过程中触发指定的逻辑,在类型上大概分为两类:

  • UAnimNotify

UAnimNotify 是 单点触发 类的动画通知,相关逻辑是一次性触发完成的,从代码(C++)上来讲,扩展 UAnimNotify 主要是要重载实现 Notify 函数:

void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation);
  • UAnimNotifyState

UAnimNotifyState 是 时间段触发 类的动画通知,相关逻辑是在一段时间内触发完成的(分为 Begin, Tick, End 三个阶段),从代码(C++)上来讲,扩展 UAnimNotifyState 主要是要重载实现以下三个函数:

void NotifyBegin(class USkeletalMeshComponent * MeshComp, class UAnimSequenceBase * Animation, float TotalDuration);
void NotifyTick(class USkeletalMeshComponent * MeshComp, class UAnimSequenceBase * Animation, float FrameDeltaTime);
void NotifyEnd(class USkeletalMeshComponent * MeshComp, class UAnimSequenceBase * Animation);

基础内容叙述的比较简略,有兴趣的朋友可以参考文档继续了解.

问题

动画通知一般都会提供一些可配置的参数,譬如:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta=(DisplayName="Particle System"))
UParticleSystem* PSTemplate;

并且,动画通知一般也应该提供可配置的(静态)参数设置(而不要记录其他(动态)数据)

对于 UAnimNotify 而言,由于是 单点触发 的,所以一般也不会有记录(动态)数据的需求,但是对于 UAnimNotifyState,往往都涉及清理操作(时间段结束时),所以记录(动态)数据很多时候都是刚需:

考虑你要开发一个 UAnimNotifyState,用于在一段时间内播放某个循环粒子(时间结束后销毁),一个普遍的实现方法就是在 NotifyBegin 时创建粒子,并记录下粒子引用(数据),之后在 NotifyEnd 时检查引用并释放粒子,按照以上思路,我们很容易在该 UAnimNotifyState 的实现中添加以下数据成员:

TWeakObjectPtr<UParticleSystemComponent> ParticleSystemComponent;

但是,这里存在一个很隐晦的问题: 动画通知其实是共享的(因为动画实例播放动画时引用的是同一份动画资源)

说的有些抽象,这里举个例子: 假设我们有某一动画(名为 ‘Animation’),该动画中设置了两个动画通知(名为 ‘Notify1’ 和 ‘Notify2’(这里注意一下,动画中不同动画通知之间彼此是数据独立的)),现在有 A 和 B 两个动画实例同时播放 ‘Animation’,那么 A 和 B 访问的其实是同一份 ‘Animation’ 资源,自然的,相关的 ‘Notify1’ 和 ‘Notify2’ 也便是共享的.

那么回到上面的那个问题,如果我们直接在 UAnimNotifyState 的实现中添加(动态)数据,那么不同动画实例播放同一动画序列时便会产生’数据竞争’,进而导致逻辑出错.

有一些方法可以处理动画通知共享数据的问题,比较简单的一种就是在动画通知中将不同动画实例之间的数据进行隔离:

// data container for animation notify
template<typename T>
class TANDataContainer
{
public:
	// do not buffer result
	T* Get(USkeletalMeshComponent* MeshComponent)
	{
		auto Key = GenKey(MeshComponent);
		if (Key >= 0)
		{
			if (DataContainer.Contains(Key))
			{
				return &DataContainer[Key];
			}
		}

		return nullptr;
	}

	// do not buffer result
	T* GetOrAdd(USkeletalMeshComponent* MeshComponent)
	{
		auto Key = GenKey(MeshComponent);
		if (Key >= 0)
		{
			return &DataContainer.FindOrAdd(Key);
		}

		return nullptr;
	}

	bool Remove(USkeletalMeshComponent* MeshComponent)
	{
		auto Key = GenKey(MeshComponent);
		if (Key >= 0)
		{
			return DataContainer.Remove(Key) > 0;
		}

		return false;
	}

protected:
	int GenKey(USkeletalMeshComponent* MeshComponent)
	{
		if (MeshComponent)
		{
			return MeshComponent->GetUniqueID();
		}

		// return -1 as invalid key
		// NOTE key is valid when >= 0
		return -1;
	}

protected:
	TMap<int, T> DataContainer;
};

代码使用大概是这个样子(动画通知中使用 TANDataContainer 来存储(动态)数据):

struct FANData
{
    TWeakObjectPtr<UParticleSystemComponent> ParticleSystemComponent;
};

TANDataContainer<FANData> DataContainer;

经过以上步骤,不同动画实例间的数据共享问题就解决了,但是仍然存在一个更隐晦的问题: 同一动画实例间的数据共享问题.

你可能会觉得奇怪,同一动画实例怎么会有数据共享问题呢 ? 这种情况其实是很普遍的,这往往发生在动画播放过程中,又重新触发播放了相同动画(动画重入问题).

在上面这种情况下, 对于 UAnimNotifyState 而言,其一般的触发顺序是(略去了 NotifyTick, 另外的,也可能出现多一次 NotifyBegin/NotifyEnd 调用的情况,此处也略去不谈):

NotifyBegin(old) -> NotifyEnd(old) -> NotifyBegin(new) -> NotifyEnd(new)

所以实际上也并不会出现数据竞争的问题,但是如果你将动画的触发方式改为了 Branching Point (对触发方式有兴趣的朋友还是请参考文档,这里不做解释),那么对于 UAnimNotifyState 而言,其触发顺序往往是这样的(略去了 NotifyTick):

NotifyBegin(old) -> NotifyBegin(new) -> NotifyEnd(old) -> NotifyEnd(new)

于是数据共享问题又出现了(同一动画实例) …

为了解决这个问题,我们需要重载实现 UAnimNotifyState 中的以下函数:

void BranchingPointNotifyBegin(FBranchingPointNotifyPayload& BranchingPointPayload);
void BranchingPointNotifyTick(FBranchingPointNotifyPayload& BranchingPointPayload);
void BranchingPointNotifyEnd(FBranchingPointNotifyPayload& BranchingPointPayload);

其中 BranchingPointPayload 参数可以为我们提供 MontageInstanceID 数据,基于此我们就可以区分出相同动画的不同实例(当然,这种实现方式也导致对应的 UAnimNotifyState 只能用于 蒙太奇(Montage) 中),官方有个相关的实现 UAnimNotifyState_DisableRootMotion,有兴趣的朋友可以看看:

UCLASS(editinlinenew, const, hidecategories = Object, collapsecategories, MinimalAPI, meta = (DisplayName = "Disable Root Motion"))
class UAnimNotifyState_DisableRootMotion : public UAnimNotifyState
{
	GENERATED_UCLASS_BODY()

public:
	virtual void BranchingPointNotifyBegin(FBranchingPointNotifyPayload& BranchingPointPayload) override;
	virtual void BranchingPointNotifyEnd(FBranchingPointNotifyPayload& BranchingPointPayload) override;

#if WITH_EDITOR
	virtual bool CanBePlaced(UAnimSequenceBase* Animation) const override;
#endif
};
UAnimNotifyState_DisableRootMotion::UAnimNotifyState_DisableRootMotion(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	bIsNativeBranchingPoint = true;
}

void UAnimNotifyState_DisableRootMotion::BranchingPointNotifyBegin(FBranchingPointNotifyPayload& BranchingPointPayload)
{
	Super::BranchingPointNotifyBegin(BranchingPointPayload);

	if (USkeletalMeshComponent* MeshComp = BranchingPointPayload.SkelMeshComponent)
	{
		if (UAnimInstance* AnimInstance = MeshComp->GetAnimInstance())
		{
			if (FAnimMontageInstance* MontageInstance = AnimInstance->GetMontageInstanceForID(BranchingPointPayload.MontageInstanceID))
			{
				MontageInstance->PushDisableRootMotion();
			}
		}
	}
}

void UAnimNotifyState_DisableRootMotion::BranchingPointNotifyEnd(FBranchingPointNotifyPayload& BranchingPointPayload)
{
	Super::BranchingPointNotifyEnd(BranchingPointPayload);

	if (USkeletalMeshComponent* MeshComp = BranchingPointPayload.SkelMeshComponent)
	{
		if (UAnimInstance* AnimInstance = MeshComp->GetAnimInstance())
		{
			if (FAnimMontageInstance* MontageInstance = AnimInstance->GetMontageInstanceForID(BranchingPointPayload.MontageInstanceID))
			{
				MontageInstance->PopDisableRootMotion();
			}
		}
	}
}

#if WITH_EDITOR
bool UAnimNotifyState_DisableRootMotion::CanBePlaced(UAnimSequenceBase* Animation) const
{
	return (Animation && Animation->IsA(UAnimMontage::StaticClass()));
}
#endif // WITH_EDITOR

值得注意的是,使用 Branching Point 时,同一触发时间只能存在一个 Branching Point 类动画通知,如果在同一触发时间配置了多个 Branching Point 类动画通知,会有程序告警,并且不能正确触发动画通知:

Branching Point ‘XXX’ overlaps with ‘XXX’ at time: XXX. One of them will not get triggered!

(实际上,上述说法仅对 Branching Point 类 UAnimNotify 成立, 同一触发时间如果存在多个 Branching Point 类 UAnimNotifyState 的话,虽然也会有程序告警,但实际上还是能正确触发的,有兴趣的朋友可以搜索查看 FAnimMontageInstance::UpdateActiveStateBranchingPoints 函数来了解原由)

引用\[1\]:出现如下报错信息: \[ INFO\] \[1564730909.628248268\]: Constructing new MoveGroup connection for group 'arm_group' in namespace '' \[ INFO\] \[1564730910.984667942\]: MoveItSimpleControllerManager: Waiting for redwall_arm/follow_joint_trajectory to come up \[ERROR\] \[1564730915.984849668\]: MoveItSimpleControllerManager: Action client not connected: redwall_arm/follow_joint_trajectory \[ INFO\] \[1564730916.087709422\]: Returned 0 controllers in list \[ INFO\] \[1564730916.107154984\]: Trajectory execution is managing controllers 。 引用\[2\]:发现竟然没有找到follow_joint_trajectory消息,难道move_group没有发布吗?纠结了一天,终于找到原因了,之前虚拟机械臂有gazebo的ros_control插件自动帮我们获取了follow_joint_trajectory的动作action信息,现在到真实机器人了,需要我们自己写程序了。问题找到了,最重要的是要理解一点,move_group此时是follow_joint_trajectory的client端,动作消息是client端的move_group发布的,听起来有点绕,但是千万不要把报错信息的action client not connect搞混淆了,我们现在client是存在且被感知的(就是move_group),而之所以报错是因为我缺少了action server端,我们需要添加一个server订阅这个action。 引用\[3\]:Animation Blueprints Exposed types on SkeletalControlBase to allow creation of derived classes.Added support for creating sub-classes of FAnimNode_StateMachine outside Engine module.Allowed UAnimNotify_PlayMontageNotify to be inherited by other modules.Bug Fix:Fixed sub-classes of FAnimNode_SkeletalControlBase not having deprecated CacheBones called。 问题:constructing new movegroup connection for group 'manipulator' in namespace 回答: 根据提供的引用内容,报错信息中显示正在构建一个新的MoveGroup连接,连接的组是'manipulator',命名空间为空。\[1\]这个报错信息表明正在尝试建立一个名为'manipulator'的组的MoveGroup连接。然而,报错信息中还显示了一个错误,即Action client 'redwall_arm/follow_joint_trajectory'未连接。\[2\]这可能是因为缺少一个action server端,需要添加一个server来订阅这个action。根据提供的信息,还无法确定是否已经在命名空间中找到了'manipulator'组的相关信息。 #### 引用[.reference_title] - *1* *2* [通过ROS控制真实机械臂(5)---Moveit!真实机械臂(move_group和demo.launch修改)](https://blog.csdn.net/qq_34935373/article/details/95916111)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Unreal Engine 4.20 Release Notes](https://blog.csdn.net/pizi0475/article/details/81636150)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值