LyraLog21 LryaGAS总结1

Who: 谁能放技能?AbilitySystemComponent

How: 技能的逻辑?GameplayAbility

Change: 技能的效果?GameplayEffect

What: 技能改变的属性?GamePlayAttribute

If: 技能涉及的条件?GameplayTag

Visual: 技能的视效?GameplayCue

Async: 技能的长时行动?GameplayTask

Send: 技能消息事件?GamePlayEvent


概括

GAS是虚幻的技能系统

拥有AbilitySystemComponent的对象拥有技能的能力。GameplayAbility描述技能行为,通过 GameplayEffect 来 修改属性 GamePlayAttribute

GameplayCue 来描述技能中纯表现性的东西,通常是音频和例子特效。GE可以配置,也可以在GA中用接口直接加出来。

GameplayTask 来描述技能行为中的长时任务。

===

ASC是一个容器维护了

  • 拥有维护技能对象的容器ActivatableAbilities
  • 维护ActiveGameplayEffects的容器
  • 维护独立Cue的容器(区别于GE附带的) ActiveGameplayCues
  • 技能预测效果的Tag ReplicatedPredictionKeyMap
  • 手动添加的Tag 的容器 ReplicatedLooseTags

ASC是一个UActorComponent。通常挂在角色或者PlayerState上。但是也不绝对。

例如lyra在gamestate挂了一个。game state是全局唯一、所有客户端都可访问且自动复制的 Actor,所以它非常适合承载与任何单个玩家无关的、游戏全局级别的 GAS 逻辑。lyra在 ULyraGamePhaseSubsystem 维护阶段逻辑

GameplayCue 来描述技能中纯表现性的东西,通常是音频和例子特效。GE可以配置,也可以在GA中用Execute GameplayCueWithParamsOnOwner来调用

GameplayTask 用来描述技能行为中的长时行为。例如:

  • 等待输入 WaitInputPress/Release
  • 播放蒙太奇并等待完成 PlayMontageAndWait

技能集配置和获取失去

技能集的设计 基于DataAsset配置 动态获得和失去 GA GE AS

class UEqZeroAbilitySet : public UPrimaryDataAsset
{
    // GA + AbilityLevel + InputTag
    UPROPERTY(EditDefaultsOnly, Category = "Gameplay Abilities", meta=(TitleProperty=Ability))
    TArray<FEqZeroAbilitySet_GameplayAbility> GrantedGameplayAbilities;

    // GE + EffectLevel
    UPROPERTY(EditDefaultsOnly, Category = "Gameplay Effects", meta=(TitleProperty=GameplayEffect))
    TArray<FEqZeroAbilitySet_GameplayEffect> GrantedGameplayEffects;

    // AS
    UPROPERTY(EditDefaultsOnly, Category = "Attribute Sets", meta=(TitleProperty=AttributeSet))
    TArray<FEqZeroAbilitySet_AttributeSet> GrantedAttributes;
};

这个配置的获得可以关联武器的装备和脱下,或者GameFeatureAction_AddAbilities

技能的触发

技能激活策略

我们自己定义了技能的触发方式

UENUM(BlueprintType)
enum class EEqZeroAbilityActivationPolicy : uint8
{
    OnInputTriggered,
    WhileInputActive,
    OnSpawn
};

按下触发,按住一直触发,角色OnSpawn时触发

增强输入到技能激活

在增强输入中,我们绑定了InputAction触发到某个 参数为GameplayTag的回调函数的触发。然后转发给ASC

具体细节:

输入数据结构

UEqZeroInputConfig (DataAsset)
 ├─ NativeInputActions[]   → Move, Look, Crouch 等固定操作
 └─ AbilityInputActions[]  → 每条是 { UInputAction, FGameplayTag }
                              例如: { IA_Fire, InputTag.Ability.Fire }
                              例如: { IA_Jump, InputTag.Ability.Jump }

绑定层:HeroComponent 在 InitState 数据就绪后绑定输入

从而 输入到 GameplayTag 的流程打通,技能能够得到这个输入

HandleChangeInitState (DataAvailable → DataInitialized)
 └─ InitializePlayerInput(InputComponent)
     ├─ AddMappingContext (IMC → EnhancedInput 子系统)
     ├─ BindNativeAction (Move/Look/Crouch → 固定函数)
     └─ BindAbilityActions (所有 AbilityInputActions)
         → 对每个 { InputAction, InputTag }:
             Triggered → Input_AbilityInputTagPressed(InputTag)
             Completed → Input_AbilityInputTagReleased(InputTag)

路由层:HeroComponent → ASC 的 Tag 缓冲

// HeroComponent 收到输入回调
void Input_AbilityInputTagPressed(FGameplayTag InputTag) {
    EqZeroASC->AbilityInputTagPressed(InputTag);  // 转发给 ASC
}

ASC 收到 InputTag 后,【不立即激活技能】,而是写入三个缓冲数组:

void AbilityInputTagPressed(const FGameplayTag& InputTag) {
    // 遍历所有已授予的 GA,找 DynamicSpecSourceTags 匹配此 InputTag 的
    for (AbilitySpec : ActivatableAbilities.Items) {
        if (AbilitySpec.GetDynamicSpecSourceTags().HasTagExact(InputTag)) {
            InputPressedSpecHandles.AddUnique(AbilitySpec.Handle);  // 按下队列
            InputHeldSpecHandles.AddUnique(AbilitySpec.Handle);     // 持续队列
        }
    }
}

void AbilityInputTagReleased(const FGameplayTag& InputTag) {
    for (AbilitySpec : ActivatableAbilities.Items) {
        if (AbilitySpec.GetDynamicSpecSourceTags().HasTagExact(InputTag)) {
            InputReleasedSpecHandles.AddUnique(AbilitySpec.Handle); // 释放队列
            InputHeldSpecHandles.Remove(AbilitySpec.Handle);        // 移出持续队列
        }
    }
}

InputTag → GA 的匹配机制:AbilitySet 在 GiveAbility 时,将 InputTag 写入 AbilitySpec.DynamicAbilityTags。ASC 通过 GetDynamicSpecSourceTags().HasTagExact(InputTag) 查找匹配的 GA。

FGameplayAbilitySpec AbilitySpec(AbilityCDO, AbilityToGrant.AbilityLevel);
AbilitySpec.SourceObject = SourceObject;
AbilitySpec.GetDynamicSpecSourceTags().AddTag(AbilityToGrant.InputTag);

const FGameplayAbilitySpecHandle AbilitySpecHandle = EqZeroASC->GiveAbility(AbilitySpec);

处理层:PostProcessInput 统一消费

PlayerController::PostProcessInput (每帧 Tick)
 └─ ASC::ProcessAbilityInput(DeltaTime, bGamePaused)

ProcessAbilityInput 是整个输入→技能的核心调度,一帧内按严格顺序处理:

① 检查全局阻塞 — HasMatchingGameplayTag(TAG_Gameplay_AbilityInputBlocked)
   → 如果阻塞,ClearAbilityInput() 丢弃所有输入,直接返回

② 处理 WhileInputActive(持续按住的技能)
   → 遍历 InputHeldSpecHandles
   → 技能未激活 && ActivationPolicy == WhileInputActive → 加入待激活列表

③ 处理 OnInputTriggered(按下触发的技能)
   → 遍历 InputPressedSpecHandles
   → 技能已激活 → AbilitySpecInputPressed (传递输入事件给运行中的技能)
   → 技能未激活 && ActivationPolicy == OnInputTriggered → 加入待激活列表

④ 统一激活
   → 遍历待激活列表,TryActivateAbility(Handle)
   → 之所以统一激活,是避免 WhileInputActive 激活后又被 OnInputTriggered 发送事件

⑤ 处理 InputReleased(松开的技能)
   → 遍历 InputReleasedSpecHandles
   → 技能已激活 → AbilitySpecInputReleased (传递释放事件给运行中的技能)
   → InputPressed = false

⑥ 清空 Pressed/Released 缓冲(Held 保留到下一帧)

这么设计的意义是

维护 FGameplayAbilitySpec->InputPressed 的值,能对 WaitInputPress/Release 产生作用。通过ASC接口 AbilitySpecInputPressed/Release

对于【按下】触发的技能,按下激活成功,继续按住,被判断掉了,没有实际作用

对于【按住】触发的技能,按下激活成功,继续按住会一直 InputPressed=true并且传递事件,但是技能激活中不能再次激活,这个自动步枪技能的结束是基于蒙太奇时间的

OnSpawn 自动激活

OnSpawn 策略的 GA 在两个时机尝试激活:

① OnGiveAbility — GA 被授予时
   UEqZeroGameplayAbility::OnGiveAbility
     → TryActivateAbilityOnSpawn(ActorInfo, Spec)

② InitAbilityActorInfo — Avatar 切换时(重生)
   UEqZeroAbilitySystemComponent::InitAbilityActorInfo
     → bHasNewPawnAvatar == true
       → TryActivateAbilitiesOnSpawn()  // 遍历所有已授予 GA
         → 每个 GA 的 TryActivateAbilityOnSpawn

为什么需要两个时机:

  • 时机①:正常流程——先有 Avatar,后授予技能(Equipment 装备时、GameFeature 注入时)
  • 时机②:ASC 在 PlayerState 上 + 重生流程——技能早就授予了(上一条命),但 Avatar 是新的 Pawn

激活条件

void TryActivateAbilityOnSpawn(ActorInfo, Spec) const {
    if (ActivationPolicy != OnSpawn) return;
    if (Spec.IsActive()) return;           // 已经在运行了
    if (AvatarActor->GetTearOff()) return; // 正在销毁
    if (AvatarActor->GetLifeSpan() > 0) return; // 即将销毁

    // 根据 NetExecutionPolicy 决定谁来激活
    bool bLocalShouldActivate  = IsLocallyControlled() && (LocalPredicted || LocalOnly);
    bool bServerShouldActivate = IsNetAuthority()       && (ServerOnly || ServerInitiated);

    if (bLocalShouldActivate || bServerShouldActivate)
        ASC->TryActivateAbility(Spec.Handle);
}

要点:

  • Spec.IsActive() 防止重复激活(重生时已有 GA 可能还在运行)
  • GetTearOff() / GetLifeSpan() 过滤正在销毁的 Avatar
  • 网络角色检查确保只在正确端激活

典型用途,需要在角色生成时激活的监听类技能。

例如交互技能,周期性扫描周围的可交互物。

激活失败

TryActivateAbility(Handle)
  → InternalTryActivateAbility (引擎)
    → CanActivateAbility 失败
      → NotifyAbilityFailed(Handle, Ability, FailureReason)  ← 引擎调用

ASC 重写的 NotifyAbilityFailed:

NotifyAbilityFailed(Handle, Ability, FailureReason)
  ├─ Super::NotifyAbilityFailed
  │    → 触发 ASC 的 AbilityFailedCallbacks 委托(通用监听)
  │
  ├─ 判断是否需要转发给客户端
  │    if (!Avatar->IsLocallyControlled() && Ability->IsSupportedForNetworking())
  │      → ClientNotifyAbilityFailed (Client RPC, Unreliable)
  │         → HandleAbilityFailed (客户端执行)
  │    else
  │      → HandleAbilityFailed (本地直接执行)
  │
  └─ HandleAbilityFailed
       → EqZeroAbility->OnAbilityFailedToActivate(FailureReason)
          ├─ NativeOnAbilityFailedToActivate (C++)
          │    ├─ 查 FailureTagToUserFacingMessages 表 → 广播 UI 消息
          │    └─ 查 FailureTagToAnimMontage 表 → 广播失败动画
          └─ ScriptOnAbilityFailedToActivate (蓝图可重写)

简单来说就是技能失败了,有个回调,我们自己广播一下。

技能的互斥管理

enum class EEqZeroAbilityActivationGroup : uint8 {
    Independent,           // 独立,不受任何约束(默认,绝大多数技能)
    Exclusive_Replaceable, // 排他可替换,会被新的排他技能取消
    Exclusive_Blocking     // 排他阻塞,阻止其他排他技能激活
};

这是ASC上的一个 int32 ActivationGroupCounts[(uint8)EEqZeroAbilityActivationGroup::MAX];

每个技能属于一个组,数值相当于一个计数器

技能激活 → NotifyAbilityActivated
  → AddAbilityToActivationGroup(Group, Ability)
    ├─ Independent: 无操作
    └─ Exclusive_Replaceable / Exclusive_Blocking:
        → CancelActivationGroupAbilities(Exclusive_Replaceable, 排除自身)
        → 也就是:新的排他技能进来,取消所有"可替换"的排他技能

技能结束 → NotifyAbilityEnded
  → RemoveAbilityFromActivationGroup(Group, Ability)
    → ActivationGroupCounts[Group]--

ActivationGroupCounts 是一个计数器数组,IsActivationGroupBlocked 通过检查 Exclusive_Blocking 计数 > 0 来阻塞:

bool IsActivationGroupBlocked(Group) const {
    if (Group == Independent) return false;  // 独立永远不被阻塞
    // Replaceable 和 Blocking 都在有 Blocking 技能时被阻塞
    return ActivationGroupCounts[Exclusive_Blocking] > 0;
}

虽然死亡确实是占用了 Exclusive_Blocking,但是却不会阻塞 Independent 因为

bool UEqZeroGameplayAbility::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, FGameplayTagContainer* OptionalRelevantTags) const
{
    if (!ActorInfo || !ActorInfo->AbilitySystemComponent.IsValid())
    {
        return false;
    }

    if (!Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags))
    {
        return false;
    }

    //@TODO Possibly remove after setting up tag relationships
    UEqZeroAbilitySystemComponent* EqZeroASC = CastChecked<UEqZeroAbilitySystemComponent>(ActorInfo->AbilitySystemComponent.Get());
    if (EqZeroASC->IsActivationGroupBlocked(ActivationGroup))
    {
        if (OptionalRelevantTags)
        {
            OptionalRelevantTags->AddTag(EqZeroGameplayTags::Ability_ActivateFail_ActivationGroup);
        }
        return false;
    }

    return true;
}

官方说这部分可能被删除。。。

本质上是技能激活 CanActivateAbility 流程的自定义

GameplayAbility::CanActivateAbility 里面有三个虚函数 CD, Cost,tag检查,我们可以重写。

例如ASC的配置 TObjectPtr TagRelationshipMapping; 感觉这个才是重点

就重写了一张表,配置了技能的阻塞和取消关系。

这个表会在ability的接口 DoesAbilitySatisfyTagRequirements 中重写。就是上面所说的 CanActivateAbility 中三个重写中tag检查那部分。

===

为什么这么设计,相比GameplayAbility直接配置有什么好处?

原生 GA Tag 配置

GA_RangedWeapon:
  BlockAbilitiesWithTag: [Ability.Type.Action.Reload]
  CancelAbilitiesWithTag: []
  ActivationBlockedTags: [Status.Death]
  ActivationRequiredTags: []

问题:每个 GA 各管各的。项目有 20 个 GA,新增 GA_Grenade (手雷) 需要被冲刺阻塞:

  • 改 GA_Dash 加 BlockAbilitiesWithTag → 要改已有 GA
  • 或者改 GA_Grenade 加 ActivationBlockedTags → 要知道所有可能阻塞它的技能的 Tag
  • 再加一个 GA_Slide 也阻塞手雷 → 又要改

N 个技能有互斥关系 = 改 N 个 GA 的配置。关系分散在每个 GA 身上,新增/修改一条规则要翻遍所有相关 GA。

TagRelationshipMapping — 集中式规则表

TagRelationshipMapping DataAsset (一张表管所有):
  Ability.Type.Action.Dash:
    Block:  [Ability.Type.Action.Melee, Ability.Type.Action.Grenade]
    Cancel: [Ability.Type.Action.Reload]
  Ability.Type.Action.Death:
    Block:  [Ability.Type.Action]  // 一条规则阻塞所有 Action 类技能

ActivationGroupCounts — 解决一类完全不同的问题

如果用 Tag 做:

  • GA_Death 的 BlockAbilitiesWithTag 要列出 所有 非独立技能的 Tag
  • 每新增一个技能都要记得加到这个列表

实际的效果,死亡技能激活的时候,会在ActivationGroupCounts里面写一个阻塞

阻塞技能在进组的时候会 取消所有Exclusive_Replaceable的技能

Cost和CD

CD 在lyra中只是一个简单的GE,配置了一个数字。

Cost 本质上是自己重写Ability的 CheckCost

引擎的流程好像是,基于Attr的修改,看能否成功。

Lyra自定义的流程是 定义一个Cost数组,UEqZeroAbilityCost父类是Uobject

UEqZeroAbilityCost 的子类实现CheckCost,ApplyCost

这里面通过拿到Actor,Control 去拿到 库存组件等做一些逻辑

GameplayTag的管理

后面再重构吧

[根命名空间]           [职责]                              [定义位置]
─────────────────────────────────────────────────────────────────────
Ability.*              技能系统元数据(类型/行为/失败原因)    C++ Native
State.*                实体状态标签(替代旧 Status)          C++ Native
Input.*                输入映射标签                          C++ Native
Event.*                GAS GameplayEvent 事件               C++ Native + INI
Effect.*               GE 分类(伤害类型/治疗类型/Trait)     INI
Cue.*                  GameplayCue 视觉/音效                INI
Message.*              GameplayMessageSubsystem 通道        INI
Movement.*             移动模式                             C++ Native
Phase.*                游戏流程/阶段                         INI(GameFeature)
UI.*                   UI 层级/HUD 插槽                     INI
Platform.*             平台能力检测                          INI
Cosmetic.*             外观/表现                            INI
SetByCaller.*          GE 数值传递                          C++ Native
Cheat.*                调试作弊                             C++ Native

技能实现总结

基于当前阶段,可能不完全

GA_Jump

bool CanActivateAbility(...) {
    return EqZeroCharacter->CanJump()
}

void CharacterJumpStart() {
    if (Character->IsLocallyControlled() && !Character->bPressedJump) {
        Character->UnCrouch()
        Character->Jump();    
    }
}

void CharacterJumpStop() {
    if (Character->IsLocallyControlled() && !Character->bPressedJump) {
        Character->StopJumping();    
    }
}

void ActivateAbility(...)
    CharacterJumpStart()
    AbilityTask_StartAbilityState("Jumping")
        OnStateEnd -> CharacterJumpStop()
        OnStateInterruped-> CharacterJumpStop()
    AbilityTask_WaitInputRelease
        OnRelease->EndAbility

简单的伪代码,包括蓝图逻辑。所以,跳跃键没有松开的时候,角色虽然落地了,但是技能并没有结束。

GA_Dash

主要流程

  1. 方向计算 — 玩家 vs AI 双路径

玩家 GetLastMovementInputVector(输入方向)+ 摄像机前向(含 Z 轴 1.2x 偏移)

AI NavigationSystem::ProjectPointToNavigation 投影到导航网格获取导航方向,退化时使用 Actor 朝向

  1. 方向蒙太奇选择 — 角度区间映射

根据前向量和 输入向量的Yaw插值,判断用哪个蒙太奇做动作

void ActivateAbility(...)
{
    if (IsLocallyControlled())
    {
        // 计算移动意图,获取移动方向和该方向的蒙太奇
        Direction = ...;
        Montage = ...;

        // 提交技能
        CommitAbility(...);

        if (HasAuthority(&ActivationInfo))
        {
            // 单机或 Listen Server:直接执行
            ExecuteDash(Direction, Montage);
        }
        else
        {
            // DS 客户端:本地预测执行 + RPC 通知服务器
            ExecuteDash(Direction, Montage);
            ServerSendInfo(Direction, Montage);
        }
    }
}

我们讨论DS的情况

ExecuteDash + ServerSendInfo(server rpc)

ExecuteDash:

  • AbilityTask_PlayMontageAndWait:播放方向蒙太奇动画(不含 Root Motion,纯姿态动画)
  • AbilityTask_ApplyRootMotionConstantForce:施加恒定力的 Root Motion,参数包括:
    • Strength = 1850(推力大小)
    • RootMotionDuration = 0.25s(Root Motion 持续时间)
    • ClampVelocity 模式,最大速度 1000
    • bIsAdditive = true(叠加到现有移动)

ServerSendInfo_Implementation:

把Direction,Montage 通过RPC带到服务器了

  • CommitAbility
  • ExecuteDash 这里和客户端跑同样的流程

CD的配置

cooldowns里面配置一个普通的GE的子类蓝图

这是一个周期性的GE,1.5s 并且会给Owner 加一个TAG

GA_Melee

项目的GA默认是LocalPredicated

void UEqZeroGameplayAbility_Melee::ActivateAbility(...)
{
    Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

    CommitAbility(Handle, ActorInfo, ActivationInfo);

    PlayMeleeMontage();

    if (HasAuthority(&ActivationInfo))
    {
        MeleeCapsuleTrace();
    }
}

双端的逻辑

Commit 技能

PlayMontageAndWait

服务端执行碰撞检测

如果检测到了:

ApplyRootMotion, Apply GE 和 Cue

这里GE是一个伤害的GE,Cue还没写。

项目的伤害GE配置

/Game/GameplayEffects/Damage/GameplayEffectParent_Damage_Basic.GameplayEffectParent_Damage_Basic

/Game/GameplayEffects/Damage/GE_Damage_Basic_Instant.GE_Damage_Basic_Instant

/Game/GameplayEffects/Damage/GE_Damage_Basic_Periodic.GE_Damage_Basic_Periodic

GameplayEffectParent_Damage_Basic

配置了一个 GE.Damage.Basic 的 TAG 和 Execution伤害计算器,还有一点伤害

子类两个:

  • GE_Damage_Basic_Instant
    • 追加了一个TAG GE.Damage.Instant 描述瞬时伤害
  • GE_Damage_Basic_Periodic
    • 追加了一个TAG GE.Damage.Periodic 描述周期伤害,但是周期配置是。无限的1s间隔触发

瞬时伤害GE_Damage_Basic_Instant的子类包括

  • Melee
    • 追加TAG GE.Damage.Melee
    • 修改了伤害值
  • RifleAuto
    • 追加TAG GE.Damage.RifleAuto
    • 修改了伤害值
  • Pistol
    • 追加TAG GE.Damage.Pistol
    • 修改了伤害值

周期伤害GE_Damage_Basic_Periodic的子类

/Game/Environments/Gameplay/GE_GameplayEffectPad_Damage.GE_GameplayEffectPad_Damage

追加了TAG,伤害值改了一下。

地面上哪个周期性扣血的圈。

GA_Death

生命值组件检测到 HP 小于0的时候,通过 event 触发的。

他配置了CameraMode

取消和阻塞所有 Ability.Type.Action的技能。这个和 TagRelationshipMapping 好像有点重复了

ServerInitiated 这是一个服务器技能

void ActivateAbility(...)

  • 取消所有技能除了 Ability_Behavior_SurvivesDeath(重生技能) 和自己的技能
  • 触发 StartDeath,在 ASC的 LooseGameplayTag 标记一个 Status_Death_Dying = 1 【广播事件】
    • 这个事件做了很多事,例如B_Hero_Default里面,播放蒙太奇,布娃娃
  • 设置摄像机模式
  • 触发GameplayCue
    • 播放例子特效,设置材质消融
  • 添加WaitDelay任务,等8s后 死亡结束

GA_AutoRespawn

注意这是一个OnSpawn技能

所以他监听的是 生命值组件的 OnDeathStarted 事件

他会在 OnDeathStarted 触发的时候注册一个timer 5s 去激活 OnPlayerReset

OnPlayerReset 的时候

  • 去结束死亡技能
  • 调用 GameMode->RequestPlayerRestartNextFrame,这会触发 APlayerController::ServerRestartPlayer_Implementation 的流程,从而走到UE的重生里面。

ServerRestartPlayer_Implementation

这里面会有一些GameMode重写的函数,例如

GameMode->PlayerCanRestart

GameMode->RestartPlayer(this);

===

开火技能

/Game/Weapons/GA_Weapon_Fire.GA_Weapon_Fire 核心逻辑是这个,

子类手枪和自动步枪的开火,主要是配置上的修改

这个前两篇很详细了略

交互技能

/Game/Input/Abilities/GA_Interact.GA_Interact 父类 UEqZeroGameplayAbility_Interact

这是一个OnSpawn技能,

激活触发了 UAbilityTask_GrantNearbyInteraction 会角色周围扫描范围内的交互物(实现了对应接口的物体)

同时在蓝图的激活时,他又会 寻找交互物,这次是一条射线,找到会显示一个交互UI,按G可以调用上面获得的交互技能。

例如他从某个石头上读取到了交互技能 /Game/Interact/GA_Interaction_Collect.GA_Interaction_Collect

开镜技能

LocalPredicted

比较简单,但是IMC里面还缺点东西。

void UEqZeroGameplayAbility_ADS::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
    Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

    // 切换 ADS 摄像机
    SetCameraMode(DefaultCameraMode);

    // 缓存默认移速,并应用 ADS 移速倍率
    if (AEqZeroCharacter* Character = GetEqZeroCharacterFromActorInfo())
    {
        if (UCharacterMovementComponent* Movement = Character->GetCharacterMovement())
        {
            MaxWalkSpeedDefault = Movement->MaxWalkSpeed;
            Movement->MaxWalkSpeed = MaxWalkSpeedDefault * ADSMultiplier;
        }
    }

    // 添加 ADS 输入映射上下文(更高优先级使移速修改器生效)
    if (AEqZeroPlayerController* PC = GetEqZeroPlayerControllerFromActorInfo())
    {
        if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
        {
            if (ADSInputMappingContext)
            {
                FModifyContextOptions Options;
                Options.bForceImmediately = true;
                Subsystem->AddMappingContext(ADSInputMappingContext, ADSMappingContextPriority, Options);
            }
        }
    }

    // 本地端:通知 UI、播放音效
    if (IsLocallyControlled())
    {
        BroadcastToUI(true);

        if (ZoomInSound)
        {
            UGameplayStatics::PlaySound2D(GetWorld(), ZoomInSound);
        }
    }

    // 等待输入松开后结束技能
    UAbilityTask_WaitInputRelease* WaitInputReleaseTask = UAbilityTask_WaitInputRelease::WaitInputRelease(this, true);
    WaitInputReleaseTask->OnRelease.AddDynamic(this, &UEqZeroGameplayAbility_ADS::OnInputReleased);
    WaitInputReleaseTask->ReadyForActivation();
}
上一篇
下一篇