尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

UE5 GAS实战:ARPG战斗框架从设计到落地

UE5 GAS实战:ARPG战斗框架从设计到落地 1. 为什么ARPG战斗框架要选GAS做ARPG的人都有一个共识战斗系统是这个品类里最复杂、最容易失控的模块。动作游戏的战斗不是简单的“攻击扣血”它牵扯到连招派生、取消窗口、无敌帧、霸体、元素反应、Buff叠加、冷却缩减、资源消耗与回复、伤害类型克制……这些东西如果全部手写状态机代码量会膨胀到没人敢改的程度。我在早期项目里就吃过这个亏。当时用传统的状态机加事件分发来做战斗一开始只有普攻和三个技能代码还算清爽。等到策划加了“技能释放期间受到特定伤害触发反击”“连击数达到阈值解锁终结技”“不同元素附着产生不同反应”这些需求之后整个战斗模块变成了一团乱麻——状态之间的跳转条件散落在十几个文件里改一个技能经常把另一个技能搞崩。后来转向UE5的GASGameplay Ability System情况才彻底改观。GAS本质上是一套为“能力驱动型玩法”设计的框架它把技能、属性、状态、标签、效果这几件事拆成了独立的模块用一套统一的规则来管理。对于ARPG这种“大量技能大量状态大量交互”的场景GAS几乎是量身定做的。这篇文章我会从实际项目出发把ARPG战斗框架用GAS落地的完整思路拆开讲。包括为什么这么设计、核心模块怎么划分、技能怎么配置、标签怎么用、伤害怎么算、常见坑怎么避。适合已经对UE5有一定了解、正在做或者准备做ARPG战斗系统的朋友参考。如果你还在犹豫要不要上GAS或者已经上了但被各种概念绕晕了这篇应该能帮你理清思路。2. 战斗框架的整体设计与模块拆解2.1 传统状态机方案为什么会失控先说清楚问题才能理解GAS的价值。传统做法一般是这样的定义一个枚举表示角色状态Idle、Move、Attack、Hit、Dead……然后在Tick或者事件回调里根据输入和当前状态决定下一步跳到哪里。技能逻辑写在角色类或者技能类里用if-else或者switch来分支。这套方案在技能数量少、交互简单的时候没问题。但ARPG的战斗有几个特点会让它迅速崩溃第一状态是正交的。一个角色可能同时处于“移动中”“被减速”“正在释放技能”“身上有燃烧Buff”这几个状态。用单一枚举根本表达不了这种组合你只能不断加枚举值最后变成状态爆炸。第二技能之间会互相打断和取消。普攻第三段可以取消前两段的后摇闪避可以取消大部分技能某些技能只能在受击时触发。这些规则如果用硬编码写每加一个技能就要改一遍所有相关技能。第三数值和状态要分离。攻击力、防御力、暴击率这些属性会被Buff临时修改Buff消失后要恢复。手写的话你得自己维护一套“原始值修正值”的计算逻辑很容易出错。GAS的设计恰好对应解决了这三个问题GameplayTag解决状态正交Ability的取消和阻塞机制解决技能交互AttributeSet解决数值修正。这不是巧合GAS本来就是为这类需求设计的。2.2 GAS五大核心模块在ARPG里的分工GAS的核心概念看起来多但放到ARPG战斗框架里每个都有明确的位置。我用一张表把它们的职责和ARPG场景对应起来GAS模块核心职责ARPG中的典型用途AbilitySystemComponent (ASC)能力系统的总入口管理技能、标签、效果挂在角色和武器上驱动整个战斗逻辑GameplayAbility (GA)单个技能的完整逻辑普攻、技能、闪避、受击反应GameplayEffect (GE)对属性的修改和状态的施加伤害、治疗、Buff、Debuff、冷却AttributeSet (AS)定义和存储属性生命、攻击、防御、暴击、元素抗性GameplayTag (GT)层级化的状态标记状态判定、技能阻塞、取消规则理解这五个模块的关系是关键。我习惯用一个类比ASC是“大脑”GA是“动作”GE是“效果”AS是“身体数据”GT是“神经信号”。大脑ASC根据神经信号GT决定能不能做某个动作GA动作产生效果GE效果改变身体数据AS。2.3 为什么ASC要挂在PlayerState而不是Character上这是新手最容易踩的坑之一。很多教程直接把ASC挂在Character上单机跑起来没问题但一到多人就出各种诡异Bug。原因在于Character是会被销毁和重建的。角色死亡后如果做了Ragdoll或者延迟销毁再复活时Character可能是新实例但PlayerState是持久的。如果ASC挂在Character上复活后技能冷却、Buff、属性全部丢失。挂在PlayerState上这些数据就能跨死亡保留。对于ARPG来说这个区别尤其重要。很多ARPG有“死亡后保留Buff”“复活后技能冷却继续走”的需求ASC挂PlayerState是唯一正确的做法。具体配置上在Character里通过接口获取ASC// Character构造函数中 AbilitySystemComponent CastUAbilitySystemComponent( GetPlayerState()-GetAbilitySystemComponent()); // 更稳妥的写法是在PossessedBy和OnRep_PlayerState里初始化 void AMyCharacter::PossessedBy(AController* NewController) { Super::PossessedBy(NewController); InitAbilityActorInfo(); } void AMyCharacter::OnRep_PlayerState() { Super::OnRep_PlayerState(); InitAbilityActorInfo(); }注意InitAbilityActorInfo要在服务端和客户端都调用服务端在PossessedBy里客户端在OnRep_PlayerState里。漏掉任何一个都会导致技能在某一端不生效。2.4 战斗框架的分层结构一个成熟的ARPG战斗框架我一般会分成四层数据层AttributeSet定义所有属性DataTable配置技能数值和GE参数。这一层不包含逻辑只存数据。能力层所有GameplayAbility的实现每个技能一个类或者用同一个类配不同参数。这一层负责“技能做什么”。规则层GameplayTag体系和GE的配置定义“什么状态下能做什么”“做了什么之后变成什么状态”。这一层是战斗手感的灵魂。表现层动画、特效、音效、镜头震动。这一层通过GameplayCue来触发和逻辑层解耦。分层的意义在于策划调数值不用碰代码改Tag规则不用动技能逻辑换特效不影响战斗判定。这在长期迭代的项目里能省下大量沟通成本。3. 核心细节解析与实操要点3.1 GameplayTag体系怎么设计才不乱GameplayTag是GAS里最容易被低估、也最容易用乱的东西。用好了战斗规则清晰得像文档用乱了标签数量爆炸没人记得住哪个标签是干嘛的。我的经验是ARPG的Tag体系要按“维度”来组织而不是按“技能”来组织。具体分几个大类State类描述角色当前状态。比如State.Dead、State.Stunned、State.Invincible、State.Casting。这类标签通常是互斥的或者有明确优先级的。Ability类标记正在释放的技能。比如Ability.Attack.Light、Ability.Skill.Fireball。这类标签用于技能之间的阻塞和取消判定。Effect类标记身上挂着的效果。比如Effect.Buff.AttackUp、Effect.Debuff.Burn。这类标签用于效果叠加和驱散判定。Input类标记输入状态。比如Input.Hold、Input.ComboWindow。这类标签用于连招系统。Event类用于触发特定逻辑。比如Event.Hit.Damage、Event.Combo.Reset。这类标签通常配合AbilityTrigger使用。命名规范上我强烈建议用点分层级并且层级不要超过四层。比如Ability.Skill.Fireball.Cast就比FireballSkillCasting好得多因为前者可以用Ability.Skill来匹配所有技能后者只能精确匹配。实操心得Tag的层级匹配是GAS的核心机制。Ability.Skill.Fireball会自动匹配Ability.Skill和Ability。利用这个特性你可以用父标签做批量判定用子标签做精确判定。比如取消规则里写“所有Ability.Skill都能被闪避取消”就不用一个个技能去配。3.2 技能阻塞与取消的配置逻辑ARPG战斗手感的核心很大程度上取决于技能之间的阻塞和取消规则。GAS提供了几个关键配置项来实现这套规则AbilityTags这个技能自己带的标签。比如普攻带Ability.Attack.Light。BlockAbilitiesWithTag释放这个技能时阻止哪些标签的技能激活。比如大招带BlockAbilitiesWithTag Ability.Attack释放期间就不能普攻。CancelAbilitiesWithTag释放这个技能时取消哪些正在运行的技能。比如闪避带CancelAbilitiesWithTag Ability.Skill闪避可以打断技能。ActivationBlockedTags身上有这些标签时这个技能不能激活。比如受击硬直带State.Stunned所有技能都配ActivationBlockedTags State.Stunned。ActivationOwnedTags技能激活期间给自己加的标签。比如技能释放期间加State.Casting。ActivationRequiredTags必须身上有这些标签才能激活。比如终结技需要State.ComboReady。这套配置组合起来就能表达出复杂的战斗规则。我举个实际例子普攻第三段的设计是“只能在前两段之后触发可以取消前两段的收招但释放期间不能被闪避取消”。配置如下// 普攻第三段 AbilityTags.AddTag(FGameplayTag::RequestGameplayTag(Ability.Attack.Light.Heavy)); ActivationRequiredTags.AddTag(FGameplayTag::RequestGameplayTag(State.ComboWindow)); CancelAbilitiesWithTag.AddTag(FGameplayTag::RequestGameplayTag(Ability.Attack.Light)); ActivationBlockedTags.AddTag(FGameplayTag::RequestGameplayTag(State.Stunned)); // 不配置CancelAbilitiesWithTag包含闪避所以闪避不能取消它注意CancelAbilitiesWithTag和BlockAbilitiesWithTag的区别要搞清楚。Cancel是“打断正在运行的”Block是“阻止还没运行的”。闪避要能打断技能用Cancel大招要阻止普攻用Block。3.3 连招窗口的实现方式连招是ARPG战斗的核心体验之一。GAS本身不直接提供连招系统但用Tag和Ability的组合可以很优雅地实现。我的做法是在攻击动画的特定帧通常是收招阶段通过AnimNotify添加一个State.ComboWindow标签持续一段时间后移除。下一段攻击的ActivationRequiredTags里配置这个标签。玩家在窗口期内按攻击键下一段就能触发窗口期过了连招重置。具体实现上AnimNotify里调用ASC添加和移除标签// AnimNotify_ComboWindow.cpp void UAnimNotify_ComboWindow::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) { if (AActor* Owner MeshComp-GetOwner()) { if (IAbilitySystemInterface* ASI CastIAbilitySystemInterface(Owner)) { UAbilitySystemComponent* ASC ASI-GetAbilitySystemComponent(); ASC-AddLooseGameplayTag( FGameplayTag::RequestGameplayTag(State.ComboWindow)); } } }窗口关闭可以用另一个AnimNotify或者用GE的Duration来管理。我倾向于用GE因为可以统一管理持续时间也方便策划调参数。连招重置的逻辑放在攻击技能结束时如果State.ComboWindow没有被消耗也就是没有触发下一段就重置连击计数。这个可以用AbilityTask或者直接在EndAbility里判断。实操心得连招窗口的时长是手感的关键。太短了玩家按不出来太长了连招没有节奏感。我的经验值是0.3到0.5秒具体要看动画的收招时长。另外窗口期内如果玩家提前输入应该缓存输入而不是直接触发等当前段结束后立刻接上这样手感更跟手。3.4 伤害计算与GE的配置伤害计算是战斗框架里最需要精细设计的地方。GAS的GE提供了多种修改属性的方式用对了能省很多事。伤害GE一般用Instant类型的GE通过Modifier修改生命属性。但直接改生命有个问题伤害需要经过防御、暴击、元素抗性等多层计算这些计算放在哪里我的方案是分两步先用一个ExecutionCalculationExecCalc来计算最终伤害再用一个Instant GE来应用。ExecCalc是GAS提供的自定义计算类可以访问攻击方和防御方的属性做复杂的公式运算。// ExecCalc_Damage.cpp void UExecCalc_Damage::Execute_Implementation( const FGameplayEffectCustomExecutionParameters ExecutionParams, FGameplayEffectCustomExecutionOutput OutExecutionOutput) const { // 获取攻击方和防御方的ASC UAbilitySystemComponent* SourceASC ExecutionParams.GetSourceAbilitySystemComponent(); UAbilitySystemComponent* TargetASC ExecutionParams.GetTargetAbilitySystemComponent(); // 获取属性 float Attack SourceASC-GetNumericAttribute(UMyAttributeSet::GetAttackAttribute()); float Defense TargetASC-GetNumericAttribute(UMyAttributeSet::GetDefenseAttribute()); float CritRate SourceASC-GetNumericAttribute(UMyAttributeSet::GetCritRateAttribute()); // 计算伤害 float Damage FMath::Max(Attack - Defense * 0.5f, Attack * 0.1f); // 暴击判定 bool bCrit FMath::FRand() CritRate; if (bCrit) Damage * 1.5f; // 输出到生命属性 OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData( UMyAttributeSet::GetHealthAttribute(), EGameplayModOp::Additive, -Damage)); }这套方案的好处是伤害公式集中在一个地方策划要调公式只改ExecCalc暴击、元素反应这些逻辑可以模块化插入伤害数字可以通过GameplayCue单独表现和计算解耦。注意ExecCalc里不要做任何有副作用的操作比如播放特效、修改其他属性。ExecCalc应该是纯计算只输出对目标属性的修改。表现层的事情交给GameplayCue。3.5 属性集的设计与数值修正AttributeSet的设计直接影响后续扩展性。我的经验是属性要分“基础属性”和“衍生属性”两类。基础属性是直接存储的比如生命、最大生命、攻击、防御。衍生属性是通过基础属性计算出来的比如暴击伤害倍率、元素抗性百分比。衍生属性可以用Attribute Based on Other Attributes的方式在AttributeSet里用PreAttributeChange或者PostGameplayEffectExecute来计算。// 在AttributeSet里定义衍生属性 void UMyAttributeSet::PostGameplayEffectExecute( const FGameplayEffectModCallbackData Data) { Super::PostGameplayEffectExecute(Data); if (Data.EvaluatedData.Attribute GetHealthAttribute()) { // 生命变化后检查是否死亡 if (GetHealth() 0.f) { // 触发死亡逻辑 // 这里通过Tag或者委托通知外部 } } }数值修正方面GE的Modifier支持Additive、Multiplicative、Division等多种运算。Buff一般用Additive加基础值Debuff用Multiplicative乘百分比。要注意运算顺序GAS会先算Additive再算Multiplicative最后算Division。如果顺序不对结果会差很多。实操心得属性变化一定要走GE不要直接SetNumericAttribute。直接Set会绕过GAS的修正系统导致Buff叠加时数值错乱。我见过有人为了图省事直接改属性结果Buff一多就崩排查了半天才发现是这个问题。4. 实操过程与核心环节实现4.1 从零搭建战斗框架的完整步骤假设你现在有一个空的UE5项目要搭一套ARPG战斗框架我按实际顺序把步骤列出来。第一步创建AttributeSet。定义生命、最大生命、攻击、防御、暴击率、暴击伤害、移动速度这几个基础属性。用ATTRIBUTE_ACCESSORS宏生成Getter和Setter。第二步创建ASC和初始化。在PlayerState里创建ASC在Character里实现IAbilitySystemInterface接口在PossessedBy和OnRep_PlayerState里初始化ActorInfo。第三步配置GameplayTag。在项目设置里创建Tag层级至少要有State、Ability、Effect、Event四个根节点。第四步创建初始GE。做一个InitGE在角色开始时应用初始化所有属性到基础值。第五步实现第一个技能。从普攻开始创建一个GA_Attack_Light类配置AbilityTags、ActivationBlockedTags在ActivateAbility里播放动画、应用伤害GE。第六步绑定输入。在Character里绑定输入通过ASC的TryActivateAbilityByTag来触发技能。第七步实现伤害计算。创建ExecCalc_Damage配置伤害公式在攻击技能里应用。第八步实现连招。添加ComboWindow标签配置下一段攻击的ActivationRequiredTags实现连招重置。第九步实现受击和死亡。创建受击GA在生命归零时触发死亡GA添加State.Dead标签阻塞所有技能。第十步实现Buff系统。创建Buff GE用Duration类型配置Modifier修改属性用GrantedTags标记Buff状态。这个顺序不是死的但建议按这个来因为后面的步骤依赖前面的。特别是AttributeSet和ASC初始化一定要先搞定不然后面全是坑。4.2 技能配置的完整参数示例我拿一个具体的技能来展示完整配置。假设是火球术效果是释放期间不能移动可以被打断造成火焰伤害并附加燃烧Debuff。// GA_Fireball.h UCLASS() class UGA_Fireball : public UGameplayAbility { GENERATED_BODY() public: UGA_Fireball(); virtual void ActivateAbility( const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override; virtual void EndAbility( const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) override; protected: UPROPERTY(EditDefaultsOnly, Category Fireball) TSubclassOfUGameplayEffect DamageEffect; UPROPERTY(EditDefaultsOnly, Category Fireball) TSubclassOfUGameplayEffect BurnEffect; UPROPERTY(EditDefaultsOnly, Category Fireball) UAnimMontage* CastMontage; };构造函数里的配置UGA_Fireball::UGA_Fireball() { // 技能标签 AbilityTags.AddTag(FGameplayTag::RequestGameplayTag(Ability.Skill.Fireball)); // 释放期间给自己加的状态标签 ActivationOwnedTags.AddTag(FGameplayTag::RequestGameplayTag(State.Casting)); // 被这些标签阻塞 ActivationBlockedTags.AddTag(FGameplayTag::RequestGameplayTag(State.Stunned)); ActivationBlockedTags.AddTag(FGameplayTag::RequestGameplayTag(State.Dead)); // 释放时取消这些技能 CancelAbilitiesWithTag.AddTag(FGameplayTag::RequestGameplayTag(Ability.Attack)); // 释放时阻塞这些技能 BlockAbilitiesWithTag.AddTag(FGameplayTag::RequestGameplayTag(Ability.Skill)); // 网络执行策略 NetExecutionPolicy EGameplayAbilityNetExecutionPolicy::LocalPredicted; // 实例化策略 InstancingPolicy EGameplayAbilityInstancingPolicy::InstancedPerActor; }ActivateAbility里的逻辑void UGA_Fireball::ActivateAbility(...) { if (!CommitAbility(Handle, ActorInfo, ActivationInfo)) { EndAbility(Handle, ActorInfo, ActivationInfo, true, true); return; } // 播放施法动画 if (CastMontage) { UAbilityTask_PlayMontageAndWait* MontageTask UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy( this, NAME_None, CastMontage); MontageTask-OnCompleted.AddDynamic(this, UGA_Fireball::OnMontageCompleted); MontageTask-OnInterrupted.AddDynamic(this, UGA_Fireball::OnMontageInterrupted); MontageTask-ReadyForActivation(); } // 应用伤害和燃烧 // 实际项目中伤害应该在动画的特定帧通过AnimNotify触发 ApplyDamageAndBurn(); }注意NetExecutionPolicy的选择很关键。LocalPredicted适合玩家主动释放的技能响应快ServerOnly适合服务器权威的逻辑LocalOnly适合纯表现。选错了会导致技能延迟或者不同步。4.3 伤害数字与表现层的解耦战斗逻辑和表现分离是GAS的一大优势但很多人没用好。正确的做法是逻辑层只负责计算和修改属性表现层通过GameplayCue来触发。GameplayCue的配置很简单在GE里添加GameplayCue标签比如GameplayCue.Damage.Fire然后在GameplayCueNotify里实现表现逻辑。// GC_Damage_Fire.cpp void AGC_Damage_Fire::HandleGameplayCue( AActor* MyTarget, EGameplayCueEvent::Type EventType, const FGameplayCueParameters Parameters) { if (EventType EGameplayCueEvent::Executed) { // 播放受击特效 // 显示伤害数字 // 播放音效 // 触发镜头震动 } }伤害数字的显示我一般用WidgetComponent或者Niagara来实现。关键是伤害数值要从Parameters里取而不是重新计算。float DamageAmount Parameters.RawMagnitude;实操心得GameplayCue的标签要和GE的标签对应上但不要混用。GE的标签是逻辑标签GameplayCue的标签是表现标签。比如GE带Effect.Damage.FireGameplayCue带GameplayCue.Damage.Fire。这样换表现的时候只改GameplayCue逻辑层完全不用动。4.4 网络同步的关键配置ARPG如果是多人或者有联机需求网络同步是绕不开的。GAS的网络模型是“客户端预测服务器权威”配置对了很稳配置错了各种鬼畜。几个关键点ASC的ReplicationMode玩家角色用MixedAI用Minimal。Mixed模式下玩家自己的GE会同步到客户端其他玩家的只同步Tag。Minimal只同步Tag不同步GE。技能的NetExecutionPolicy玩家主动技能用LocalPredicted被动触发用ServerOnly纯表现用LocalOnly。GE的ReplicationPolicyInstant GE不需要同步Duration GE需要同步。用bReplicate true。属性同步AttributeSet的属性用ReplicatedUsing标记在OnRep里更新UI。UPROPERTY(ReplicatedUsing OnRep_Health) FGameplayAttributeData Health; UFUNCTION() void OnRep_Health(const FGameplayAttributeData OldHealth) { GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Health, OldHealth); }注意客户端预测的技能如果被服务器拒绝会回滚。回滚的时候表现层也要回滚不然会出现“技能放了但没伤害”的鬼畜现象。GameplayCue要处理好回滚事件。5. 常见问题与排查技巧实录5.1 技能不触发的原因排查技能按了没反应是最常见的问题。我整理了一个排查顺序按这个走基本能定位排查项检查方法常见原因ASC是否初始化打印ASC指针PossessedBy或OnRep没调用技能是否授予打印ActivatableAbilitiesGiveAbility没执行Tag是否匹配打印当前TagsActivationBlockedTags阻塞输入是否绑定打印输入回调输入绑定失败或优先级问题冷却是否就绪打印CooldownTag冷却GE还在生效网络权限打印NetMode客户端没权限但没预测我遇到最多的是ActivationBlockedTags的问题。比如角色死亡后加了State.Dead复活时忘了移除所有技能都放不出来。排查的时候先看当前身上有哪些Tag再看技能的ActivationBlockedTags基本一眼就能看出来。5.2 GE叠加导致的数值异常Buff叠加是数值异常的重灾区。常见现象是叠了三个攻击Buff攻击力变成原来的好几倍或者Buff消失后属性没恢复。根本原因一般是GE的Stacking配置不对。GE有几个关键配置StackingTypeNone不叠加Aggregate按层数叠加AggregateBySource按来源叠加。StackLimitCount最大叠加层数。StackDurationRefreshPolicy叠加时是否刷新持续时间。StackPeriodResetPolicy叠加时是否重置周期。StackExpirationPolicy层数到期后是清除全部还是减一层。我的经验是攻击Buff用Aggregate限制3层叠加时刷新持续时间到期减一层。这样既不会无限叠也不会突然全没。实操心得调试GE叠加的时候用showdebug abilitysystem命令能看到当前所有GE的层数和剩余时间。比打印日志直观多了。5.3 动画和逻辑不同步的处理ARPG里动画和逻辑不同步是很烦人的问题。表现是动画播完了但技能还没结束或者技能结束了动画还在播。根本原因是动画时长和技能时长没有对齐。我的做法是技能的EndAbility由动画的AnimNotify触发而不是由定时器或者动画结束回调触发。具体来说在动画的最后一帧放一个AnimNotify调用ASC的EndAbility。这样动画和逻辑严格同步。// AnimNotify_EndAbility.cpp void UAnimNotify_EndAbility::Notify(...) { if (IAbilitySystemInterface* ASI CastIAbilitySystemInterface(MeshComp-GetOwner())) { UAbilitySystemComponent* ASC ASI-GetAbilitySystemComponent(); // 通过Tag找到当前技能并结束 FGameplayTagContainer Tags; Tags.AddTag(FGameplayTag::RequestGameplayTag(Ability.Skill.Fireball)); ASC-CancelAbilities(Tags); } }如果动画被中断比如被闪避取消技能的EndAbility也要被调用这个GAS会自动处理因为CancelAbilitiesWithTag会触发技能的CancelAbility。5.4 性能优化的几个关键点GAS在大量AI同时战斗的时候会有性能压力。我实测下来几个优化点效果最明显减少GE的数量能用Tag表达的不要用GE。比如“正在施法”这个状态用ActivationOwnedTags就够了不需要专门做一个GE。合并AttributeSet不要每个属性一个AttributeSet合并成一个。AttributeSet的数量影响同步和遍历的性能。控制GameplayCue的频率伤害数字和特效不要每帧触发用节流或者合并。AI的ASC用Minimal模式AI不需要客户端预测用Minimal能省很多同步开销。技能用InstancedPerActor不要用InstancedPerExecution除非技能需要独立实例。InstancedPerActor能复用实例减少GC压力。实操心得用Unreal Insights的GameplayAbilitySystem轨道能看到每个技能的耗时。我优化过一个项目发现大部分耗时在GE的Modifier计算上把一些复杂的ExecCalc改成预计算之后帧率提升了15%左右。5.5 常见问题速查表问题现象可能原因解决方法技能按了没反应Tag阻塞/冷却/未授予检查ActivationBlockedTags和Cooldown伤害数字不显示GameplayCue未配置检查GE的GameplayCue标签Buff消失后属性没恢复GE的Modifier配置错误检查Modifier的运算类型客户端技能不同步NetExecutionPolicy错误玩家技能改LocalPredicted动画播完技能没结束EndAbility未调用用AnimNotify触发EndAbility连招接不上ComboWindow时长太短调整窗口时长到0.3-0.5秒死亡后还能放技能State.Dead未阻塞所有技能加ActivationBlockedTags属性同步延迟AttributeSet未标记Replicated加ReplicatedUsing标记6. 从项目实战中沉淀的几条经验做ARPG战斗框架这些年踩过的坑比写过的代码还多。最后分享几条我觉得最有价值的经验都是文档里不会写的。第一条Tag体系要在项目初期就定好后期改成本极高。我经历过一个项目中期发现Tag命名混乱想重构结果发现Tag散落在几十个蓝图和C文件里改了两天没改完最后放弃了。所以一开始就要定好命名规范并且写进项目文档所有人遵守。第二条技能逻辑尽量用配置而不是硬编码。我现在的做法是一个通用的GA类通过DataAsset配置不同的技能参数。这样加新技能只需要配数据不用写代码。策划也能自己调效率高很多。第三条伤害计算一定要可追溯。我见过太多项目伤害算出来不对但没人知道是哪一步算错了。我的做法是在ExecCalc里把每一步的中间值都打日志用UE_LOG输出调试的时候一目了然。第四条表现层和逻辑层的边界要清晰。逻辑层不要直接播放特效表现层不要直接改属性。所有交互通过GameplayCue和Tag来。这样换美术资源的时候逻辑层完全不用动。第五条多测试极端情况。比如同时叠10个Buff、技能释放中死亡、网络延迟500ms、AI同时放技能。这些情况在正常测试里不容易触发但线上一定会出。我一般会写一些自动化测试用例专门跑这些极端场景。这套框架我在两个项目里落地过一个是单机ARPG一个是多人联机。单机项目大概两周搭完核心框架联机项目因为要处理同步花了大概一个月。整体来说GAS的学习曲线是陡的但一旦跑通后续加技能的速度会快很多。如果你正在做ARPG我强烈建议花时间把GAS吃透这个投入绝对值得。
返回列表