UE5GAS GameAbility源码解析 CommitAbility

文章目录


一、 CommitAbility

cpp 复制代码
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)

cpp 复制代码
if (!CommitCheck(Handle, ActorInfo, ActivationInfo, OptionalRelevantTags))
{
    return false;
}
  • 作用 :这是技能释放前的最后一道防线。它内部会调用 K2_CommitCheck 蓝图可重写函数,以及检查与该技能关联的 技能标签 (Ability Tags)冷却/消耗属性

  • 火球术例子

    • 战士按下火球术按键,技能开始激活 (ActivateAbility 被调用)。
    • 在技能播放前摇动画的过程中,战士的魔法可能被其他技能吸干,或者被敌人沉默了(添加了 GameplayTag 阻止施法)。
    • 当动画播完,即将发出火球的那一刻,CommitCheck 被调用。
    • 它检查发现:"当前魔法值不足 25" 或者战士身上有"沉默"标签。
    • 结果 :检查失败,函数返回 falseCommitAbility 也返回 false,导致技能释放失败。火球不会射出,动画可能会播放一个失败的反馈,并且冷却和魔法不会被消耗。这是一种"安全回滚"机制。

第2部分:执行消耗 (CommitExecute)

cpp 复制代码
CommitExecute(Handle, ActorInfo, ActivationInfo);
  • 作用 :如果检查通过了,就在这里真正地扣除资源 (如魔法值、体力等)并应用冷却时间
  • 火球术例子
    • CommitCheck 检查通过,战士有足够的蓝,也没有被沉默。
    • CommitExecute 函数被执行。
    • 结果:战士的属性集 (AttributeSet) 上的"魔法值"属性被永久性地减去 25 点。同时,一个为期 5 秒的冷却效果 (Gameplay Effect) 被应用到战士身上,防止他立刻再次使用火球术。

第3部分:调用蓝图事件 (K2_CommitExecute)

cpp 复制代码
K2_CommitExecute();
  • 作用 :这是一个 BlueprintNativeEvent(蓝图可重写事件)。在 C++ 中它是一个空函数,设计目的就是让蓝图设计师可以在这里添加自定义的提交逻辑。
  • 火球术例子
    • 设计师可能想在资源正式扣除的这一刻播放一个特殊的音效或粒子效果。
    • 他们可以在蓝图中重写这个 K2_CommitExecute 事件,然后在里面添加播放音效和粒子的节点。
    • 结果:当代码执行到这里时,就会调用蓝图里实现的逻辑,播放那些效果。

第4部分:广播提交事件 (NotifyAbilityCommit)

cpp 复制代码
ActorInfo->AbilitySystemComponent->NotifyAbilityCommit(this);
  • 作用:通知整个能力系统组件 (AbilitySystemComponent):"这个技能已经成功提交了!"。其他系统可以监听这个事件来做出反应。
  • 火球术例子
    • UI 系统可能监听了这个事件。当收到通知时,它会立刻刷新玩家的魔法条 UI,显示出刚刚扣除 25 点魔法后的新数值,而不需要等待每帧的查询。
    • 成就系统也可能监听这个事件,统计"玩家总共释放了多少次火球术"。
    • 结果:游戏的其他部分及时地知道了这个技能已经成功释放的事实,并做出了相应的响应。

总结与流程梳理

对于"火球术"这个技能,CommitAbility 的完整流程是:

  1. 尝试提交:前摇动画结束,准备发射火球。
  2. 最终检查 :检查魔法值是否 >= 25?身上是否有"沉默"等阻止性标签?→ 如果否,失败返回,什么都不发生
  3. 执行消耗魔法值属性被减去 25,5秒冷却计时开始
  4. 蓝图事件:播放一个"法力消耗"的特殊音效。
  5. 广播事件:UI 系统收到通知,立即更新魔法值显示。
  6. 返回成功:CommitAbility 返回 true,后续的技能逻辑(如生成火球投射物)继续执行。

为什么需要 CommitAbility?

这种设计将 "技能逻辑 " 和 "资源消耗" 进行了分离,提供了极大的灵活性和安全性。

  • 安全性:确保只有在万无一失的情况下才会扣除资源,避免了玩家因网络延迟或瞬间状态变化而卡掉资源却放不出技能的不良体验。
  • 灵活性 :你可以在 ActivateAbility 中处理复杂的逻辑(如瞄准、动画),而将统一的消耗检查放在 CommitCheck 中。CommitExecuteK2_CommitExecute 的分离也让设计师可以轻松地添加视觉效果而不影响核心逻辑。
  • 可扩展性NotifyAbilityCommit 事件让游戏的其他模块能够对技能的成功释放做出反应,实现了系统间的低耦合通信。

简单来说,CommitAbility 是技能从"准备"阶段进入"不可逆的正式执行"阶段的关键大门

二、 CommitCheck

cpp 复制代码
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部分:基础有效性检查

cpp 复制代码
if (!bValidHandle || !bValidActorInfoPieces || !bValidSpecFound) { ... return false; }
  • 作用 :这是最底层的安全检查,确保函数接收到的参数是有效的,并且能在能力系统组件(ASC)中找到对应的技能实例(FGameplayAbilitySpec)。
  • 为何重要:防止在数据无效的情况下继续执行逻辑,导致游戏崩溃。
  • 火球术例子(异常情况 ):
    • 战士释放火球的过程中,技能突然被服务器强制移除(例如,卸下了装备火球术的武器)。
    • 此时,Handle 可能变得无效,或者在 ASC 中 FindAbilitySpecFromHandle 查找不到对应的技能。
    • 结果 :此检查失败,函数返回 false,并打印一条警告日志。CommitAbility 也会因此失败,阻止了一次无效的资源消耗。

第2部分 & 第3部分:冷却时间检查 (CheckCooldown)

cpp 复制代码
if (!AbilitySystemGlobals.ShouldIgnoreCosts() && !CheckCost(...))

作用

  1. ShouldIgnoreCosts(): 同样是全局调试开关,如果开启,则不会检查资源消耗,技能可以无限释放。
  2. CheckCooldown(Handle, ActorInfo, OptionalRelevantTags): 这是实际的资源消耗检查逻辑。它会检查技能所需的属性(如魔法值)是否足够。
  • 火球术例子
    • 战士按下火球术按键开始施法(前摇动画)。
    • 在动画播放的这1秒内,战士受到了敌人的一个"法力燃烧"技能的影响,魔法值被烧掉了 30 点。
    • 动画结束,CommitCheck 被调用。
    • CheckCost 函数执行,它去查询战士当前的魔法值属性,发现当前魔法值(例如只剩 10 点)不足 25 点。
    • 结果CheckCost 返回 false,导致 CommitCheck 返回 false。技能释放失败。正是因为有了这个最终的检查,才避免了"战士在蓝量不足的情况下依然把火球扔出去"的逻辑错误

总结与重要性

CommitCheck 是 GAS 中保证状态同步防止作弊的关键环节。

  1. 解决时序问题 :它解决了从技能激活 (ActivateAbility) 到资源消耗 (Commit) 之间的时间差 所导致的状态不一致问题。在多人游戏中,由于网络延迟和客户端预测,这个时间差会变得更加不可控,CommitCheck 在服务器端的最终裁决显得尤为重要。

  2. CanActivateAbility 的区别:

    • CanActivateAbility 是技能开始时的检查,通常还包括输入是否被抑制等条件。它决定技能按钮是否可被按下、是否能开始播放动画。
    • CommitCheck 是技能即将产生效果和消耗时 的最终检查,只关注最核心的资源条件(冷却和成本) 。一个技能可能 CanActivateAbility 返回 true(能开始播动画),但 CommitCheck 返回 false(动画播完发现没蓝了)。
  3. 安全网:它是资源扣除前的最后一道安全网,确保了游戏的规则不被破坏(例如,玩家不可能在没蓝或无冷却时成功释放技能)。

简单比喻:

  • CanActivateAbility 像是申请信用卡------检查你的信用记录等基本条件,决定是否给你发卡。

  • ActivateAbility 像是刷卡购物------你把卡递给收银员,表示你想买东西。

  • CommitCheck 像是银行批准这笔交易------在扣款前最后一刻,银行检查你的额度是否足够、卡片是否被盗用。

  • CommitExecute 像是最终扣款------检查通过,钱从你的账户划走。

如果没有 CommitCheck,我们的战士就会经常上演"尬舞"(播放了施法动画)却搓不出火球(因为资源在动画期间被消耗了)的奇怪现象。

三、CommitExecute

cpp 复制代码
void UGameplayAbility::CommitExecute(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
    // 1. 应用冷却时间
    ApplyCooldown(Handle, ActorInfo, ActivationInfo);

    // 2. 应用技能消耗
    ApplyCost(Handle, ActorInfo, ActivationInfo);
}

第1部分:应用冷却时间 (ApplyCooldown)

cpp 复制代码
ApplyCooldown(Handle, ActorInfo, ActivationInfo);
  • 作用 :这个方法负责启动技能的冷却计时器 。其内部通常通过向自身(技能的拥有者)应用一个冷却效果(Cooldown Gameplay Effect) 来实现。

  • 内部机制

    1. ApplyCooldown 会创建一个 GameplayEffectSpec
    2. 这个 Effect 会带有一个持续时间(Duration),例如 5 秒。
    3. 它还会添加一个标签(Tag) ,比如 Cooldown.Fireball。这个标签会被 CheckCooldown 函数用来查询技能是否处于冷却状态。
  • 火球术例子

    • 战士成功释放火球术后,代码执行到这里。
    • ApplyCooldown 被调用。
    • 结果:战士的能力系统组件(ASC)上被施加了一个持续 5 秒的冷却效果。在这 5 秒内,任何对 CheckCooldown 的调用都会发现 Cooldown.Fireball 标签存在,从而阻止技能的再次释放。战士技能图标上也会出现一个旋转的冷却计时器。

第2部分:应用技能消耗 (ApplyCost)

cpp 复制代码
ApplyCost(Handle, ActorInfo, ActivationInfo);
  • 作用 :这个方法负责永久性地扣除技能所需的资源。其内部也是通过应用一个消耗效果(Cost Gameplay Effect) 来实现。

  • 内部机制

    1. ApplyCost 会创建一个 GameplayEffectSpec
    2. 这个 Effect 的修改器(Modifier)会使用 ModifierOp::Additive 并提供一个负值,例如 -25.0f,作用于"魔法值(Mana)"属性上。
    3. 这个 Effect 通常是瞬间(Instant) 的,意味着扣除操作立即生效,没有持续时间。
  • 火球术例子

    • 紧接着应用冷却之后,ApplyCost 被调用。
    • 结果 :战士的"魔法值"属性被立刻减去 25 点。这个数值会通过网络同步,更新到所有客户端,因此所有玩家看到的战士蓝条都会减少。如果魔法值不足 25,ApplyCost 甚至会触发一个确保性的检查(尽管之前 CommitCheck 已经查过了),如果真的不够,可能会触发一个"技能执行失败"的回滚逻辑。

流程回顾与重要性

让我们将 CommitCheckCommitExecute 结合起来,看一个完整的"提交"流程:

  1. 最终检查 (CommitCheck):

    • CheckCooldown(): "火球术还在冷却吗?" ->
    • CheckCost(): "魔法值够 25 点吗?" ->
    • 结果 : CommitCheck 返回 true,允许提交。
  2. 执行消耗 (CommitExecute)

    • ApplyCooldown(): 为火球术添加一个 5 秒的冷却效果。
    • ApplyCost(): 从魔法值中永久扣除 25 点。
    • 结果: 资源被正式消耗,技能进入冷却。

为什么使用 Gameplay Effect (GE) 来实现?

这是 GAS 设计的一个精妙之处:

  • 统一性 : 冷却和消耗与游戏中其他所有效果(如 buff、debuff、伤害、治疗)使用同一套系统(GameplayEffect)。这意味着它们受益于相同的预测(Prediction)、复制(Replication)和标签(Tag)机制。
  • 灵活性 : 设计师可以在蓝图中配置一个 GameplayEffect 资产来定义技能的消耗和冷却,而无需修改代码。他们可以轻松地制作一个"消耗生命值而非魔法值"的技能,或者一个"根据敌人数量动态改变冷却时间"的技能。
  • 可扩展性: 因为冷却是一个 GE,它可以被其他系统影响。例如,一个"减少所有技能冷却时间 20%"的 buff,可以直接修改冷却 GE 的持续时间,而无需知道具体是哪个技能。

总结一下
CommitExecute 是一个"动手 "的函数。它不像 CommitCheck 那样只是"动眼"检查。它的工作简单而关键:

  1. 调用 ApplyCooldown -> 给技能"上锁"一段时间

  2. 调用 ApplyCost -> 从玩家身上"扣钱"

这两个操作共同确保了技能的使用是有代价、有限制的,是维持游戏平衡和资源循环的核心环节。

四、CheckCooldown

cpp 复制代码
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部分:安全性检查

cpp 复制代码
if (!ensure(ActorInfo))
{
    return true;
}
  • 作用 :确保 ActorInfo 参数有效。如果无效,使用 ensure 在开发时弹出警告,但函数返回 true(通过检查)。
  • 逻辑:这是一种"失败安全"的设计。当数据异常时,选择让检查通过而不是阻止游戏运行。

第2部分:获取冷却标签

cpp 复制代码
const FGameplayTagContainer* CooldownTags = GetCooldownTags();
if (CooldownTags && !CooldownTags->IsEmpty())
  • 作用:获取这个技能定义的所有冷却标签。
  • 火球术例子
    • GetCooldownTags() 可能会返回包含 Cooldown.Spell.Fireball 的标签容器。
    • 如果技能没有定义冷却标签(容器为空),说明这个技能没有冷却限制,直接返回 true 通过检查。

第3-4部分:检查标签存在性

cpp 复制代码
if (AbilitySystemComponent->HasAnyMatchingGameplayTags(*CooldownTags))
  • 作用:检查能力系统组件当前是否拥有任何与冷却标签匹配的标签。
  • 火球术例子
    • 当战士释放火球术后,ApplyCooldown 会给他添加 Cooldown.Spell.Fireball 标签。
    • 如果战士在冷却期间再次尝试释放,这里检查会发现他拥有 Cooldown.Spell.Fireball 标签。
    • 结果:进入 if 分支,表示冷却检查失败。

第5-6部分:填充失败信息(可选)

cpp 复制代码
if (OptionalRelevantTags)
{
    const FGameplayTag& FailCooldownTag = UAbilitySystemGlobals::Get().ActivateFailCooldownTag;
    if (FailCooldownTag.IsValid())
    {
        OptionalRelevantTags->AddTag(FailCooldownTag);
    }
    
    OptionalRelevantTags->AppendMatchingTags(AbilitySystemComponent->GetOwnedGameplayTags(), *CooldownTags);
}
  • 作用 :如果调用者提供了 OptionalRelevantTags 参数,就填充详细的失败信息。

  • 具体内容

    1. 通用失败标签 :添加一个全局的冷却失败标签,如 Ability.Activate.Fail.Cooldown
    2. 具体阻挡标签 :添加实际导致失败的冷却标签,如 Cooldown.Spell.Fireball
  • 火球术例子

    • 当冷却检查失败时,OptionalRelevantTags 会被填充为:
      • Ability.Activate.Fail.Cooldown(通用失败原因)
      • Cooldown.Spell.Fireball(具体是哪个技能在冷却)
    • 这些信息可用于
      • UI 显示:"火球术还在冷却中!"
      • 播放特定的冷却失败音效
      • 调试日志

第7部分:返回检查结果

cpp 复制代码
return false; // 冷却检查失败

作用 :明确返回 false,表示技能处于冷却状态,不能释放。

第8部分:默认通过

cpp 复制代码
return true; // 冷却检查通过
  • 作用:如果以上所有检查都没问题(没有冷却标签或标签不存在),返回 true,表示技能不在冷却中。

完整工作流程示例

场景1:火球术不在冷却中

  1. 战士按下火球术按键
  2. CheckCooldown 被调用
  3. GetCooldownTags() 返回 Cooldown.Spell.Fireball
  4. 检查战士的 ASC,发现没有 Cooldown.Spell.Fireball 标签
  5. 函数返回 true,冷却检查通过
  6. 技能可以继续释放

场景2:火球术在冷却中

  1. 战士在冷却期间按下火球术按键
  2. CheckCooldown 被调用
  3. GetCooldownTags() 返回 Cooldown.Spell.Fireball
  4. 检查战士的 ASC,发现存在 Cooldown.Spell.Fireball 标签
  5. OptionalRelevantTags 添加失败信息
  6. 函数返回 false,冷却检查失败
  7. 技能释放被阻止

设计优势

  1. 基于标签的灵活性
    • 共享冷却 :多个技能可以使用同一个冷却标签(如 Cooldown.Spell),实现"法术公共冷却"。
    • 分组冷却 :不同类别的技能可以使用不同的冷却标签(如 Cooldown.AttackCooldown.Defense)。
  2. 详细的信息反馈 :通过 OptionalRelevantTags 参数,调用者可以获得详细的失败原因,便于实现丰富的用户反馈。
  3. 与 Gameplay Effect 集成:冷却标签通常由冷却 Gameplay Effect 添加,这使得冷却系统可以受益于 GAS 的完整功能(预测、复制、修饰器等)。

这种基于标签的冷却检查机制是 GAS 的核心设计理念之一,它提供了极大的灵活性和可扩展性。

五、ApplyCooldown

cpp 复制代码
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部分:获取冷却效果定义

cpp 复制代码
UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
  • 作用:获取这个技能对应的冷却 Gameplay Effect(GE)资产。
  • 火球术例子
    • GetCooldownGameplayEffect() 是一个虚函数,通常在技能的蓝图或C++类中重写。
    • 对于火球术,它可能返回一个名为 GE_Fireball_Cooldown 的 Gameplay Effect。
    • 这个 GE 的定义可能包含:
      • 持续时间:5 秒
      • 授予的标签Cooldown.Spell.Fireball(这就是 CheckCooldown 检查的标签)

第2部分:应用冷却效果到拥有者

cpp 复制代码
ApplyGameplayEffectToOwner(Handle, ActorInfo, ActivationInfo, CooldownGE, GetAbilityLevel(Handle, ActorInfo));
  • 作用:将冷却 GE 应用到技能的使用者(战士)的能力系统组件上。

  • 参数说明

    • CooldownGE:上一步获取的冷却效果资产
    • GetAbilityLevel(Handle, ActorInfo):获取技能的当前等级,用于计算可能随等级变化的冷却时间
  • 火球术例子

    • 假设火球术等级为 1,冷却时间为固定的 5 秒。
    • ApplyGameplayEffectToOwner 会在战士的 ASC 上创建并应用这个冷却效果。
    • 实际效果 :战士身上被添加了 Cooldown.Spell.Fireball 标签,持续 5 秒。

完整的工作流程示例

火球术释放流程中的冷却部分

  1. 技能释放:战士按下火球术按键,技能成功通过所有检查
  2. 提交阶段CommitAbility 被调用
  3. 应用冷却ApplyCooldown 执行:
    • 调用 GetCooldownGameplayEffect() 获取 GE_Fireball_Cooldown
    • 调用 ApplyGameplayEffectToOwner 将这个效果应用到战士身上
  4. 冷却生效
    • 战士的 ASC 上现在有了 GE_Fireball_Cooldown 效果
    • 该效果授予 Cooldown.Spell.Fireball 标签,持续 5 秒
    • 5 秒后效果自动移除,标签也随之移除

冷却 Gameplay Effect 的具体配置示例

GE_Fireball_Cooldown 可能这样配置:

cpp 复制代码
Gameplay Effect: GE_Fireball_Cooldown
├── Duration Policy: Has Duration
├── Duration: 5.0 seconds
└── Granted Tags:
    ├── Cooldown.Spell.Fireball
    └── Cooldown.Spell  # 可选的父级标签,用于分组冷却

设计优势与灵活性

  1. 基于 Gameplay Effect 的标准化

    • 冷却使用与其他游戏效果相同的系统,保证了行为的一致性
    • 受益于 GAS 的预测、复制和堆栈机制
  2. 灵活的冷却配置

    • 动态冷却:可以通过 Modifier 让冷却时间基于属性变化
cpp 复制代码
Duration: 5.0 - (0.1 * CooldownReductionAttribute)
  • 等级化冷却:不同技能等级可以有不同的冷却时间
  • 条件性冷却:可以通过标签要求/忽略来实现复杂的冷却逻辑
  1. 冷却标签的层次结构
    • 可以设置父子标签关系,实现分组冷却
    • 例如:Cooldown.Spell.Fireball → Cooldown.Spell → Cooldown
    • 其他系统可以监听不同层级的标签变化

与 CheckCooldown 的配合

ApplyCooldownCheckCooldown 是冷却机制的两个互补部分:

  • ApplyCooldown设置冷却状态(添加标签)
  • CheckCooldown检查冷却状态(检查标签是否存在)

这种基于标签的设计使得冷却系统非常灵活和可扩展,是 GAS 架构的精妙体现。

实际应用场景

场景:减少冷却时间的装备

战士装备了一件"冷却缩减 20%"的装备:

  1. 装备效果添加一个修改 CooldownDuration 属性的 Gameplay Effect
  2. 当火球术释放时,ApplyCooldown 应用的冷却时间会自动计算缩减后的值
  3. 实际的冷却时间变为 4 秒而不是 5 秒
  4. CheckCooldown 仍然正常工作,只是冷却时间变短了

这种设计使得装备、天赋、buff 等系统可以很容易地与冷却机制交互,而无需修改技能本身的代

六、CheckCost

cpp 复制代码
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部分:获取消耗效果定义

cpp 复制代码
UGameplayEffect* CostGE = GetCostGameplayEffect();
  • 作用:获取这个技能对应的消耗 Gameplay Effect 资产。
  • 火球术例子
    • GetCostGameplayEffect() 返回一个名为 GE_Fireball_Cost 的 Gameplay Effect。
    • 这个 GE 可能包含一个对"魔法值"属性的修改器,操作类型为"Add"且值为 -25。

第2部分:获取能力系统组件

cpp 复制代码
UAbilitySystemComponent* AbilitySystemComponent = ActorInfo ? ActorInfo->AbilitySystemComponent.Get() : nullptr;
if (ensure(AbilitySystemComponent))
  • 作用:安全地获取技能使用者的能力系统组件。
  • 确保有效性 :使用 ensure 保证在开发阶段能发现空指针问题。

第3部分:关键检查 - 能否应用属性修改器

cpp 复制代码
if (!AbilitySystemComponent->CanApplyAttributeModifiers(CostGE, GetAbilityLevel(Handle, ActorInfo), MakeEffectContext(Handle, ActorInfo)))
  • 作用:这是核心的检查逻辑。它尝试"模拟"应用消耗效果,看是否会违反任何限制。
  • 内部机制
  1. CanApplyAttributeModifiers 会遍历 GE 中的所有属性修改器
  2. 对于每个修改器,它会计算应用后的结果,并检查是否违反限制
  3. 最重要的检查:对于"增加值"为负数的修改器(即消耗),它会检查当前属性值是否足够(即属性值 + 修改值 ≥ 0)
  • 火球术例子
    • 当前战士魔法值 = 30,火球术消耗 = 25
    • CanApplyAttributeModifiers 计算:30 + (-25) = 5 ≥ 0 → 通过检查
    • 当前战士魔法值 = 20,火球术消耗 = 25
    • CanApplyAttributeModifiers 计算:20 + (-25) = -5 < 0 → 检查失败

第4部分:处理检查失败

cpp 复制代码
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部分:默认通过

cpp 复制代码
return true;
  • 作用:如果以上检查都通过,或者技能没有定义消耗效果,返回 true。

完整工作流程示例

场景1:魔法值充足

  1. 战士按下火球术按键
  2. CheckCost 被调用
  3. 获取 GE_Fireball_Cost(消耗25魔法值)
  4. CanApplyAttributeModifiers 模拟计算:30 - 25 = 5 ≥ 0
  5. 函数返回 true,消耗检查通过
  6. 技能可以继续释放

场景2:魔法值不足

  1. 战士按下火球术按键
  2. CheckCost 被调用
  3. 获取 GE_Fireball_Cost(消耗25魔法值)
  4. CanApplyAttributeModifiers 模拟计算:20 - 25 = -5 < 0
  5. OptionalRelevantTags 添加失败标签
  6. 函数返回 false,消耗检查失败
  7. 技能释放被阻止

消耗 Gameplay Effect 的具体配置示例

GE_Fireball_Cost 可能这样配置:

cpp 复制代码
Gameplay Effect: GE_Fireball_Cost
├── Duration Policy: Instant
└── Modifiers:
    └── Attribute: Mana
        ├── Modifier Magnitude: Scalable Float (-25.0)
        └── Modifier Op: Additive

设计优势与特点

  1. 统一的检查机制

    • 使用与真实消耗相同的 Gameplay Effect 系统进行检查
    • 确保检查逻辑与实际消耗逻辑完全一致
  2. 支持复杂的消耗规则

    • 多属性消耗:可以同时检查魔法值、体力值等多个属性
    • 条件性消耗:基于技能等级或其他属性的动态消耗
    • 百分比消耗:消耗当前魔法值的 10%,而不是固定值
  3. 属性限制系统的集成

    • 自动继承属性系统的所有限制规则
    • 例如,如果某个属性被标记为"不能低于0",CanApplyAttributeModifiers 会自动强制执行这个限制

与 ApplyCost 的配合

CheckCostApplyCost 是消耗机制的两个互补部分:

  • CheckCost预检查是否能支付消耗(模拟应用)
  • ApplyCost实际执行消耗(真正应用效果)

这种"先检查后执行"的模式确保了资源消耗的安全性。

实际应用场景

场景:百分比消耗的技能

假设有一个技能消耗当前魔法值的 20%:

cpp 复制代码
Gameplay Effect: GE_PercentageSpell_Cost
└── Modifiers:
    └── Attribute: Mana
        ├── Modifier Magnitude: Attribute-Based
        │   ├── Attribute: Mana (Current Value)
        │   └── Coefficient: -0.2  # 消耗20%
        └── Modifier Op: Additive

CheckCost 的工作流程:

  1. 获取当前魔法值(例如 100)
  2. 计算消耗量:100 × 20% = 20
  3. 检查:100 - 20 = 80 ≥ 0 → 通过检查

这种设计使得实现复杂的消耗规则变得非常简单和统一。

七、ApplyCost

cpp 复制代码
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部分:获取消耗效果定义

cpp 复制代码
UGameplayEffect* CostGE = GetCostGameplayEffect();
  • 作用:获取这个技能对应的消耗 Gameplay Effect 资产。
  • 火球术例子
    • GetCostGameplayEffect() 返回一个名为 GE_Fireball_Cost 的 Gameplay Effect。
    • 这个 GE 包含一个对"魔法值"属性的修改器,操作类型为"Add"且值为 -25。

第2部分:应用消耗效果到拥有者

cpp 复制代码
ApplyGameplayEffectToOwner(Handle, ActorInfo, ActivationInfo, CostGE, GetAbilityLevel(Handle, ActorInfo));

作用 :将消耗 GE 应用到技能的使用者(战士)的能力系统组件上。
参数说明

  • CostGE:消耗效果资产

  • GetAbilityLevel(Handle, ActorInfo):技能等级,用于计算可能随等级变化的消耗量

  • 火球术例子

    • ApplyGameplayEffectToOwner 在战士的 ASC 上创建并应用这个消耗效果。
    • 实际效果:战士的"魔法值"属性被永久性地减去 25 点。

完整的工作流程示例

火球术释放流程中的消耗部分

  1. 预检查阶段CheckCost 通过模拟应用验证战士有足够魔法值(30 ≥ 25)
  2. 提交阶段CommitAbility 被调用,通过所有检查
  3. 实际消耗ApplyCost 执行:
    • 调用 GetCostGameplayEffect() 获取 GE_Fireball_Cost
    • 调用 ApplyGameplayEffectToOwner 将这个效果应用到战士身上
  4. 资源扣除
    • 战士的"魔法值"属性从 30 变为 5
    • 这个变化会通过网络同步到所有客户端
    • UI 系统更新魔法条显示

消耗 Gameplay Effect 的具体配置示例

GE_Fireball_Cost 的典型配置:

cpp 复制代码
Gameplay Effect: GE_Fireball_Cost
├── Duration Policy: Instant  # 瞬间效果,立即生效
└── Modifiers:
    └── Attribute: Mana
        ├── Modifier Magnitude: Scalable Float (-25.0)
        └── Modifier Op: Additive  # 加法操作,负值表示扣除

更复杂的消耗配置示例

百分比消耗的技能

cpp 复制代码
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

多资源消耗的技能

cpp 复制代码
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 的完美配合

ApplyCostCheckCost 共同构成了完整的消耗机制:

阶段 函数 作用 火球术例子
预检查 CheckCost 模拟应用,验证资源是否足够 检查 30 - 25 = 5 ≥ 0
实际执行 ApplyCost 真正应用效果,扣除资源 实际执行 30 - 25 = 5

这种"先检查后执行"的模式确保了:

  1. 安全性:只有在确认资源足够的情况下才会真正扣除
  2. 一致性:检查逻辑和执行逻辑使用相同的 Gameplay Effect
  3. 可预测性:检查结果与实际执行结果完全一致

设计优势

  1. 基于 Gameplay Effect 的标准化

    • 消耗使用与其他游戏效果相同的系统
    • 受益于 GAS 的预测、复制和属性修改机制
  2. 极大的灵活性

    • 动态消耗:消耗量可以基于属性、技能等级等动态计算
    • 复杂消耗:支持多属性消耗、百分比消耗、条件消耗等
    • 可视化配置:设计师可以在编辑器中配置复杂的消耗规则
  3. 网络同步支持

    • 消耗效果自动支持网络复制
    • 客户端预测消耗,提供流畅的体验

实际应用场景

场景:随等级变化的消耗

火球术等级提升后,消耗增加但威力也增强:

cpp 复制代码
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 架构优雅性的完美体现。

相关推荐
zhangzhangkeji2 小时前
UE5 多线程(4):资源竞争与原子变量。UE 建议使用 STL版本的原子量,不用自己版本的原子量 TAtomic<T> 的实现了
ue5
AI视觉网奇2 小时前
ue slot 插槽用法笔记
笔记·学习·ue5
lllljz3 小时前
Blender导出模型到Unity或UE5引擎材质丢失模型出错
unity·ue5·游戏引擎·blender·材质
AI视觉网奇3 小时前
blender fbx 比例不对 比例调整
笔记·学习·ue5
哎呦哥哥和巨炮叔叔3 小时前
Unreal Engine 是否支持光线追踪?UE5 光线追踪原理与性能解析
ue5·unreal engine·光线追踪·lumen·实时渲染·渲染101云渲染·ue云渲染
zhangzhangkeji3 小时前
UE5 多线程(3):线程退出与单例线程
ue5
AI视觉网奇4 小时前
static mesh 转skeleton mesh
笔记·学习·ue5
AI视觉网奇20 小时前
metahuman 购买安装记录
笔记·学习·ue5
速冻鱼Kiel21 小时前
虚幻状态树解析
ue5·游戏引擎·虚幻
暮志未晚Webgl1 天前
UE5游戏打包
游戏·ue5