UE5GAS GameAbility源码解析 CommitAbility
文章目录
- 一、 CommitAbility
- 代码核心作用简述
- 代码逐部分解析(结合具体例子)
- 第1部分:最终检查 (`CommitCheck`)
- 第2部分:执行消耗 (`CommitExecute`)
- 第3部分:调用蓝图事件 (`K2_CommitExecute`)
- 第4部分:广播提交事件 (`NotifyAbilityCommit`)
- 总结与流程梳理
- 为什么需要 CommitAbility?
- 二、 CommitCheck
- 第1部分:基础有效性检查
- 第2部分 & 第3部分:冷却时间检查 (`CheckCooldown`)
- 总结与重要性
- 三、CommitExecute
- 第1部分:应用冷却时间 (`ApplyCooldown`)
- 第2部分:应用技能消耗 (ApplyCost)
- 流程回顾与重要性
- 为什么使用 Gameplay Effect (GE) 来实现?
- 四、CheckCooldown
- 第1部分:安全性检查
- 第2部分:获取冷却标签
- 第3-4部分:检查标签存在性
- 第5-6部分:填充失败信息(可选)
- 第7部分:返回检查结果
- 第8部分:默认通过
- 完整工作流程示例
- 场景1:火球术不在冷却中
- 场景2:火球术在冷却中
- 设计优势
- 五、ApplyCooldown
- 第1部分:获取冷却效果定义
- 第2部分:应用冷却效果到拥有者
- 完整的工作流程示例
- 冷却 Gameplay Effect 的具体配置示例
- 设计优势与灵活性
- 与 CheckCooldown 的配合
- 实际应用场景
- 六、CheckCost
- 第1部分:获取消耗效果定义
- 第2部分:获取能力系统组件
- 第3部分:关键检查 - 能否应用属性修改器
- 第4部分:处理检查失败
- 第5部分:默认通过
- 完整工作流程示例
- 消耗 Gameplay Effect 的具体配置示例
- 设计优势与特点
- 与 ApplyCost 的配合
- 实际应用场景
- 七、ApplyCost
- 第1部分:获取消耗效果定义
- 第2部分:应用消耗效果到拥有者
- 完整的工作流程示例
- 消耗 Gameplay Effect 的具体配置示例
- 更复杂的消耗配置示例
- 与 CheckCost 的完美配合
- 设计优势
- 实际应用场景
- 总结
一、 CommitAbility
bool UGameplayAbility::CommitAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, OUT FGameplayTagContainer* OptionalRelevantTags)
{// 1. 最终检查 (CommitCheck)if (!CommitCheck(Handle, ActorInfo, ActivationInfo, OptionalRelevantTags)){return false;}// 2. 执行消耗 (CommitExecute)CommitExecute(Handle, ActorInfo, ActivationInfo);// 3. 调用蓝图事件 (K2_CommitExecute)K2_CommitExecute();// 4. 广播提交事件 (NotifyAbilityCommit)ActorInfo->AbilitySystemComponent->NotifyAbilityCommit(this);return true;
}
代码核心作用简述
CommitAbility
的中文可以理解为“提交技能”或“确认执行技能”。它的核心作用是:在技能正式生效前,进行最终的资源检查和消耗,并触发后续的执行逻辑。
你可以把它想象成一个交易的“最终确认”按钮。你看中了一个商品(启动了技能),把它加入购物车(ActivateAbility
),但在点击“付款”(CommitAbility
)之前,系统会最后检查一次你的账户余额和优惠券(检查冷却和消耗),如果都满足,就扣款(消耗资源),然后订单正式生效(技能效果产生)。
代码逐部分解析(结合具体例子)
我们以一个简单的游戏技能为例:战士的“火球术”。
这个火球术需要:
- 魔法值 (Mana):消耗 25 点。
- 冷却时间 (Cooldown):技能释放后进入 5 秒冷却。
第1部分:最终检查 (CommitCheck
)
if (!CommitCheck(Handle, ActorInfo, ActivationInfo, OptionalRelevantTags))
{return false;
}
-
作用:这是技能释放前的最后一道防线。它内部会调用
K2_CommitCheck
蓝图可重写函数,以及检查与该技能关联的 技能标签 (Ability Tags) 和 冷却/消耗属性。 -
火球术例子:
- 战士按下火球术按键,技能开始激活 (ActivateAbility 被调用)。
- 在技能播放前摇动画的过程中,战士的魔法可能被其他技能吸干,或者被敌人沉默了(添加了
GameplayTag
阻止施法)。 - 当动画播完,即将发出火球的那一刻,
CommitCheck
被调用。 - 它检查发现:“当前魔法值不足 25” 或者战士身上有“沉默”标签。
- 结果:检查失败,函数返回
false
。CommitAbility
也返回false
,导致技能释放失败。火球不会射出,动画可能会播放一个失败的反馈,并且冷却和魔法不会被消耗。这是一种“安全回滚”机制。
第2部分:执行消耗 (CommitExecute
)
CommitExecute(Handle, ActorInfo, ActivationInfo);
- 作用:如果检查通过了,就在这里真正地扣除资源(如魔法值、体力等)并应用冷却时间。
- 火球术例子:
CommitCheck
检查通过,战士有足够的蓝,也没有被沉默。CommitExecute
函数被执行。- 结果:战士的属性集 (AttributeSet) 上的“魔法值”属性被永久性地减去 25 点。同时,一个为期 5 秒的冷却效果 (Gameplay Effect) 被应用到战士身上,防止他立刻再次使用火球术。
第3部分:调用蓝图事件 (K2_CommitExecute
)
K2_CommitExecute();
- 作用:这是一个 BlueprintNativeEvent(蓝图可重写事件)。在 C++ 中它是一个空函数,设计目的就是让蓝图设计师可以在这里添加自定义的提交逻辑。
- 火球术例子:
- 设计师可能想在资源正式扣除的这一刻播放一个特殊的音效或粒子效果。
- 他们可以在蓝图中重写这个
K2_CommitExecut
e 事件,然后在里面添加播放音效和粒子的节点。 - 结果:当代码执行到这里时,就会调用蓝图里实现的逻辑,播放那些效果。
第4部分:广播提交事件 (NotifyAbilityCommit
)
ActorInfo->AbilitySystemComponent->NotifyAbilityCommit(this);
- 作用:通知整个能力系统组件 (AbilitySystemComponent):“这个技能已经成功提交了!”。其他系统可以监听这个事件来做出反应。
- 火球术例子:
- UI 系统可能监听了这个事件。当收到通知时,它会立刻刷新玩家的魔法条 UI,显示出刚刚扣除 25 点魔法后的新数值,而不需要等待每帧的查询。
- 成就系统也可能监听这个事件,统计“玩家总共释放了多少次火球术”。
- 结果:游戏的其他部分及时地知道了这个技能已经成功释放的事实,并做出了相应的响应。
总结与流程梳理
对于“火球术”这个技能,CommitAbility
的完整流程是:
- 尝试提交:前摇动画结束,准备发射火球。
- 最终检查:检查魔法值是否 >= 25?身上是否有“沉默”等阻止性标签?→ 如果否,失败返回,什么都不发生。
- 执行消耗:魔法值属性被减去 25,5秒冷却计时开始。
- 蓝图事件:播放一个“法力消耗”的特殊音效。
- 广播事件:UI 系统收到通知,立即更新魔法值显示。
- 返回成功:
CommitAbility
返回true
,后续的技能逻辑(如生成火球投射物)继续执行。
为什么需要 CommitAbility?
这种设计将 “技能逻辑” 和 “资源消耗” 进行了分离,提供了极大的灵活性和安全性。
- 安全性:确保只有在万无一失的情况下才会扣除资源,避免了玩家因网络延迟或瞬间状态变化而卡掉资源却放不出技能的不良体验。
- 灵活性:你可以在
ActivateAbility
中处理复杂的逻辑(如瞄准、动画),而将统一的消耗检查放在CommitCheck
中。CommitExecute
和K2_CommitExecute
的分离也让设计师可以轻松地添加视觉效果而不影响核心逻辑。 - 可扩展性:
NotifyAbilityCommit
事件让游戏的其他模块能够对技能的成功释放做出反应,实现了系统间的低耦合通信。
简单来说,CommitAbility
是技能从“准备”阶段进入“不可逆的正式执行”阶段的关键大门。
二、 CommitCheck
bool UGameplayAbility::CommitCheck(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, OUT FGameplayTagContainer* OptionalRelevantTags)
{// 1. 基础有效性检查const bool bValidHandle = Handle.IsValid();const bool bValidActorInfoPieces = (ActorInfo && (ActorInfo->AbilitySystemComponent != nullptr));const bool bValidSpecFound = bValidActorInfoPieces && (ActorInfo->AbilitySystemComponent->FindAbilitySpecFromHandle(Handle) != nullptr);if (!bValidHandle || !bValidActorInfoPieces || !bValidSpecFound){ABILITY_LOG(Warning, TEXT("UGameplayAbility::CommitCheck provided an invalid handle or actor info or couldn't find ability spec: %s Handle Valid: %d ActorInfo Valid: %d Spec Not Found: %d"), *GetName(), bValidHandle, bValidActorInfoPieces, bValidSpecFound);return false;}// 2. 全局设置检查(是否忽略冷却/消耗)UAbilitySystemGlobals& AbilitySystemGlobals = UAbilitySystemGlobals::Get();// 3. 冷却时间检查 (CheckCooldown)if (!AbilitySystemGlobals.ShouldIgnoreCooldowns() && !CheckCooldown(Handle, ActorInfo, OptionalRelevantTags)){return false;}// 4. 技能消耗检查 (CheckCost)if (!AbilitySystemGlobals.ShouldIgnoreCosts() && !CheckCost(Handle, ActorInfo, OptionalRelevantTags)){return false;}return true;
}
第1部分:基础有效性检查
if (!bValidHandle || !bValidActorInfoPieces || !bValidSpecFound) { ... return false; }
- 作用:这是最底层的安全检查,确保函数接收到的参数是有效的,并且能在能力系统组件(ASC)中找到对应的技能实例(
FGameplayAbilitySpec
)。 - 为何重要:防止在数据无效的情况下继续执行逻辑,导致游戏崩溃。
- 火球术例子(异常情况):
- 战士释放火球的过程中,技能突然被服务器强制移除(例如,卸下了装备火球术的武器)。
- 此时,
Handle
可能变得无效,或者在 ASC 中FindAbilitySpecFromHandle
查找不到对应的技能。 - 结果:此检查失败,函数返回
false
,并打印一条警告日志。CommitAbility
也会因此失败,阻止了一次无效的资源消耗。
第2部分 & 第3部分:冷却时间检查 (CheckCooldown
)
if (!AbilitySystemGlobals.ShouldIgnoreCosts() && !CheckCost(...))
作用:
ShouldIgnoreCosts()
: 同样是全局调试开关,如果开启,则不会检查资源消耗,技能可以无限释放。CheckCooldown(Handle, ActorInfo, OptionalRelevantTags)
: 这是实际的资源消耗检查逻辑。它会检查技能所需的属性(如魔法值)是否足够。
- 火球术例子:
- 战士按下火球术按键开始施法(前摇动画)。
- 在动画播放的这1秒内,战士受到了敌人的一个“法力燃烧”技能的影响,魔法值被烧掉了 30 点。
- 动画结束,
CommitCheck
被调用。 CheckCost
函数执行,它去查询战士当前的魔法值属性,发现当前魔法值(例如只剩 10 点)不足 25 点。- 结果:
CheckCost
返回false
,导致CommitCheck
返回false
。技能释放失败。正是因为有了这个最终的检查,才避免了“战士在蓝量不足的情况下依然把火球扔出去”的逻辑错误。
总结与重要性
CommitCheck
是 GAS 中保证状态同步和防止作弊的关键环节。
-
解决时序问题:它解决了从技能激活 (
ActivateAbility
) 到资源消耗 (Commit
) 之间的时间差所导致的状态不一致问题。在多人游戏中,由于网络延迟和客户端预测,这个时间差会变得更加不可控,CommitCheck
在服务器端的最终裁决显得尤为重要。 -
与
CanActivateAbility
的区别:CanActivateAbility
是技能开始时的检查,通常还包括输入是否被抑制等条件。它决定技能按钮是否可被按下、是否能开始播放动画。CommitCheck
是技能即将产生效果和消耗时的最终检查,只关注最核心的资源条件(冷却和成本)。一个技能可能CanActivateAbility
返回true
(能开始播动画),但CommitCheck
返回false
(动画播完发现没蓝了)。
-
安全网:它是资源扣除前的最后一道安全网,确保了游戏的规则不被破坏(例如,玩家不可能在没蓝或无冷却时成功释放技能)。
简单比喻:
-
CanActivateAbility
像是申请信用卡——检查你的信用记录等基本条件,决定是否给你发卡。 -
ActivateAbility
像是刷卡购物——你把卡递给收银员,表示你想买东西。 -
CommitCheck
像是银行批准这笔交易——在扣款前最后一刻,银行检查你的额度是否足够、卡片是否被盗用。 -
CommitExecute
像是最终扣款——检查通过,钱从你的账户划走。
如果没有 CommitCheck
,我们的战士就会经常上演“尬舞”(播放了施法动画)却搓不出火球(因为资源在动画期间被消耗了)的奇怪现象。
三、CommitExecute
void UGameplayAbility::CommitExecute(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{// 1. 应用冷却时间ApplyCooldown(Handle, ActorInfo, ActivationInfo);// 2. 应用技能消耗ApplyCost(Handle, ActorInfo, ActivationInfo);
}
第1部分:应用冷却时间 (ApplyCooldown
)
ApplyCooldown(Handle, ActorInfo, ActivationInfo);
-
作用:这个方法负责启动技能的冷却计时器。其内部通常通过向自身(技能的拥有者)应用一个冷却效果(Cooldown Gameplay Effect) 来实现。
-
内部机制:
ApplyCooldown
会创建一个GameplayEffectSpec
。- 这个 Effect 会带有一个持续时间(Duration),例如 5 秒。
- 它还会添加一个标签(Tag),比如
Cooldown.Fireball
。这个标签会被CheckCooldown
函数用来查询技能是否处于冷却状态。
-
火球术例子:
- 战士成功释放火球术后,代码执行到这里。
ApplyCooldown
被调用。- 结果:战士的能力系统组件(ASC)上被施加了一个持续 5 秒的冷却效果。在这 5 秒内,任何对
CheckCooldown
的调用都会发现Cooldown.Firebal
l 标签存在,从而阻止技能的再次释放。战士技能图标上也会出现一个旋转的冷却计时器。
第2部分:应用技能消耗 (ApplyCost)
ApplyCost(Handle, ActorInfo, ActivationInfo);
-
作用:这个方法负责永久性地扣除技能所需的资源。其内部也是通过应用一个消耗效果(Cost Gameplay Effect) 来实现。
-
内部机制:
ApplyCost
会创建一个GameplayEffectSpec
。- 这个 Effect 的修改器(Modifier)会使用
ModifierOp::Additive
并提供一个负值
,例如-25.0f
,作用于“魔法值(Mana)”属性上。 - 这个 Effect 通常是
瞬间(Instant)
的,意味着扣除操作立即生效,没有持续时间。
-
火球术例子:
- 紧接着应用冷却之后,
ApplyCost
被调用。 - 结果:战士的“魔法值”属性被立刻减去 25 点。这个数值会通过网络同步,更新到所有客户端,因此所有玩家看到的战士蓝条都会减少。如果魔法值不足 25,
ApplyCost
甚至会触发一个确保性的检查(尽管之前CommitCheck
已经查过了),如果真的不够,可能会触发一个“技能执行失败”的回滚逻辑。
- 紧接着应用冷却之后,
流程回顾与重要性
让我们将 CommitCheck
和 CommitExecute
结合起来,看一个完整的“提交”流程:
-
最终检查 (CommitCheck):
CheckCooldown()
: “火球术还在冷却吗?” -> 否CheckCost()
: “魔法值够 25 点吗?” -> 是- 结果:
CommitCheck
返回true
,允许提交。
-
执行消耗 (CommitExecute):
ApplyCooldown()
: 为火球术添加一个 5 秒的冷却效果。ApplyCost()
: 从魔法值中永久扣除 25 点。- 结果: 资源被正式消耗,技能进入冷却。
为什么使用 Gameplay Effect (GE) 来实现?
这是 GAS 设计的一个精妙之处:
- 统一性: 冷却和消耗与游戏中其他所有效果(如 buff、debuff、伤害、治疗)使用同一套系统(
GameplayEffect
)。这意味着它们受益于相同的预测(Prediction)、复制(Replication)和标签(Tag)机制。 - 灵活性: 设计师可以在蓝图中配置一个
GameplayEffect
资产来定义技能的消耗和冷却,而无需修改代码。他们可以轻松地制作一个“消耗生命值而非魔法值”的技能,或者一个“根据敌人数量动态改变冷却时间”的技能。 - 可扩展性: 因为冷却是一个 GE,它可以被其他系统影响。例如,一个“减少所有技能冷却时间 20%”的 buff,可以直接修改冷却 GE 的持续时间,而无需知道具体是哪个技能。
总结一下:
CommitExecute
是一个“动手”的函数。它不像 CommitCheck
那样只是“动眼
”检查。它的工作简单而关键:
-
调用
ApplyCooldown
-> 给技能“上锁”一段时间。 -
调用
ApplyCost
-> 从玩家身上“扣钱”。
这两个操作共同确保了技能的使用是有代价、有限制的,是维持游戏平衡和资源循环的核心环节。
四、CheckCooldown
bool UGameplayAbility::CheckCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, OUT FGameplayTagContainer* OptionalRelevantTags) const
{// 1. 安全性检查if (!ensure(ActorInfo)){return true;}// 2. 获取技能的冷却标签const FGameplayTagContainer* CooldownTags = GetCooldownTags();if (CooldownTags && !CooldownTags->IsEmpty()){// 3. 获取能力系统组件if (UAbilitySystemComponent* AbilitySystemComponent = ActorInfo->AbilitySystemComponent.Get()){// 4. 检查是否拥有任何冷却标签if (AbilitySystemComponent->HasAnyMatchingGameplayTags(*CooldownTags)){// 5. 如果检查失败,填充相关信息标签if (OptionalRelevantTags){const FGameplayTag& FailCooldownTag = UAbilitySystemGlobals::Get().ActivateFailCooldownTag;if (FailCooldownTag.IsValid()){OptionalRelevantTags->AddTag(FailCooldownTag);}// 6. 添加具体是哪个冷却标签导致的失败OptionalRelevantTags->AppendMatchingTags(AbilitySystemComponent->GetOwnedGameplayTags(), *CooldownTags);}// 7. 返回false表示冷却检查失败return false;}}}// 8. 返回true表示冷却检查通过return true;
}
第1部分:安全性检查
if (!ensure(ActorInfo))
{return true;
}
- 作用:确保
ActorInfo
参数有效。如果无效,使用ensure
在开发时弹出警告,但函数返回true
(通过检查)。 - 逻辑:这是一种"失败安全"的设计。当数据异常时,选择让检查通过而不是阻止游戏运行。
第2部分:获取冷却标签
const FGameplayTagContainer* CooldownTags = GetCooldownTags();
if (CooldownTags && !CooldownTags->IsEmpty())
- 作用:获取这个技能定义的所有冷却标签。
- 火球术例子:
GetCooldownTags()
可能会返回包含Cooldown.Spell.Fireball
的标签容器。- 如果技能没有定义冷却标签(容器为空),说明这个技能没有冷却限制,直接返回
true
通过检查。
第3-4部分:检查标签存在性
if (AbilitySystemComponent->HasAnyMatchingGameplayTags(*CooldownTags))
- 作用:检查能力系统组件当前是否拥有任何与冷却标签匹配的标签。
- 火球术例子:
- 当战士释放火球术后,
ApplyCooldown
会给他添加Cooldown.Spell.Fireball
标签。 - 如果战士在冷却期间再次尝试释放,这里检查会发现他拥有
Cooldown.Spell.Fireball
标签。 - 结果:进入 if 分支,表示冷却检查失败。
- 当战士释放火球术后,
第5-6部分:填充失败信息(可选)
if (OptionalRelevantTags)
{const FGameplayTag& FailCooldownTag = UAbilitySystemGlobals::Get().ActivateFailCooldownTag;if (FailCooldownTag.IsValid()){OptionalRelevantTags->AddTag(FailCooldownTag);}OptionalRelevantTags->AppendMatchingTags(AbilitySystemComponent->GetOwnedGameplayTags(), *CooldownTags);
}
-
作用:如果调用者提供了
OptionalRelevantTags
参数,就填充详细的失败信息。 -
具体内容:
- 通用失败标签:添加一个全局的冷却失败标签,如
Ability.Activate.Fail.Cooldown
。 - 具体阻挡标签:添加实际导致失败的冷却标签,如
Cooldown.Spell.Fireball
。
- 通用失败标签:添加一个全局的冷却失败标签,如
-
火球术例子:
- 当冷却检查失败时,
OptionalRelevantTags
会被填充为:Ability.Activate.Fail.Cooldown
(通用失败原因)Cooldown.Spell.Fireball
(具体是哪个技能在冷却)
- 这些信息可用于:
- UI 显示:“火球术还在冷却中!”
- 播放特定的冷却失败音效
- 调试日志
- 当冷却检查失败时,
第7部分:返回检查结果
return false; // 冷却检查失败
作用:明确返回 false
,表示技能处于冷却状态,不能释放。
第8部分:默认通过
return true; // 冷却检查通过
- 作用:如果以上所有检查都没问题(没有冷却标签或标签不存在),返回 true,表示技能不在冷却中。
完整工作流程示例
场景1:火球术不在冷却中
- 战士按下火球术按键
CheckCooldown
被调用GetCooldownTags()
返回Cooldown.Spell.Fireball
- 检查战士的 ASC,发现没有
Cooldown.Spell.Fireball
标签 - 函数返回
true
,冷却检查通过 - 技能可以继续释放
场景2:火球术在冷却中
- 战士在冷却期间按下火球术按键
CheckCooldown
被调用GetCooldownTags()
返回Cooldown.Spell.Fireball
- 检查战士的 ASC,发现存在
Cooldown.Spell.Fireball
标签 - 向
OptionalRelevantTags
添加失败信息 - 函数返回
false
,冷却检查失败 - 技能释放被阻止
设计优势
- 基于标签的灵活性:
- 共享冷却:多个技能可以使用同一个冷却标签(如
Cooldown.Spell
),实现"法术公共冷却"。 - 分组冷却:不同类别的技能可以使用不同的冷却标签(如
Cooldown.Attack
、Cooldown.Defense
)。
- 共享冷却:多个技能可以使用同一个冷却标签(如
- 详细的信息反馈:通过
OptionalRelevantTags
参数,调用者可以获得详细的失败原因,便于实现丰富的用户反馈。 - 与 Gameplay Effect 集成:冷却标签通常由冷却 Gameplay Effect 添加,这使得冷却系统可以受益于 GAS 的完整功能(预测、复制、修饰器等)。
这种基于标签的冷却检查机制是 GAS 的核心设计理念之一,它提供了极大的灵活性和可扩展性。
五、ApplyCooldown
void UGameplayAbility::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{// 1. 获取冷却效果的GameplayEffect定义UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();// 2. 如果存在冷却效果,就应用到拥有者身上if (CooldownGE){ApplyGameplayEffectToOwner(Handle, ActorInfo, ActivationInfo, CooldownGE, GetAbilityLevel(Handle, ActorInfo));}
}
第1部分:获取冷却效果定义
UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
- 作用:获取这个技能对应的冷却 Gameplay Effect(GE)资产。
- 火球术例子:
GetCooldownGameplayEffect()
是一个虚函数,通常在技能的蓝图或C++类中重写。- 对于火球术,它可能返回一个名为
GE_Fireball_Cooldown
的 Gameplay Effect。 - 这个 GE 的定义可能包含:
- 持续时间:5 秒
- 授予的标签:
Cooldown.Spell.Fireball
(这就是CheckCooldown
检查的标签)
第2部分:应用冷却效果到拥有者
ApplyGameplayEffectToOwner(Handle, ActorInfo, ActivationInfo, CooldownGE, GetAbilityLevel(Handle, ActorInfo));
-
作用:将冷却 GE 应用到技能的使用者(战士)的能力系统组件上。
-
参数说明:
CooldownGE
:上一步获取的冷却效果资产GetAbilityLevel(Handle, ActorInfo)
:获取技能的当前等级,用于计算可能随等级变化的冷却时间
-
火球术例子:
- 假设火球术等级为 1,冷却时间为固定的 5 秒。
ApplyGameplayEffectToOwner
会在战士的 ASC 上创建并应用这个冷却效果。- 实际效果:战士身上被添加了
Cooldown.Spell.Fireball
标签,持续 5 秒。
完整的工作流程示例
火球术释放流程中的冷却部分:
- 技能释放:战士按下火球术按键,技能成功通过所有检查
- 提交阶段:
CommitAbility
被调用 - 应用冷却:
ApplyCooldown
执行:- 调用
GetCooldownGameplayEffect()
获取GE_Fireball_Cooldown
- 调用
ApplyGameplayEffectToOwner
将这个效果应用到战士身上
- 调用
- 冷却生效:
- 战士的 ASC 上现在有了
GE_Fireball_Cooldown
效果 - 该效果授予
Cooldown.Spell.Fireball
标签,持续 5 秒 - 5 秒后效果自动移除,标签也随之移除
- 战士的 ASC 上现在有了
冷却 Gameplay Effect 的具体配置示例
GE_Fireball_Cooldown
可能这样配置:
Gameplay Effect: GE_Fireball_Cooldown
├── Duration Policy: Has Duration
├── Duration: 5.0 seconds
└── Granted Tags:├── Cooldown.Spell.Fireball└── Cooldown.Spell # 可选的父级标签,用于分组冷却
设计优势与灵活性
-
基于 Gameplay Effect 的标准化:
- 冷却使用与其他游戏效果相同的系统,保证了行为的一致性
- 受益于 GAS 的预测、复制和堆栈机制
-
灵活的冷却配置:
- 动态冷却:可以通过 Modifier 让冷却时间基于属性变化
Duration: 5.0 - (0.1 * CooldownReductionAttribute)
- 等级化冷却:不同技能等级可以有不同的冷却时间
- 条件性冷却:可以通过标签要求/忽略来实现复杂的冷却逻辑
- 冷却标签的层次结构:
- 可以设置父子标签关系,实现分组冷却
- 例如:Cooldown.Spell.Fireball → Cooldown.Spell → Cooldown
- 其他系统可以监听不同层级的标签变化
与 CheckCooldown 的配合
ApplyCooldown
和 CheckCooldown
是冷却机制的两个互补部分:
ApplyCooldown
:设置冷却状态(添加标签)CheckCooldown
:检查冷却状态(检查标签是否存在)
这种基于标签的设计使得冷却系统非常灵活和可扩展,是 GAS 架构的精妙体现。
实际应用场景
场景:减少冷却时间的装备
战士装备了一件"冷却缩减 20%"的装备:
- 装备效果添加一个修改
CooldownDuration
属性的 Gameplay Effect - 当火球术释放时,
ApplyCooldown
应用的冷却时间会自动计算缩减后的值 - 实际的冷却时间变为 4 秒而不是 5 秒
CheckCooldown
仍然正常工作,只是冷却时间变短了
这种设计使得装备、天赋、buff 等系统可以很容易地与冷却机制交互,而无需修改技能本身的代
六、CheckCost
bool UGameplayAbility::CheckCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, OUT FGameplayTagContainer* OptionalRelevantTags) const
{// 1. 获取消耗效果的GameplayEffect定义UGameplayEffect* CostGE = GetCostGameplayEffect();if (CostGE){// 2. 获取能力系统组件UAbilitySystemComponent* AbilitySystemComponent = ActorInfo ? ActorInfo->AbilitySystemComponent.Get() : nullptr;if (ensure(AbilitySystemComponent)){// 3. 关键检查:能否应用属性修改器if (!AbilitySystemComponent->CanApplyAttributeModifiers(CostGE, GetAbilityLevel(Handle, ActorInfo), MakeEffectContext(Handle, ActorInfo))){// 4. 如果检查失败,填充失败信息const FGameplayTag& CostTag = UAbilitySystemGlobals::Get().ActivateFailCostTag;if (OptionalRelevantTags && CostTag.IsValid()){OptionalRelevantTags->AddTag(CostTag);}return false;}}}return true;
}
第1部分:获取消耗效果定义
UGameplayEffect* CostGE = GetCostGameplayEffect();
- 作用:获取这个技能对应的消耗 Gameplay Effect 资产。
- 火球术例子:
GetCostGameplayEffect()
返回一个名为GE_Fireball_Cost
的 Gameplay Effect。- 这个 GE 可能包含一个对"魔法值"属性的修改器,操作类型为"Add"且值为 -25。
第2部分:获取能力系统组件
UAbilitySystemComponent* AbilitySystemComponent = ActorInfo ? ActorInfo->AbilitySystemComponent.Get() : nullptr;
if (ensure(AbilitySystemComponent))
- 作用:安全地获取技能使用者的能力系统组件。
- 确保有效性:使用
ensure
保证在开发阶段能发现空指针问题。
第3部分:关键检查 - 能否应用属性修改器
if (!AbilitySystemComponent->CanApplyAttributeModifiers(CostGE, GetAbilityLevel(Handle, ActorInfo), MakeEffectContext(Handle, ActorInfo)))
- 作用:这是核心的检查逻辑。它尝试"模拟"应用消耗效果,看是否会违反任何限制。
- 内部机制:
CanApplyAttributeModifiers
会遍历 GE 中的所有属性修改器- 对于每个修改器,它会计算应用后的结果,并检查是否违反限制
- 最重要的检查:对于"增加值"为负数的修改器(即消耗),它会检查当前属性值是否足够(即属性值 + 修改值 ≥ 0)
- 火球术例子:
- 当前战士魔法值 = 30,火球术消耗 = 25
CanApplyAttributeModifiers
计算:30 + (-25) = 5 ≥ 0 → 通过检查- 当前战士魔法值 = 20,火球术消耗 = 25
CanApplyAttributeModifiers
计算:20 + (-25) = -5 < 0 → 检查失败
第4部分:处理检查失败
if (!AbilitySystemComponent->CanApplyAttributeModifiers(...))
{const FGameplayTag& CostTag = UAbilitySystemGlobals::Get().ActivateFailCostTag;if (OptionalRelevantTags && CostTag.IsValid()){OptionalRelevantTags->AddTag(CostTag);}return false;
}
- 作用:当消耗检查失败时,提供失败原因并返回
false
。 - 火球术例子:
- 如果战士魔法值不足,
OptionalRelevantTags
会被添加Ability.Activate.Fail.Cost
标签 - UI 系统可以使用这个标签显示"魔法值不足"的提示
- 如果战士魔法值不足,
第5部分:默认通过
return true;
- 作用:如果以上检查都通过,或者技能没有定义消耗效果,返回 true。
完整工作流程示例
场景1:魔法值充足
- 战士按下火球术按键
CheckCost
被调用- 获取
GE_Fireball_Cost
(消耗25魔法值) CanApplyAttributeModifiers
模拟计算:30 - 25 = 5 ≥ 0- 函数返回
true
,消耗检查通过 - 技能可以继续释放
场景2:魔法值不足
- 战士按下火球术按键
CheckCost
被调用- 获取
GE_Fireball_Cost
(消耗25魔法值) CanApplyAttributeModifiers
模拟计算:20 - 25 = -5 < 0- 向
OptionalRelevantTags
添加失败标签 - 函数返回
false
,消耗检查失败 - 技能释放被阻止
消耗 Gameplay Effect 的具体配置示例
GE_Fireball_Cost
可能这样配置:
Gameplay Effect: GE_Fireball_Cost
├── Duration Policy: Instant
└── Modifiers:└── Attribute: Mana├── Modifier Magnitude: Scalable Float (-25.0)└── Modifier Op: Additive
设计优势与特点
-
统一的检查机制:
- 使用与真实消耗相同的 Gameplay Effect 系统进行检查
- 确保检查逻辑与实际消耗逻辑完全一致
-
支持复杂的消耗规则:
- 多属性消耗:可以同时检查魔法值、体力值等多个属性
- 条件性消耗:基于技能等级或其他属性的动态消耗
- 百分比消耗:消耗当前魔法值的 10%,而不是固定值
-
属性限制系统的集成:
- 自动继承属性系统的所有限制规则
- 例如,如果某个属性被标记为"不能低于0",
CanApplyAttributeModifiers
会自动强制执行这个限制
与 ApplyCost 的配合
CheckCost
和 ApplyCost
是消耗机制的两个互补部分:
CheckCost
:预检查是否能支付消耗(模拟应用)ApplyCost
:实际执行消耗(真正应用效果)
这种"先检查后执行"的模式确保了资源消耗的安全性。
实际应用场景
场景:百分比消耗的技能
假设有一个技能消耗当前魔法值的 20%:
Gameplay Effect: GE_PercentageSpell_Cost
└── Modifiers:└── Attribute: Mana├── Modifier Magnitude: Attribute-Based│ ├── Attribute: Mana (Current Value)│ └── Coefficient: -0.2 # 消耗20%└── Modifier Op: Additive
CheckCost
的工作流程:
- 获取当前魔法值(例如 100)
- 计算消耗量:100 × 20% = 20
- 检查:100 - 20 = 80 ≥ 0 → 通过检查
这种设计使得实现复杂的消耗规则变得非常简单和统一。
七、ApplyCost
void UGameplayAbility::ApplyCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{// 1. 获取消耗效果的GameplayEffect定义UGameplayEffect* CostGE = GetCostGameplayEffect();// 2. 如果存在消耗效果,就应用到拥有者身上if (CostGE){ApplyGameplayEffectToOwner(Handle, ActorInfo, ActivationInfo, CostGE, GetAbilityLevel(Handle, ActorInfo));}
}
第1部分:获取消耗效果定义
UGameplayEffect* CostGE = GetCostGameplayEffect();
- 作用:获取这个技能对应的消耗 Gameplay Effect 资产。
- 火球术例子:
GetCostGameplayEffect()
返回一个名为GE_Fireball_Cost
的 Gameplay Effect。- 这个 GE 包含一个对"魔法值"属性的修改器,操作类型为"Add"且值为 -25。
第2部分:应用消耗效果到拥有者
ApplyGameplayEffectToOwner(Handle, ActorInfo, ActivationInfo, CostGE, GetAbilityLevel(Handle, ActorInfo));
作用:将消耗 GE 应用到技能的使用者(战士)的能力系统组件上。
参数说明:
-
CostGE
:消耗效果资产 -
GetAbilityLevel(Handle, ActorInfo)
:技能等级,用于计算可能随等级变化的消耗量 -
火球术例子:
ApplyGameplayEffectToOwner
在战士的 ASC 上创建并应用这个消耗效果。- 实际效果:战士的"魔法值"属性被永久性地减去 25 点。
完整的工作流程示例
火球术释放流程中的消耗部分:
- 预检查阶段:
CheckCost
通过模拟应用验证战士有足够魔法值(30 ≥ 25) - 提交阶段:
CommitAbility
被调用,通过所有检查 - 实际消耗:
ApplyCost
执行:- 调用
GetCostGameplayEffect()
获取 GE_Fireball_Cost - 调用
ApplyGameplayEffectToOwner
将这个效果应用到战士身上
- 调用
- 资源扣除:
- 战士的"魔法值"属性从 30 变为 5
- 这个变化会通过网络同步到所有客户端
- UI 系统更新魔法条显示
消耗 Gameplay Effect 的具体配置示例
GE_Fireball_Cost
的典型配置:
Gameplay Effect: GE_Fireball_Cost
├── Duration Policy: Instant # 瞬间效果,立即生效
└── Modifiers:└── Attribute: Mana├── Modifier Magnitude: Scalable Float (-25.0)└── Modifier Op: Additive # 加法操作,负值表示扣除
更复杂的消耗配置示例
百分比消耗的技能:
Gameplay Effect: GE_PercentageSpell_Cost
├── Duration Policy: Instant
└── Modifiers:└── Attribute: Mana├── Modifier Magnitude: Attribute-Based│ ├── Backing Attribute: Mana (Current Value)│ ├── Coefficient: -0.20 # 消耗当前魔法值的20%│ └── Pre-Multiply: true└── Modifier Op: Additive
多资源消耗的技能:
Gameplay Effect: GE_PowerfulSpell_Cost
├── Duration Policy: Instant
└── Modifiers:├── Attribute: Mana│ ├── Modifier Magnitude: Scalable Float (-40.0)│ └── Modifier Op: Additive└── Attribute: Health├── Modifier Magnitude: Scalable Float (-10.0) # 同时消耗生命值└── Modifier Op: Additive
与 CheckCost 的完美配合
ApplyCost
和 CheckCost
共同构成了完整的消耗机制:
阶段 | 函数 | 作用 | 火球术例子 |
---|---|---|---|
预检查 | CheckCost | 模拟应用,验证资源是否足够 | 检查 30 - 25 = 5 ≥ 0 |
实际执行 | ApplyCost | 真正应用效果,扣除资源 | 实际执行 30 - 25 = 5 |
这种"先检查后执行"的模式确保了:
- 安全性:只有在确认资源足够的情况下才会真正扣除
- 一致性:检查逻辑和执行逻辑使用相同的 Gameplay Effect
- 可预测性:检查结果与实际执行结果完全一致
设计优势
-
基于 Gameplay Effect 的标准化:
- 消耗使用与其他游戏效果相同的系统
- 受益于 GAS 的预测、复制和属性修改机制
-
极大的灵活性:
- 动态消耗:消耗量可以基于属性、技能等级等动态计算
- 复杂消耗:支持多属性消耗、百分比消耗、条件消耗等
- 可视化配置:设计师可以在编辑器中配置复杂的消耗规则
-
网络同步支持:
- 消耗效果自动支持网络复制
- 客户端预测消耗,提供流畅的体验
实际应用场景
场景:随等级变化的消耗
火球术等级提升后,消耗增加但威力也增强:
Gameplay Effect: GE_Fireball_Cost
└── Modifiers:└── Attribute: Mana├── Modifier Magnitude: Scalable Float│ ├── Value: 25.0│ └── Curve Table: FireballCostCurve # 根据技能等级查表└── Modifier Op: Additive
在 FireballCostCurve
中:
- 等级 1:消耗 25 魔法值
- 等级 2:消耗 30 魔法值
- 等级 3:消耗 35 魔法值
当 ApplyCost
调用 GetAbilityLevel()
获取当前技能等级后,会自动从曲线表中查找对应的消耗值。
总结
ApplyCost
虽然代码简洁,但它是技能资源循环的核心执行者:
- 输入:消耗 Gameplay Effect 定义
- 处理:通过标准化的 Gameplay Effect 系统应用效果
- 输出:玩家属性被永久性修改
这种设计确保了技能消耗机制的可靠性、灵活性和可维护性,是 GAS 架构优雅性的完美体现。