LyraLog11 GAS 3 跳跃&冲刺&近战

跳跃技能

class UEqZeroGameplayAbility_Jump : public UEqZeroGameplayAbility {}

C++很简单,就是角色的那几个接口

UEqZeroGameplayAbility_Jump::UEqZeroGameplayAbility_Jump(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
    NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
}

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

    const AEqZeroCharacter* EqZeroCharacter = Cast<AEqZeroCharacter>(ActorInfo->AvatarActor.Get());
    if (!EqZeroCharacter || !EqZeroCharacter->CanJump())
    {
        return false;
    }

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

    return true;
}

void UEqZeroGameplayAbility_Jump::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
    CharacterJumpStop();
    Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}

void UEqZeroGameplayAbility_Jump::CharacterJumpStart()
{
    if (AEqZeroCharacter* EqZeroCharacter = GetEqZeroCharacterFromActorInfo())
    {
        if (EqZeroCharacter->IsLocallyControlled() && !EqZeroCharacter->bPressedJump)
        {
            EqZeroCharacter->UnCrouch();
            EqZeroCharacter->Jump();
        }
    }
}

void UEqZeroGameplayAbility_Jump::CharacterJumpStop()
{
    if (AEqZeroCharacter* EqZeroCharacter = GetEqZeroCharacterFromActorInfo())
    {
        if (EqZeroCharacter->IsLocallyControlled() && EqZeroCharacter->bPressedJump)
        {
            EqZeroCharacter->StopJumping();
        }
    }
}

GA蓝图

创建

/Game/Characters/Heroes/Abilities/GA_Hero_Jump.GA_Hero_Jump

只改一个TAG

OnAbilityAdded 的时候,通过注册了一个跳跃按钮到屏幕,这个是移动端的,移动端不管。

由于C++是没有 ActivateAbility的,Lyra是写蓝图里面的

先试试,

技能怎么配置就不贴图了,PawnData里面的技能集合填一填

能跳了,但是之前偷懒跳跃状态机没连

另外比起引擎里面另一个跳跃技能,这里为什么多了两个异步状态呢

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

    if (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo))
    {
        if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
        {
            return;
        }

        ACharacter * Character = CastChecked<ACharacter>(ActorInfo->AvatarActor.Get());
        Character->Jump();
    }
}

开启了一个Jumping状态,这个状态会被EndAbility 打破所有Task而触发。其实只是记了一个带名字的状态

    /**
     * Starts a new ability state.
     *
     * @param StateName The name of the state.
     * @param bEndCurrentState If true, all other active ability states will be ended.
     */
    UFUNCTION(BlueprintCallable, Meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "true", HideSpawnParms = "Instigator"), Category = "Ability|Tasks")
    static UE_API UAbilityTask_StartAbilityState* StartAbilityState(UGameplayAbility* OwningAbility, FName StateName, bool bEndCurrentState = true);

激活的时候绑定一些事件。然后等待输入Release

void UAbilityTask_StartAbilityState::Activate()
{
    if (Ability)
    {
        if (bEndCurrentState && Ability->OnGameplayAbilityStateEnded.IsBound())
        {
            Ability->OnGameplayAbilityStateEnded.Broadcast(NAME_None);
        }

        EndStateHandle = Ability->OnGameplayAbilityStateEnded.AddUObject(this, &UAbilityTask_StartAbilityState::OnEndState);
        InterruptStateHandle = Ability->OnGameplayAbilityCancelled.AddUObject(this, &UAbilityTask_StartAbilityState::OnInterruptState);
    }
}

等待输入Release

void UAbilityTask_WaitInputRelease::Activate()
{
    StartTime = GetWorld()->GetTimeSeconds();

    UAbilitySystemComponent* ASC = AbilitySystemComponent.Get();
    if (ASC && Ability)
    {
        if (bTestInitialState && IsLocallyControlled())
        {
            FGameplayAbilitySpec *Spec = Ability->GetCurrentAbilitySpec();
            if (Spec && !Spec->InputPressed)
            {
                OnReleaseCallback();
                return;
            }
        }

        // 注册一个输入release代理
        DelegateHandle = ASC->AbilityReplicatedEventDelegate(EAbilityGenericReplicatedEvent::InputReleased, GetAbilitySpecHandle(), GetActivationPredictionKey()).AddUObject(this, &UAbilityTask_WaitInputRelease::OnReleaseCallback);
        if (IsForRemoteClient())
        {
            if (!ASC->CallReplicatedEventDelegateIfSet(EAbilityGenericReplicatedEvent::InputReleased, GetAbilitySpecHandle(), GetActivationPredictionKey()))
            {
                SetWaitingOnRemotePlayerData();
            }
        }
    }
}

跳跃状态机

起跳,跳跃循环,到顶Apex,然后下落,落地。最后时期去Idle还是Cycle

除了Land都比较简单

都是这个,Seq Player 换一个动画名字。

除了名字带Loop的状态,需要把Loop去掉

落地比较复杂

这里又是根据距离曲线推算位置

冲刺技能

/ShooterCore/Game/Dash/GA_Hero_Dash.GA_Hero_Dash

配置一个冲刺技能,输入和GA

核对一下配置,这个是GA的TAG和激活时,拥有者会加上什么TAG

还有一个Trigger,就是可以通过send event trigger 这个TAG能激活这个技能。这里可能先不用。

输入配置,配置了InputAction的冲刺,触发了TAG InputTag.Ability.Dash,然后找到Ability然后触发。

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

    // 只在本地控制端计算方向,确保客户端和服务器使用相同的数据
    if (IsLocallyControlled())
    {
        // 计算合法的方向 和 冲刺蒙太奇动画

        // 客户端提交技能
        if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
        {
            CancelAbility(Handle, ActorInfo, ActivationInfo, true);
            return;
        }

        // 如果蹲伏就起来
        if (AEqZeroCharacter* Character = GetEqZeroCharacterFromActorInfo())
        {
            if (Character->IsCrouched())
            {
                Character->UnCrouch();
            }
        }

        // 这里是权威,其实如果我们讨论DS,前面IsLocallyControlled已经把DS的服务器去掉了
        // 所以总是走else
        if (HasAuthority(&ActivationInfo))
        {
            // 单机或 Listen Server:直接执行
            ExecuteDash(Direction, Montage);
        }
        else
        {
            // DS 客户端:本地预测执行 + RPC 通知服务器
            ExecuteDash(Direction, Montage);
            ServerSendInfo(Direction, Montage);
        }
    }
}

服务器的Commit技能,然后 ExecuteDash

void UEqZeroGameplayAbility_Dash::ServerSendInfo_Implementation(const FVector& InDirection, UAnimMontage* InMontage)
{
    Direction = InDirection;
    Montage = InMontage;
    CommitAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo);
    ExecuteDash(Direction, Montage);
}

注意客户端和服务器都来到了 ExecuteDash

这里面是 UAbilityTask_PlayMontageAndWait 播放蒙太奇和 UAbilityTask_ApplyRootMotionConstantForce 进行RootMotion

void UEqZeroGameplayAbility_Dash::ExecuteDash(const FVector& InDirection, UAnimMontage* InMontage)
{
    Direction = InDirection;
    Montage = InMontage;

    UE_LOG(LogTemp, Warning, TEXT("[Dash] ExecuteDash called on %s | Direction: %s"), 
        HasAuthority(&CurrentActivationInfo) ? TEXT("Server") : TEXT("Client"),
        *Direction.ToString());

    // Play Montage (确保 Montage 动画不包含 Root Motion,只包含姿势动画)
    if (InMontage)
    {
        UAbilityTask_PlayMontageAndWait* MontageTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
            this,
            FName("PlayDashMontage"),
            InMontage,
            1.0f,
            NAME_None,
            true
        );

        if (MontageTask)
        {
            MontageTask->OnCompleted.AddDynamic(this, &UEqZeroGameplayAbility_Dash::OnMontageCompleted);
            MontageTask->OnInterrupted.AddDynamic(this, &UEqZeroGameplayAbility_Dash::OnMontageInterrupted);
            MontageTask->OnCancelled.AddDynamic(this, &UEqZeroGameplayAbility_Dash::OnMontageCancelled);
            MontageTask->ReadyForActivation();
        }
    }

    // Apply Root Motion Constant Force
    if (Strength > 0.0f && RootMotionDuration > 0.0f)
    {
        UAbilityTask_ApplyRootMotionConstantForce* RootMotionTask = UAbilityTask_ApplyRootMotionConstantForce::ApplyRootMotionConstantForce(
            this,
            FName("DashRootMotion"),
            InDirection,
            Strength,
            RootMotionDuration,
            true,
            nullptr,
            ERootMotionFinishVelocityMode::ClampVelocity,
            FVector::ZeroVector,
            1000.0f,
            false
        );

        if (RootMotionTask)
        {
            RootMotionTask->OnFinish.AddDynamic(this, &UEqZeroGameplayAbility_Dash::OnRootMotionFinished);
            RootMotionTask->ReadyForActivation();
        }
    }

    if (HasAuthority(&CurrentActivationInfo))
    {
        // TODO gameplay cue    
    }
}

这里cue是一些粒子特效,晚点弄。。。

把CD的GE加出来,配置在GA的cooldown哪里的effect

/Game/GameplayEffects/GE_HeroDash_Cooldown.GE_HeroDash_Cooldown

TODO 缺一个UI的CD

近战

UEqZeroGameplayAbility_Melee

如何释放看代码吧

另外还有一个 AN_Melee SendGameplayEventToActor GameplayEvent.MeleeHit 这个没看到哪里引用了。

可能是为了通过在动画播放到某个状态的时候触发攻击,然后技能里面WaitGameplayEvent

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

    CommitAbility(Handle, ActorInfo, ActivationInfo);

    PlayMeleeMontage();

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

最后会落实到对某个目标的 ASC 是加一个 effect

void UEqZeroGameplayAbility_Melee::ApplyMeleeGameplayEffectAndCue(UAbilitySystemComponent* TargetASC, const FHitResult& HitResult)
{
    auto SourceASC = GetEqZeroAbilitySystemComponentFromActorInfo();
    if (!SourceASC || !TargetASC)
    {
        return;
    }

    FGameplayEffectContextHandle ContextHandle = SourceASC->MakeEffectContext();
    ContextHandle.AddHitResult(HitResult);

    if (DamageEffectClass)
    {
        FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(DamageEffectClass, 1.0f, ContextHandle);
        if (SpecHandle.IsValid())
        {
            SourceASC->ApplyGameplayEffectSpecToTarget(*SpecHandle.Data.Get(), TargetASC);
        }
    }
}

DamageEffectClass是这个

/Game/GameplayEffects/Damage/GE_Damage_Basic_Instant.GE_Damage_Basic_Instant

上一篇
下一篇