AbilitySystemComponent_Abilities源码解析(一)
文章目录
- 一、InitializeComponent
- 二、UninitializeComponent
- 三、OnComponentDestroyed
- 四、TickComponent
- 五、InitAbilityActorInfo
- 六、GetShouldTick
- 场景分析
- 场景1:服务器端播放动画蒙太奇
- 场景2:客户端接收动画同步
- 场景3:有需要Tick的属性集
- 场景4:没有任何需要Tick的情况
- 设计意义和性能优化
- 实际应用例子
- 七、SetAvatarActor
- 典型应用场景
- 底层发生的变化
- 设计意义
- 八、ClearActorInfo
- 典型应用场景
- 底层发生的变化
- 与相关函数的对比
- 设计意义和注意事项
- 九、OnRep_OwningActor
- 网络复制背景
- 典型应用场景
- 具体网络游戏示例
- 设计意义和网络同步
- 错误处理场景
- 十、RefreshAbilityActorInfo
- 与相关函数的对比
- 典型应用场景
- 底层发生的变化
- 具体游戏机制示例
一、InitializeComponent
void UAbilitySystemComponent::InitializeComponent()
{Super::InitializeComponent(); // 调用父类的初始化函数,确保基础功能正确设置// 获取拥有此能力系统组件的Actor,比如游戏中的角色或敌人AActor *Owner = GetOwner();// 初始化能力Actor信息,设置Owner和Avatar为同一个Actor// 这告诉GAS系统哪个Actor拥有这些能力,哪个Actor是这些能力效果的目标InitAbilityActorInfo(Owner, Owner);// 清理SpawnedAttributes数组中可能存在的空指针,防止后续访问时崩溃// 从后往前遍历是为了安全地移除元素(避免索引错乱)for (int32 Idx = SpawnedAttributes.Num()-1; Idx >= 0; --Idx){if (SpawnedAttributes[Idx] == nullptr){SpawnedAttributes.RemoveAt(Idx);}}// 查找Owner Actor的所有子对象,目的是自动发现并注册AttributeSetsTArray<UObject*> ChildObjects;GetObjectsWithOuter(Owner, ChildObjects, false, RF_NoFlags, EInternalObjectFlags::Garbage);// 遍历所有子对象,将找到的AttributeSet添加到SpawnedAttributes数组中for (UObject* Obj : ChildObjects){UAttributeSet* Set = Cast<UAttributeSet>(Obj); // 尝试将对象转换为AttributeSetif (Set) // 如果转换成功,说明找到了一个属性集{SpawnedAttributes.AddUnique(Set); // 添加到属性集列表,确保不重复}}// 标记属性集列表已更新,触发内部状态刷新和可能的网络同步SetSpawnedAttributesListDirty();
}
结合具体例子说明:
假设我们有一个英雄角色 HeroCharacter
,它包含:
- 一个
AbilitySystemComponent
(能力系统组件) - 一个
HealthAttributeSet
(生命值属性集,管理生命值、最大生命值等) - 一个
ManaAttributeSet
(法力值属性集,管理法力值、法力回复等)
执行流程:
-
初始化基础:首先调用父类初始化,确保组件基础功能就绪
-
设置Actor信息:获取
HeroCharacter
作为Owner
,并调用InitAbilityActorInfo
告诉GAS系统:“这个能力系统属于HeroCharacter
,能力效果也应用于HeroCharacter
” -
清理旧数据:检查现有的
SpawnedAttributes
数组,移除任何空的属性集引用 -
自动发现属性集:
GetObjectsWithOuter
会找到HeroCharacter
的所有直接子对象- 在遍历中会发现
HealthAttributeSet
和ManaAttributeSet
- 这两个属性集都会被添加到
SpawnedAttributes
数组中
-
完成注册:调用
SetSpawnedAttributesListDirty
通知系统属性集已更新,现在GAS可以开始管理这些属性(处理属性变化、网络同步、应用GameplayEffect等)
设计优势:这种自动发现机制意味着开发者只需要在Actor蓝图中添加AttributeSet组件,GAS就会自动识别和注册,无需手动配置,大大简化了设置流程。
二、UninitializeComponent
void UAbilitySystemComponent::UninitializeComponent()
{// 调用父类的UninitializeComponent函数,执行基础的反初始化逻辑// 这确保父类有机会清理它自己分配的资源Super::UninitializeComponent();// 清理活跃的游戏效果(ActiveGameplayEffects)// 这包括移除所有正在应用的buff/debuff,停止效果计时器,清理内部数据结构// 防止内存泄漏和悬空指针ActiveGameplayEffects.Uninitialize();
}
结合具体例子详细说明:
假设我们有一个英雄角色 HeroCharacter
,它包含一个 AbilitySystemComponent
,当前正应用着以下游戏效果:
- 一个持续30秒的"攻击力提升"buff
- 一个持续10秒的"中毒"debuff(每2秒扣血)
- 一个永久的"火焰抗性"被动效果
执行流程和效果:
-
Super::UninitializeComponent();
- 调用
UActorComponent
的卸载函数 - 停止组件tick,取消事件绑定,执行基础的组件清理工作
- 确保组件从世界中正确分离
- 调用
-
ActiveGameplayEffects.Uninitialize();
- 移除所有活跃效果:立即移除"攻击力提升"、"中毒"和"火焰抗性"效果
- 停止所有计时器:停止"中毒"效果的周期性伤害计时器
- 清理内部数据结构:清空效果列表、属性聚合器等内部容器
- 释放引用:断开对所有GameplayEffect对象的引用,允许垃圾回收
- 重置状态:将ActiveGameplayEffects子系统恢复到初始状态
为什么需要这样做:
- 防止内存泄漏:如果不清理,持续效果中的计时器和回调可能无法被正确释放
- 避免悬空指针:效果可能持有对已销毁Actor或Component的引用,清理可防止后续访问崩溃
- 状态一致性:确保组件在销毁或重新初始化时处于干净状态
- 网络同步:如果是网络游戏,需要正确结束效果的同步
典型调用场景:
- 角色死亡并被销毁时
- 组件被手动移除时
- 关卡切换时
- 游戏结束时
这个反初始化过程确保了 AbilitySystemComponent
能够优雅地关闭,不会留下任何残留的效果或资源。
三、OnComponentDestroyed
void UAbilitySystemComponent::OnComponentDestroyed(bool bDestroyingHierarchy)
{// 销毁活跃状态:清理所有激活的能力、游戏效果和其他活跃状态// 这确保在组件被销毁前,所有运行中的效果都被正确终止DestroyActiveState();// 遍历所有通过该组件生成的属性集(AttributeSets)// 注释解释了为什么将标记垃圾的操作从UninitializeComponent移到这里:// 因为组件可能被反初始化后重新初始化而不被销毁(例如当拥有者的关卡被移除后又重新添加到世界时)// 在这种情况下,属性集需要被保留以便重新使用for (UAttributeSet* Set : GetSpawnedAttributes()){if (Set){// 将属性集标记为垃圾,允许垃圾回收系统在适当时机回收它们// 这防止了内存泄漏,确保属性集对象被正确清理Set->MarkAsGarbage();}}// 在完成所有必要的清理工作后,最后调用父类的OnComponentDestroyed// 这是标准的UE组件销毁模式,确保父类有机会执行自己的清理逻辑Super::OnComponentDestroyed(bDestroyingHierarchy);
}
结合具体例子详细说明:
假设我们有一个英雄角色 HeroCharacter
,它包含:
- 一个
AbilitySystemComponent
- 两个属性集:
HealthAttributeSet
和ManaAttributeSet
场景1:角色永久销毁
当玩家退出游戏或角色被永久移除时:
-
DestroyActiveState()
- 立即终止所有激活的能力(如正在施放的火球术)
- 移除所有游戏效果(如攻击力buff、中毒效果)
- 停止所有内部计时器和状态机
-
属性集标记为垃圾
HealthAttributeSet
和ManaAttributeSet
都被标记为垃圾- 它们将在垃圾回收周期中被自动销毁
- 防止内存泄漏
-
父类清理
- 执行
UActorComponent
的基础销毁逻辑
- 执行
场景2:关卡流式加载(解释注释中的特殊情况)
假设游戏使用关卡流式加载:
- 玩家从"森林关卡"移动到"城堡关卡"
- "森林关卡"被卸载,
HeroCharacter
的组件被反初始化(UninitializeComponent
) - 但角色对象本身没有被销毁
- 当玩家返回"森林关卡"时,组件被重新初始化
为什么需要在这里而不是在 UninitializeComponent 中标记属性集:
-
如果在上面的场景2中,在 UninitializeComponent 就标记属性集为垃圾:
- 当关卡重新加载时,属性集已经被销毁
- 重新初始化时会找不到属性集,导致错误
-
在
OnComponentDestroyed
中标记:- 只有当真正确销毁组件时(场景1),属性集才会被标记为垃圾
- 在临时反初始化场景(场景2)中,属性集得以保留,可以在重新初始化时继续使用
设计意义:
这段代码体现了资源管理的精细控制,区分了"临时停用"和"永久销毁"两种情况,确保在组件生命周期管理中既不会内存泄漏,也不会在需要重新使用时丢失重要资源。
四、TickComponent
void UAbilitySystemComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{ // 开始性能统计范围,用于统计AbilityTasks的Tick耗时// 在性能分析工具中可以看到这个统计项SCOPE_CYCLE_COUNTER(STAT_TickAbilityTasks);// CSV性能统计,用于在CSV文件中记录AbilityTasks的独占执行时间CSV_SCOPED_TIMING_STAT_EXCLUSIVE(AbilityTasks);// 如果当前组件在权威端(服务器端),则更新动画蒙太奇的复制数据// 这确保服务器的动画状态能够正确同步到客户端if (IsOwnerActorAuthoritative()){AnimMontage_UpdateReplicatedData();}// 调用父类的TickComponent,确保基础组件的每帧逻辑得到执行Super::TickComponent(DeltaTime, TickType, ThisTickFunction);// 遍历所有生成的属性集,检查是否有需要每帧更新的可Tick属性集for (UAttributeSet* AttributeSet : GetSpawnedAttributes()){// 尝试将属性集转换为可Tick的接口ITickableAttributeSetInterface* TickableSet = Cast<ITickableAttributeSetInterface>(AttributeSet);// 如果属性集实现了可Tick接口且应该被Tick,则调用其Tick函数if (TickableSet && TickableSet->ShouldTick()){TickableSet->Tick(DeltaTime);}}
}
结合具体例子详细说明:
假设我们有一个多人游戏中的英雄角色 HeroCharacter
,它包含:
- 一个
AbilitySystemComponent
- 一个
HealthAttributeSet
(基础生命值属性) - 一个
SpecialAttributeSet
(特殊属性集,实现了ITickableAttributeSetInterface
)
执行流程和具体作用:
- 性能统计
SCOPE_CYCLE_COUNTER(STAT_TickAbilityTasks);
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(AbilityTasks);
- 在开发阶段,这些宏帮助开发者监控AbilityTasks的性能
- 如果发现
STAT_TickAbilityTasks
耗时过高,说明需要优化能力任务逻辑
- 服务器端动画同步
if (IsOwnerActorAuthoritative())
{AnimMontage_UpdateReplicatedData();
}
例子:英雄在服务器端施放"旋风斩"技能,播放相应的动画蒙太奇:
- 服务器会通过
AnimMontage_UpdateReplicatedData()
将动画状态(播放位置、速率等)同步到所有客户端 - 客户端收到数据后,会同步播放相同的动画,确保所有玩家看到的动画效果一致
- 父类Tick
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
- 执行
UActorComponent
的基础Tick逻辑 - 处理组件的基础更新任务
- 可Tick属性集的更新
for (UAttributeSet* AttributeSet : GetSpawnedAttributes())
{ITickableAttributeSetInterface* TickableSet = Cast<ITickableAttributeSetInterface>(AttributeSet);if (TickableSet && TickableSet->ShouldTick()){TickableSet->Tick(DeltaTime);}
}
具体例子:
HealthAttributeSet
:普通属性集,不需要每帧更新SpecialAttributeSet
:实现了ITickableAttributeSetInterface
,可能需要每帧更新:
场景1 - 持续恢复效果:
// 在SpecialAttributeSet的Tick函数中
void USpecialAttributeSet::Tick(float DeltaTime)
{// 每帧恢复1%的法力值float ManaRegen = GetMaxMana() * 0.01f * DeltaTime;SetMana(FMath::Min(GetMana() + ManaRegen, GetMaxMana()));
}
场景2 - 随时间衰减的buff:
void USpecialAttributeSet::Tick(float DeltaTime)
{// 随时间减少移动速度加成if (GetMoveSpeedBonus() > 0.0f){float DecayAmount = 10.0f * DeltaTime; // 每秒衰减10个单位SetMoveSpeedBonus(FMath::Max(0.0f, GetMoveSpeedBonus() - DecayAmount));}
}
场景3 - 周期性效果检查:
void USpecialAttributeSet::Tick(float DeltaTime)
{CooldownTimer += DeltaTime;if (CooldownTimer >= 1.0f) // 每秒执行一次{CooldownTimer = 0.0f;// 检查并处理周期性效果ProcessPeriodicEffects();}
}
设计意义:
- 性能优化:通过接口检查,只有真正需要Tick的属性集才会被更新,避免不必要的性能开销
- 扩展性:允许属性集实现复杂的随时间变化的逻辑,而不需要依赖外部的Timer或Tick管理
- 网络同步:确保在权威端正确处理需要网络同步的动画数据
这种设计使得属性系统既能够处理静态的属性值,也能够处理动态的、随时间变化的属性逻辑,为复杂的游戏机制提供了强大的支持。
五、InitAbilityActorInfo
void UAbilitySystemComponent::InitAbilityActorInfo(AActor* InOwnerActor, AActor* InAvatarActor)
{// 验证AbilityActorInfo指针有效,如果无效则触发断言check(AbilityActorInfo.IsValid());// 记录AvatarActor是否之前为空,用于后续判断是否需要处理延迟的GameplayCuesbool WasAbilityActorNull = (AbilityActorInfo->AvatarActor == nullptr);// 检查AvatarActor是否发生变化bool AvatarChanged = (InAvatarActor != AbilityActorInfo->AvatarActor);// 使用新的Owner和Avatar Actor初始化AbilityActorInfo结构体// 这个结构体包含了GAS系统需要知道的关于Actor的所有信息AbilityActorInfo->InitFromActor(InOwnerActor, InAvatarActor, this);// 设置组件的主人Actor(通常是PlayerController或AI Controller)SetOwnerActor(InOwnerActor);// 缓存之前的AvatarActor以便后续比较,然后设置新的AvatarActor// AvatarActor通常是实际表现能力的Pawn或Characterconst AActor* PrevAvatarActor = GetAvatarActor_Direct();SetAvatarActor_Direct(InAvatarActor);// 如果之前AvatarActor为空而现在不为空,需要处理延迟的GameplayCues// 这些Cues可能因为之前没有AvatarActor而无法在NetDeltaSerialize中执行if ((WasAbilityActorNull || PrevAvatarActor == nullptr) && InAvatarActor != nullptr){HandleDeferredGameplayCues(&ActiveGameplayEffects);}// 如果AvatarActor发生变化,通知所有相关的能力实例if (AvatarChanged){ABILITYLIST_SCOPE_LOCK(); // 锁定能力列表,防止在遍历时被修改// 遍历所有可激活的能力规格for (FGameplayAbilitySpec& Spec : ActivatableAbilities.Items){if (Spec.Ability){// 根据能力的实例化策略分别处理if (Spec.Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerActor){// 对于每个Actor实例化一次的能力,获取其主要实例UGameplayAbility* AbilityInstance = Spec.GetPrimaryInstance();// 如果能力实例存在,通知它Avatar已设置if (AbilityInstance){AbilityInstance->OnAvatarSet(AbilityActorInfo.Get(), Spec);}}else{// 对于非实例化或每次激活都实例化的能力,直接通知能力类Spec.Ability->OnAvatarSet(AbilityActorInfo.Get(), Spec);}}}}// 获取全局的GameplayTag响应表并注册此组件响应事件if (UGameplayTagReponseTable* TagTable = UAbilitySystemGlobals::Get().GetGameplayTagResponseTable()){TagTable->RegisterResponseForEvents(this);}// 重置本地动画蒙太奇信息LocalAnimMontageInfo = FGameplayAbilityLocalAnimMontage();// 如果在权威端(服务器),重置复制的动画蒙太奇信息if (IsOwnerActorAuthoritative()){SetRepAnimMontageInfo(FGameplayAbilityRepAnimMontage());}// 如果有等待处理的蒙太奇复制,立即处理if (bPendingMontageRep){OnRep_ReplicatedAnimMontage();}
}
结合具体例子详细说明:
假设我们有一个多人游戏场景,玩家控制一个英雄角色:
场景:玩家重生或切换角色
// 当玩家重生或获得新角色时调用
InitAbilityActorInfo(PlayerController, NewHeroCharacter);
执行流程和具体作用:
- 初始检查和状态记录
check(AbilityActorInfo.IsValid());
bool WasAbilityActorNull = (AbilityActorInfo->AvatarActor == nullptr);
bool AvatarChanged = (InAvatarActor != AbilityActorInfo->AvatarActor);
- 确保AbilityActorInfo有效
- 记录之前的AvatarActor状态,用于后续判断
- 初始化Actor信息
AbilityActorInfo->InitFromActor(InOwnerActor, InAvatarActor, this);
SetOwnerActor(InOwnerActor);
例子:
InOwnerActor
=PlayerController
(控制者)InAvatarActor
=HeroCharacter
(实际角色)- 这样设置后,GAS知道哪个Controller拥有能力,哪个Character执行能力
- 处理延迟的GameplayCues
if ((WasAbilityActorNull || PrevAvatarActor == nullptr) && InAvatarActor != nullptr)
{HandleDeferredGameplayCues(&ActiveGameplayEffects);
}
具体场景:
- 玩家死亡后重生,之前的AvatarActor为null
- 重生后获得新的HeroCharacter
- 这时需要执行之前因为缺少Avatar而延迟的视觉效果
- 比如:重生时立即显示的血条特效、复活光环等
- 通知能力实例Avatar变化
if (AvatarChanged)
{for (FGameplayAbilitySpec& Spec : ActivatableAbilities.Items){// 通知每个能力实例}
}
例子:
- 玩家从"法师"切换到"战士"角色
- 所有能力都需要知道新的AvatarActor
OnAvatarSet
让能力有机会:- 绑定到新角色的骨骼网格体
- 重新设置动画实例
- 更新能力特效的附着点
- 重新计算基于新角色属性的数值
- GameplayTag响应表注册
if (UGameplayTagReponseTable* TagTable = UAbilitySystemGlobals::Get().GetGameplayTagResponseTable())
{TagTable->RegisterResponseForEvents(this);
}
作用:
- 注册组件响应GameplayTag事件
- 当特定的GameplayTag被添加或移除时,自动触发对应的效果
- 比如:当角色获得"Burning"标签时自动播放燃烧特效
- 动画蒙太奇系统重置
LocalAnimMontageInfo = FGameplayAbilityLocalAnimMontage();
if (IsOwnerActorAuthoritative())
{SetRepAnimMontageInfo(FGameplayAbilityRepAnimMontage());
}
if (bPendingMontageRep)
{OnRep_ReplicatedAnimMontage();
}
作用:
- 重置所有动画蒙太奇状态
- 在服务器端初始化复制数据
- 处理任何等待的蒙太奇复制,确保动画同步
典型应用场景:
-
角色重生:
- 玩家死亡后重生,需要重新初始化所有能力到新角色
-
角色切换:
- 在MOBA游戏中切换英雄
- 在RPG游戏中切换职业
-
坐骑系统:
- 玩家上马/下马,AvatarActor发生变化
-
附身控制:
- AI或玩家控制不同的角色实体
设计意义:
这个函数是GAS系统中Actor身份变化的核心处理点,确保当控制权或物理表现实体发生变化时,所有能力、效果和视觉表现都能正确迁移到新的Actor上,维持游戏状态的连贯性和正确性。
六、GetShouldTick
bool UAbilitySystemComponent::GetShouldTick() const
{// 检查是否需要更新复制的动画蒙太奇信息:// - 必须在权威端(服务器)// - 并且复制的蒙太奇没有停止const bool bHasReplicatedMontageInfoToUpdate = (IsOwnerActorAuthoritative() && GetRepAnimMontageInfo().IsStopped == false);// 如果需要更新复制的蒙太奇信息,则返回true(需要Tick)if (bHasReplicatedMontageInfoToUpdate){return true;}// 先检查父类是否需要Tickbool bResult = Super::GetShouldTick(); // 如果父类不需要Tick,再检查是否有需要Tick的属性集if (bResult == false){// 遍历所有生成的属性集for (const UAttributeSet* AttributeSet : GetSpawnedAttributes()){// 检查属性集是否实现了ITickableAttributeSetInterface接口const ITickableAttributeSetInterface* TickableAttributeSet = Cast<const ITickableAttributeSetInterface>(AttributeSet);// 如果属性集可Tick且应该被Tick,则返回trueif (TickableAttributeSet && TickableAttributeSet->ShouldTick()){bResult = true;break; // 找到一个需要Tick的属性集就足够,可以跳出循环}}}return bResult;
}
结合具体例子详细说明:
这个函数决定了 UAbilitySystemComponent
是否需要每帧进行更新(Tick),是一个性能优化的关键函数。
场景分析
场景1:服务器端播放动画蒙太奇
// 服务器端,英雄正在播放攻击动画蒙太奇
const bool bHasReplicatedMontageInfoToUpdate = (true && false == false); // 返回true
- 英雄在服务器端施放"旋风斩"技能,播放动画蒙太奇
IsOwnerActorAuthoritative()
=true
(服务器端)- GetRepAnimMontageInfo().IsStopped = false(动画正在播放)
- 结果:返回
true
,组件需要Tick来同步动画状态到客户端
场景2:客户端接收动画同步
// 客户端,只是接收服务器的动画同步数据
const bool bHasReplicatedMontageInfoToUpdate = (false && ...); // 返回false
- 客户端上的同一个英雄角色
IsOwnerActorAuthoritative()
=false
(客户端)- 结果:第一个条件不满足,继续检查其他条件
场景3:有需要Tick的属性集
// 假设有一个需要每帧更新的特殊属性集
class URegenerationAttributeSet : public UAttributeSet, public ITickableAttributeSetInterface
{
public:virtual bool ShouldTick() const override { return true; }virtual void Tick(float DeltaTime) override {// 每帧恢复生命值Health = FMath::Min(Health + HealthRegenRate * DeltaTime, MaxHealth);}
};
在这种情况下:
- 父类
Super::GetShouldTick()
返回false
- 遍历属性集时发现
URegenerationAttributeSet
实现了ITickableAttributeSetInterface
TickableAttributeSet->ShouldTick()
返回true
- 结果:返回
true
,组件需要Tick来更新属性集的逻辑
场景4:没有任何需要Tick的情况
// 服务器端没有播放动画,也没有需要Tick的属性集
const bool bHasReplicatedMontageInfoToUpdate = (true && true); // GetRepAnimMontageInfo().IsStopped == true
bool bResult = Super::GetShouldTick(); // 假设返回false
// 遍历所有属性集,没有找到需要Tick的
- 服务器端英雄处于闲置状态,没有播放动画
- 所有属性集都不需要Tick
- 结果:返回
false
,组件不需要Tick,节省性能
设计意义和性能优化
- 按需Tick:只在真正需要的时候才进行Tick,避免不必要的性能开销
- 分层检查:
- 首先检查最高优先级的蒙太奇同步需求
- 然后检查父类的基础需求
- 最后检查属性集的Tick需求
- 网络优化:
- 服务器需要Tick来同步动画状态
- 客户端通常不需要为此Tick(除非有本地预测等高级功能)
- 可扩展性:
- 通过
ITickableAttributeSetInterface
接口,允许属性集声明自己的Tick需求 - 新的属性集可以轻松集成到现有的Tick系统中
- 通过
实际应用例子
游戏中的持续恢复效果:
// 生命恢复属性集
class UHealthRegenAttributeSet : public UAttributeSet, public ITickableAttributeSetInterface
{float Health;float MaxHealth;float HealthRegenRate; // 每秒恢复量public:virtual bool ShouldTick() const override { // 只有当生命值不满且恢复率大于0时才需要Tickreturn Health < MaxHealth && HealthRegenRate > 0.0f; }virtual void Tick(float DeltaTime) override {Health = FMath::Min(Health + HealthRegenRate * DeltaTime, MaxHealth);}
};
在这种情况下,只有当角色生命值不满且确实在恢复时,属性集才会要求Tick,进一步优化了性能。
这种精细的Tick控制机制确保了GAS系统在提供强大功能的同时,也能保持良好的运行时性能。
七、SetAvatarActor
void UAbilitySystemComponent::SetAvatarActor(AActor* InAvatarActor)
{// 验证AbilityActorInfo指针有效,如果无效则触发断言崩溃// 确保在设置AvatarActor之前,AbilityActorInfo已经被正确初始化check(AbilityActorInfo.IsValid());// 使用当前的OwnerActor和新的AvatarActor重新初始化能力Actor信息// 这会更新GAS系统中所有相关的引用和状态,确保系统知道新的AvatarActorInitAbilityActorInfo(GetOwnerActor(), InAvatarActor);
}
结合具体例子详细说明:
这个函数是GAS系统中一个关键的接口函数,用于动态改变AvatarActor(即实际表现和执行能力的Actor)。
典型应用场景
场景1:角色坐骑系统
// 玩家上马
void AHeroCharacter::MountHorse(AHorse* Horse)
{// 将AbilitySystemComponent的AvatarActor设置为马AbilitySystemComponent->SetAvatarActor(Horse);// 现在所有的能力都会在马这个Actor上执行// 比如"冲刺"能力现在会让马冲刺,而不是角色自己冲刺
}// 玩家下马
void AHeroCharacter::DismountHorse()
{// 将AvatarActor恢复为角色自己AbilitySystemComponent->SetAvatarActor(this);
}
执行流程:
check(AbilityActorInfo.IsValid())
- 确保AbilityActorInfo有效- I
nitAbilityActorInfo(GetOwnerActor(), Horse)
- 重新初始化,通知所有能力系统现在Avatar是马 - 所有激活的能力、GameplayEffect和GameplayCue都会重新绑定到新的AvatarActor
场景2:角色变形/变身
// 英雄变身为巨龙
void AHeroCharacter::TransformToDragon(ADragonForm* DragonForm)
{// 保存当前形态PreviousForm = this;// 切换到巨龙形态AbilitySystemComponent->SetAvatarActor(DragonForm);// 现在能力系统会在巨龙Actor上执行// "喷火"能力现在会从龙的嘴里喷出火焰
}
场景3:灵魂附体/控制转移
// 玩家控制另一个角色
void APlayerController::PossessNewCharacter(ACharacter* NewCharacter)
{// 解除对原角色的控制UnPossess();// 控制新角色Possess(NewCharacter);// 更新AbilitySystemComponent的AvatarActorAbilitySystemComponent->SetAvatarActor(NewCharacter);
}
场景4:分身体系
// 创建镜像分身
void AMageCharacter::CreateMirrorImage()
{AMirrorImage* MirrorImage = SpawnMirrorImage();// 将AbilitySystemComponent切换到分身// 这样玩家可以控制分身施放技能AbilitySystemComponent->SetAvatarActor(MirrorImage);// 设置定时器,一段时间后切换回来GetWorldTimerManager().SetTimer(SwitchBackTimer, this, &AMageCharacter::SwitchBackToMainBody, 10.0f);
}void AMageCharacter::SwitchBackToMainBody()
{// 切换回主体AbilitySystemComponent->SetAvatarActor(this);
}
底层发生的变化
当调用 SetAvatarActor
时,InitAbilityActorInfo
会执行以下关键操作:
- 更新AbilityActorInfo引用:
AbilityActorInfo->InitFromActor(GetOwnerActor(), InAvatarActor, this);
-
处理延迟的GameplayCues:
- 如果之前没有AvatarActor,现在有了,会执行之前延迟的视觉效果
-
通知所有能力实例:
- 调用每个能力的
OnAvatarSet
,让能力有机会重新绑定到新的AvatarActor
- 调用每个能力的
-
重置动画系统:
- 清理旧的动画蒙太奇状态
- 初始化新的动画复制数据
设计意义
- 动态角色系统:允许在运行时切换控制的目标,支持坐骑、变身、附体等复杂游戏机制
- 能力复用:同样的能力可以在不同的AvatarActor上执行,实现"一套能力,多种表现"
- 网络同步:确保AvatarActor的变化在网络游戏中正确同步
- 资源管理:正确处理新旧AvatarActor之间的资源绑定和解绑
注意事项
// 重要:在切换AvatarActor前需要确保
void SafeAvatarSwitch(AActor* NewAvatar)
{// 1. 检查AbilitySystemComponent是否有效if (!AbilitySystemComponent || !AbilitySystemComponent->AbilityActorInfo.IsValid()){return;}// 2. 检查新Avatar是否有效if (!NewAvatar){UE_LOG(LogTemp, Warning, TEXT("Attempting to set null AvatarActor!"));return;}// 3. 执行安全的Avatar切换AbilitySystemComponent->SetAvatarActor(NewAvatar);
}
这个函数虽然代码简单,但通过调用 InitAbilityActorInfo
触发了GAS系统中一系列重要的重新初始化操作,是支持动态Avatar系统的核心接口。
八、ClearActorInfo
void UAbilitySystemComponent::ClearActorInfo()
{// 验证AbilityActorInfo指针有效,如果无效则触发断言崩溃// 确保在清理操作前AbilityActorInfo已经被正确初始化check(AbilityActorInfo.IsValid());// 清除AbilityActorInfo中存储的所有Actor相关信息// 这包括OwnerActor、AvatarActor、AnimInstance、SkeletalMeshComponent等关键引用AbilityActorInfo->ClearActorInfo();// 将组件的OwnerActor设置为nullptr// OwnerActor通常是控制这个能力系统的Controller(玩家控制器或AI控制器)SetOwnerActor(nullptr);// 直接将AvatarActor设置为nullptr// AvatarActor是实际执行能力和表现效果的物理实体(如Character或Pawn)SetAvatarActor_Direct(nullptr);
}
结合具体例子详细说明:
这个函数用于完全清除GAS系统与Actor的关联,通常在需要彻底断开连接时使用。
典型应用场景
场景1:角色死亡和彻底销毁
void AHeroCharacter::OnDeath()
{// 1. 首先停止所有活跃的能力和效果AbilitySystemComponent->CancelAllAbilities();AbilitySystemComponent->RemoveAllActiveGameplayEffects();// 2. 清除Actor信息,断开GAS系统与角色的关联AbilitySystemComponent->ClearActorInfo();// 3. 然后销毁角色(或进入死亡状态)StartDeathSequence();
}
执行流程分析:
-
check(AbilityActorInfo.IsValid())
- 确保有有效的Actor信息可以清理 -
AbilityActorInfo->ClearActorInfo()
- 清除所有Actor相关引用:OwnerActor
= nullptrAvatarActor
= nullptrAnimInstance
= nullptrSkeletalMeshComponent
= nullptr- 其他组件引用全部重置
-
SetOwnerActor(nullptr)
- 明确设置Owner为null -
SetAvatarActor_Direct(nullptr)
- 明确设置Avatar为null
场景2:关卡流式卸载
// 当关卡被卸载时,清理所有角色的GAS关联
void UGameLevel::OnLevelUnloaded()
{for (AHeroCharacter* Hero : AllHeroesInLevel){if (Hero && Hero->AbilitySystemComponent){// 清除Actor信息,但保留组件本身Hero->AbilitySystemComponent->ClearActorInfo();}}
}
场景3:角色重置/重生前的清理
void AHeroCharacter::ResetCharacter()
{// 在重新初始化前先完全清理AbilitySystemComponent->ClearActorInfo();// 然后重新设置到新的ActorAbilitySystemComponent->InitAbilityActorInfo(PlayerController, this);
}
底层发生的变化
AbilityActorInfo->ClearActorInfo()
具体清理的内容:
// 模拟FGameplayAbilityActorInfo::ClearActorInfo()的实现
void FGameplayAbilityActorInfo::ClearActorInfo()
{OwnerActor = nullptr;AvatarActor = nullptr;PlayerController = nullptr;AnimInstance = nullptr;SkeletalMeshComponent = nullptr;MovementComponent = nullptr;// 其他组件引用...
}
与相关函数的对比
ClearActorInfo()
vs UninitializeComponent()
:
// ClearActorInfo() - 只清理Actor引用,组件本身保持活动
void Example1()
{AbilitySystemComponent->ClearActorInfo();// 组件仍然存在,可以后续重新初始化// 适用于角色重生、重新初始化等场景
}// UninitializeComponent() - 完整的组件反初始化
void Example2()
{AbilitySystemComponent->UninitializeComponent();// 组件进入非活动状态,清理所有内部状态// 适用于组件即将被销毁的场景
}
设计意义和注意事项
设计意义:
- 安全断开:确保在Actor生命周期结束时安全地断开GAS系统的关联
- 防止悬空指针:避免持有对已销毁Actor的引用,防止崩溃
- 状态重置:为重新初始化提供干净的基础
- 资源管理:确保所有Actor相关资源被正确释放
重要注意事项:
void SafeClearActorInfo()
{// 在调用ClearActorInfo之前应该:// 1. 取消所有活跃能力AbilitySystemComponent->CancelAllAbilities();// 2. 移除所有活跃效果AbilitySystemComponent->RemoveAllActiveGameplayEffects();// 3. 停止所有能力任务AbilitySystemComponent->CancelAllTasks();// 4. 然后执行清理AbilitySystemComponent->ClearActorInfo();
}
错误使用示例:
void IncorrectUsage()
{// 错误:在清理后尝试使用能力系统AbilitySystemComponent->ClearActorInfo();AbilitySystemComponent->TryActivateAbility(SomeAbilityHandle); // 崩溃!没有有效的Actor信息// 正确做法:清理后要么重新初始化,要么不再使用
}
这个函数是GAS系统生命周期管理的关键部分,确保在Actor关系发生变化时能够安全、干净地进行状态转移,是构建健壮的角色系统的重要基础。
九、OnRep_OwningActor
void UAbilitySystemComponent::OnRep_OwningActor()
{// 确保AbilityActorInfo有效,如果无效则输出错误并返回// ensure宏在调试模式下会弹出断言对话框,在发布模式下会记录错误if (!ensure(AbilityActorInfo.IsValid())){return;}// 获取当前的OwnerActor和AvatarActorAActor* LocalOwnerActor = GetOwnerActor();AActor* LocalAvatarActor = GetAvatarActor_Direct();// 检查当前的Actor与AbilityActorInfo中存储的是否一致// 如果不一致,说明发生了网络复制,需要更新状态if (LocalOwnerActor != AbilityActorInfo->OwnerActor || LocalAvatarActor != AbilityActorInfo->AvatarActor){// 如果新的OwnerActor有效,重新初始化能力Actor信息if (LocalOwnerActor != nullptr){InitAbilityActorInfo(LocalOwnerActor, LocalAvatarActor);}else{// 如果OwnerActor变为nullptr,清除所有Actor信息ClearActorInfo();}}
}
结合具体例子详细说明:
这是一个网络复制通知函数,当 OwningActor 在网络复制过程中发生变化时被自动调用。
网络复制背景
在虚幻引擎的网络模型中:
- 服务器是权威端,拥有真实数据
- 客户端通过网络复制接收服务器数据的变化
OnRep_
函数在客户端检测到复制变量变化时自动调用
典型应用场景
场景1:玩家控制器切换角色(网络游戏)
// 服务器端
void APlayerController::PossessNewCharacter(ACharacter* NewCharacter)
{// 服务器设置新的OwningActorAbilitySystemComponent->SetOwnerActor(NewCharacter);// 这个变化会自动通过网络复制到客户端
}// 客户端 - 自动调用OnRep_OwningActor
void UAbilitySystemComponent::OnRep_OwningActor()
{// LocalOwnerActor 现在是通过网络复制得到的新角色// LocalAvatarActor 可能还是旧的Avatar// 检测到变化,调用InitAbilityActorInfo重新初始化InitAbilityActorInfo(NewCharacter, NewCharacter);
}
执行流程分析:
- 有效性检查
if (!ensure(AbilityActorInfo.IsValid()))
{return;
}
- 确保AbilityActorInfo有效,防止空指针访问
- 如果无效,记录错误并提前返回
- 获取当前Actor引用
AActor* LocalOwnerActor = GetOwnerActor();
AActor* LocalAvatarActor = GetAvatarActor_Direct();
- 获取通过网络复制得到的最新OwnerActor
- 获取当前的AvatarActor引用
- 检测变化
if (LocalOwnerActor != AbilityActorInfo->OwnerActor || LocalAvatarActor != AbilityActorInfo->AvatarActor)
- 比较复制得到的新值与AbilityActorInfo中存储的旧值
- 如果发现不一致,说明发生了网络复制变化
- 处理变化
if (LocalOwnerActor != nullptr)
{InitAbilityActorInfo(LocalOwnerActor, LocalAvatarActor);
}
else
{ClearActorInfo();
}
- 如果新的OwnerActor有效:重新初始化整个系统
- 如果新的OwnerActor为空:彻底清理系统
具体网络游戏示例
MOBA游戏中的英雄重生:
// 服务器端 - 英雄死亡后重生
void AHeroPlayerState::OnRespawn()
{// 服务器生成新的英雄角色AHeroCharacter* NewHero = SpawnNewHero();// 设置AbilitySystemComponent的Owner为新英雄AbilitySystemComponent->SetOwnerActor(NewHero);// 这个变化会通过网络复制到客户端
}// 客户端 - 自动接收变化
void UAbilitySystemComponent::OnRep_OwningActor()
{// 此时:// LocalOwnerActor = 新重生的英雄角色// AbilityActorInfo->OwnerActor = 旧的已死亡角色(或nullptr)// 检测到变化,重新初始化能力系统InitAbilityActorInfo(NewHero, NewHero);// 现在客户端的能力系统正确关联到新英雄// 可以正常显示技能UI、处理输入等
}
MMORPG中的坐骑系统:
// 服务器端 - 玩家上马
void APlayerCharacter::Server_MountHorse_Implementation(AHorse* Horse)
{// 服务器设置Avatar为马AbilitySystemComponent->SetAvatarActor(Horse);
}// 客户端 - 接收Avatar变化
void UAbilitySystemComponent::OnRep_OwningActor()
{// 检测到AvatarActor从玩家角色变为马if (LocalOwnerActor != nullptr){// 重新初始化,现在能力在马身上执行InitAbilityActorInfo(PlayerController, Horse);// 更新UI显示马的技能UpdateAbilityUI();}
}
设计意义和网络同步
为什么需要这个函数:
- 网络状态同步:确保客户端的能力系统状态与服务器保持一致
- 预测修正:当客户端的预测与服务器权威状态不一致时进行修正
- 无缝切换:支持在网络游戏中动态切换控制的目标
- 错误恢复:在网络异常情况下恢复正确的状态
网络复制流程:
服务器改变OwnerActor → 网络复制 → 客户端检测变化 → OnRep_OwningActor → 重新初始化
错误处理场景
void UAbilitySystemComponent::OnRep_OwningActor()
{// 场景1:网络延迟导致暂时性不一致// ensure确保系统健壮性,不会因为临时状态崩溃// 场景2:服务器清理了Actor,客户端也需要清理if (LocalOwnerActor == nullptr){ClearActorInfo(); // 安全清理ShowReconnectMessage(); // 显示重新连接UI}// 场景3:客户端预测错误,需要根据服务器状态修正if (LocalOwnerActor != AbilityActorInfo->OwnerActor){// 放弃本地预测,采用服务器权威状态InitAbilityActorInfo(LocalOwnerActor, LocalAvatarActor);}
}
这个函数是GAS系统在网络游戏中保持同步的关键机制,确保当控制权或Avatar在网络中发生变化时,所有客户端都能正确更新其本地状态,维持游戏的连贯性和一致性。
十、RefreshAbilityActorInfo
void UAbilitySystemComponent::RefreshAbilityActorInfo()
{// 验证AbilityActorInfo指针有效,如果无效则触发断言崩溃// 确保在执行刷新操作前AbilityActorInfo已经被正确初始化check(AbilityActorInfo.IsValid());// 使用当前存储的OwnerActor和AvatarActor重新初始化AbilityActorInfo// 这会刷新所有Actor相关的组件引用(如AnimInstance、SkeletalMeshComponent等)// 但不会改变OwnerActor和AvatarActor本身AbilityActorInfo->InitFromActor(AbilityActorInfo->OwnerActor.Get(), AbilityActorInfo->AvatarActor.Get(), this);
}
结合具体例子详细说明:
这个函数用于刷新AbilityActorInfo中的组件引用,而不改变OwnerActor和AvatarActor本身。
与相关函数的对比
首先理解这个函数与其他初始化函数的区别:
// InitAbilityActorInfo - 完全重新初始化,改变Owner/Avatar
void FullReinit()
{// 改变Actor引用,重新设置所有状态InitAbilityActorInfo(NewOwner, NewAvatar);
}// RefreshAbilityActorInfo - 刷新组件引用,不改变Actor
void RefreshOnly()
{// 不改变Owner/Avatar,只刷新组件引用RefreshAbilityActorInfo();
}
典型应用场景
场景1:骨骼网格体动态切换
// 角色更换装备或外观
void AHeroCharacter::ChangeSkeletalMesh(USkeletalMesh* NewMesh)
{// 更换骨骼网格体GetMesh()->SetSkeletalMesh(NewMesh);// 刷新AbilityActorInfo以更新AnimInstance等引用AbilitySystemComponent->RefreshAbilityActorInfo();// 现在能力系统知道新的骨骼网格体和动画实例// 动画能力可以正确播放在新的骨骼网格体上
}
执行流程分析:
check(AbilityActorInfo.IsValid())
- 确保有有效的Actor信息可以刷新AbilityActorInfo->InitFromActor(...)
- 重新初始化组件引用:- 更新
AnimInstance
引用到新的骨骼网格体的动画实例 - 更新
SkeletalMeshComponent
引用到新的网格体组件 - 更新其他组件引用(MovementComponent等)
- 更新
场景2:动画蓝图重新编译后
// 当动画蓝图在编辑器中重新编译后
void AHeroCharacter::OnAnimBlueprintRecompiled()
{// 刷新AbilityActorInfo以获取新的AnimInstanceAbilitySystemComponent->RefreshAbilityActorInfo();// 现在GameplayAbility可以正确访问重新编译后的动画蓝图
}
场景3:组件在运行时被替换
// 动态替换移动组件
void AHeroCharacter::ReplaceMovementComponent()
{// 移除旧组件UMovementComponent* OldComponent = GetMovementComponent();OldComponent->DestroyComponent();// 创建新组件UMovementComponent* NewComponent = NewObject<UMovementComponent>(this);NewComponent->RegisterComponent();// 刷新AbilityActorInfo以获取新的MovementComponent引用AbilitySystemComponent->RefreshAbilityActorInfo();
}
底层发生的变化
InitFromActor
具体刷新的内容:
// 模拟FGameplayAbilityActorInfo::InitFromActor的实现细节
void FGameplayAbilityActorInfo::InitFromActor(AActor* InOwnerActor, AActor* InAvatarActor, UObject* InOwnerComponent)
{// 更新组件引用,但不改变Owner/Avatarif (InAvatarActor){// 重新获取骨骼网格体组件SkeletalMeshComponent = InAvatarActor->FindComponentByClass<USkeletalMeshComponent>();// 重新获取动画实例if (SkeletalMeshComponent){AnimInstance = SkeletalMeshComponent->GetAnimInstance();}// 重新获取移动组件MovementComponent = InAvatarActor->FindComponentByClass<UMovementComponent>();// 重新获取其他相关组件...}
}
具体游戏机制示例
装备系统 - 武器切换:
void AHeroCharacter::EquipWeapon(AWeapon* NewWeapon)
{// 卸下当前武器if (CurrentWeapon){CurrentWeapon->Unequip();}// 装备新武器CurrentWeapon = NewWeapon;CurrentWeapon->EquipTo(GetMesh(), "WeaponSocket");// 武器可能带有新的骨骼网格体或动画// 刷新AbilityActorInfo以确保能力系统使用正确的组件AbilitySystemComponent->RefreshAbilityActorInfo();// 现在"攻击"能力会使用新武器的动画和特效
}
形态变换 - 组件更新:
void AShapeShifter::TransformToWolf()
{// 切换到狼的骨骼网格体GetMesh()->SetSkeletalMesh(WolfSkeletalMesh);GetMesh()->SetAnimInstanceClass(WolfAnimBlueprint);// 刷新组件引用AbilitySystemComponent->RefreshAbilityActorInfo();// 现在能力系统知道我们使用的是狼的动画实例// "嚎叫"能力可以正确播放狼的嚎叫动画
}
设计意义
- 动态组件支持:允许在运行时更换组件而不破坏能力系统的功能
- 热重载兼容:支持在编辑器中进行动画蓝图等资源的热重载
- 性能优化:比完全重新初始化更轻量,只更新必要的组件引用
- 错误恢复:当组件引用意外失效时,可以通过刷新恢复功能
注意事项
void AppropriateUsage()
{// 适合使用RefreshAbilityActorInfo的场景:// 1. 组件引用发生变化,但Actor身份不变// 2. 需要更新AnimInstance、SkeletalMeshComponent等引用// 3. 修复悬空的组件指针// 不适合使用的场景:// 1. OwnerActor或AvatarActor本身发生变化(应使用InitAbilityActorInfo)// 2. 需要完全重新初始化能力系统// 3. Actor被销毁或彻底更换
}
这个函数是GAS系统中维护组件引用一致性的重要工具,特别适用于那些需要在运行时动态更换组件而不改变Actor身份的游戏机制。