LyraLog 19 换弹开镜完善

换弹技能

/EqZeroCore/Weapons/Pistol/AbilitySet_ShooterPistol.AbilitySet_ShooterPistol

这个技能集里面配置 换弹技能

/Game/Weapons/Pistol/GA_Weapon_Reload_Pistol.GA_Weapon_Reload_Pistol 这个主要配置一下换弹蒙太奇,基于武器有所不同

/Game/Weapons/GA_Weapon_ReloadMagazine.GA_Weapon_ReloadMagazine 换弹的核心逻辑

再父类就是 C++ Ability From Equipment

这个类核心是方便拿到 技能所对应的武器对象

完成这个激活

播放换弹蒙太奇,然后wait event,注意这个event在动画蒙太奇里面用 anim notify 触发的

第一块逻辑,应该是有剩余子弹的时候,不阻塞开火。

开火打断换弹?具体逻辑在哪

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

    if (auto InventoryItemInstance = GetAssociatedItem())
    {
        DidBlockFiring = InventoryItemInstance->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_MagazineAmmo) == 0;

        if (DidBlockFiring)
        {
            if (UAbilitySystemComponent* ASC = ActorInfo->AbilitySystemComponent.Get())
            {
                ASC->AddLooseGameplayTag(TAG_WeaponFireBlocked);
            }
        }
    }

    // 蒙太奇任务:播放换弹动画
    UAbilityTask_PlayMontageAndWait* MontageTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
        this,
        FName("ReloadMontage"),
        ReloadMontage,
        PlayRate,
        NAME_None,
        false
    );

    if (MontageTask)
    {
        // 动画在客户端播放完成时,不要结束服务器上的技能,让其自行结束
        MontageTask->OnCompleted.AddDynamic(this, &ThisClass::OnMontageCompleted);
        MontageTask->OnInterrupted.AddDynamic(this, &ThisClass::OnMontageInterrupted);
        MontageTask->OnCancelled.AddDynamic(this, &ThisClass::OnMontageCancelled);
        MontageTask->ReadyForActivation();
    }

    // 事件任务:等待动画 Notify 发出 GameplayEvent.ReloadDone
    UAbilityTask_WaitGameplayEvent* EventTask = UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(
        this,
        TAG_GameplayEvent_ReloadDone,
        nullptr,
        true,
        true
    );

    if (EventTask)
    {
        EventTask->EventReceived.AddDynamic(this, &ThisClass::OnReloadEventReceived);
        EventTask->ReadyForActivation();
    }
}

到这里,能换弹,具体调一下,子弹更新是没问题的。

void UEqZeroGameplayAbility_ReloadMagazine::OnReloadEventReceived(FGameplayEventData Payload)
{
    // 仅在服务器端执行实际换弹逻辑
    if (HasAuthority(&CurrentActivationInfo))
    {
        ReloadAmmoIntoMagazine(Payload);
    }

    EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}

void UEqZeroGameplayAbility_ReloadMagazine::ReloadAmmoIntoMagazine(FGameplayEventData Payload)
{
    UEqZeroInventoryItemInstance* Item = GetAssociatedItem();
    if (!Item)
    {
        return;
    }

    const int32 MagSize   = Item->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_MagazineSize); // 弹匣容量
    const int32 MagAmmo   = Item->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_MagazineAmmo); // 当前弹匣中的子弹数量
    const int32 SpareAmmo = Item->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_SpareAmmo); // 当前备用弹药数量

    // 计算本次需要装填的子弹数量,不超过备用弹药
    const int32 Needed    = FMath::Min(MagSize - MagAmmo, SpareAmmo);

    if (Needed > 0)
    {
        Item->AddStatTagStack(EqZeroGameplayTags::Lyra_Weapon_MagazineAmmo, Needed);
        Item->RemoveStatTagStack(EqZeroGameplayTags::Lyra_Weapon_SpareAmmo, Needed);
        DebugLog();
    }
}

然后没子弹也能换。

bool UEqZeroGameplayAbility_ReloadMagazine::CanActivateAbility(
    const FGameplayAbilitySpecHandle Handle,
    const FGameplayAbilityActorInfo* ActorInfo,
    const FGameplayTagContainer* SourceTags,
    const FGameplayTagContainer* TargetTags,
    OUT FGameplayTagContainer* OptionalRelevantTags) const
{
    if (!Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags))
    {
        return false;
    }

    const UEqZeroInventoryItemInstance* Item = GetAssociatedItem();
    if (!Item)
    {
        return false;
    }

    const int32 MagAmmo  = Item->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_MagazineAmmo);
    const int32 MagSize  = Item->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_MagazineSize);

    // 弹匣已满,无需装填
    if (MagAmmo >= MagSize)
    {
        if (OptionalRelevantTags)
        {
            OptionalRelevantTags->AddTag(TAG_ActivateFail_MagazineFull);
        }
        return false;
    }

    const int32 SpareAmmo = Item->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_SpareAmmo);

    // 备用弹药为空,无法装填
    if (SpareAmmo <= 0)
    {
        if (OptionalRelevantTags)
        {
            OptionalRelevantTags->AddTag(TAG_ActivateFail_NoSpareAmmo);
        }
        return false;
    }

    return true;
}

场景有枪能捡起来能切换

发现这下要把第二把武器配出来了

动画蓝图配置

/Game/Characters/Heroes/Mannequin/Animations/Locomotion/Rifle/ABP_RifleAnimLayers.ABP_RifleAnimLayers

/Game/Characters/Heroes/Mannequin/Animations/Locomotion/Rifle/ABP_RifleAnimLayers_Feminine.ABP_RifleAnimLayers_Feminine

步枪 武器实例

/EqZeroCore/Weapons/Rifle/B_WeaponInstance_Rifle.B_WeaponInstance_Rifle

步枪Actor蓝图(提供mesh)

/EqZeroCore/Weapons/Rifle/B_Rifle.B_Rifle

装备定义

/EqZeroCore/Weapons/Rifle/WID_Rifle.WID_Rifle

物品定义

/EqZeroCore/Weapons/Rifle/ID_Rifle.ID_Rifle

配置枪的定义

/EqZeroCore/Weapons/Rifle/WeaponPickupData_Rifle.WeaponPickupData_Rifle

=》

然后在场景创建这个Actor 配置好

服务器碰撞检测

void AEqZeroWeaponSpawner::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
    APawn* OverlappingPawn = Cast<APawn>(OtherActor);
    if (GetLocalRole() == ROLE_Authority && bIsWeaponAvailable && OverlappingPawn != nullptr)
    {
        AttemptPickUpWeapon(OverlappingPawn);
    }
}

尝试把武器给角色

void AEqZeroWeaponSpawner::AttemptPickUpWeapon_Implementation(APawn* Pawn)
{
    if (GetLocalRole() == ROLE_Authority && bIsWeaponAvailable && UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Pawn))
    {
        TSubclassOf<UEqZeroInventoryItemDefinition> WeaponItemDefinition = WeaponDefinition ? WeaponDefinition->InventoryItemDefinition : nullptr;
        if (WeaponItemDefinition != nullptr)
        {
            // 尝试将武器给予 Pawn
            if (GiveWeapon(WeaponItemDefinition, Pawn))
            {
                // 武器成功拾取:隐藏武器、播放特效、开始冷却
                bIsWeaponAvailable = false;
                SetWeaponPickupVisibility(false);
                PlayPickupEffects();
                StartCoolDown();
            }
        }
    }
}

给武器,设置暂时不可用,隐藏mesh,特效。开后开启CD

注意 bIsWeaponAvailable 变量在服务器修改回触发同步。

void AEqZeroWeaponSpawner::OnRep_WeaponAvailability()
{
    if (bIsWeaponAvailable)
    {
        // 武器重新可用:播放重生特效并显示网格体
        PlayRespawnEffects();
        SetWeaponPickupVisibility(true);
    }
    else
    {
        // 武器已被拾取:隐藏网格体、开始冷却并播放拾取特效
        SetWeaponPickupVisibility(false);
        StartCoolDown();
        PlayPickupEffects();
    }
}

give weapon的逻辑,有两种,已经有武器就加子弹,否则就是加上这边枪

bool AEqZeroWeaponSpawner::GiveWeapon_Implementation(TSubclassOf<UEqZeroInventoryItemDefinition> WeaponItemClass, APawn* ReceivingPawn)
{
    auto Controller = ReceivingPawn->GetController();
    if (!Controller)
    {
        return false;
    }

    auto QuickBarComponent = Controller->FindComponentByClass<UEqZeroQuickBarComponent>();
    auto InventoryManager = Controller->FindComponentByClass<UEqZeroInventoryManagerComponent>();
    if (!QuickBarComponent || !InventoryManager)
    {
        return false;
    }

    auto ExistingWeaponInstance = InventoryManager->FindFirstItemStackByDefinition(WeaponItemClass);
    if (ExistingWeaponInstance && bGiveAmmoForDuplicatedWeapon)
    {
        // 计算默认最大备用弹药量与当前剩余量的差值,补充到上限
        const int32 DefaultSpareAmmo = GetDefaultStatFromItemDef(WeaponItemClass, EqZeroGameplayTags::Lyra_Weapon_SpareAmmo);
        const int32 CurrentSpareAmmo = ExistingWeaponInstance->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_SpareAmmo);
        const int32 AmmoToAdd = DefaultSpareAmmo - CurrentSpareAmmo;
        if (AmmoToAdd > 0)
        {
            ExistingWeaponInstance->AddStatTagStack(EqZeroGameplayTags::Lyra_Weapon_SpareAmmo, AmmoToAdd);
        }
        return true;
    }

    auto NextSlot = QuickBarComponent->GetNextFreeItemSlot();
    if (NextSlot >= 0)
    {
        // 还有空位,直接添加新武器实例

        // TODO gameplay cue

        if (auto Ins = InventoryManager->AddItemDefinition(WeaponItemClass, 1))
        {
            QuickBarComponent->AddItemToSlot(NextSlot, Ins);

            // 源项目有一个捡起是否直接切换的逻辑,不打算做

            return true;
        }

    }

    return false;
}

然后是CD

项目配置的是4s后刷新武器

void AEqZeroWeaponSpawner::StartCoolDown()
{
    if (UWorld* World = GetWorld())
    {
        World->GetTimerManager().SetTimer(CoolDownTimerHandle, this, &AEqZeroWeaponSpawner::OnCoolDownTimerComplete, CoolDownTime);
    }
}

事件到了,显示武器,相关的效果。这里延迟了一下,让 bIsWeaponAvailable 客户端把武器重新显示出来后,才CheckForExistingOverlaps

void AEqZeroWeaponSpawner::ResetCoolDown()
{
    UWorld* World = GetWorld();

    if (World)
    {
        World->GetTimerManager().ClearTimer(CoolDownTimerHandle);
    }

    if (GetLocalRole() == ROLE_Authority)
    {
        bIsWeaponAvailable = true;
        PlayRespawnEffects();
        SetWeaponPickupVisibility(true);

        // 延迟后检查重叠,给客户端 OnRep 足够时间先行触发
        if (World)
        {
            World->GetTimerManager().SetTimer(CheckOverlapsDelayTimerHandle, this, &AEqZeroWeaponSpawner::CheckForExistingOverlaps, CheckExistingOverlapDelay);
        }
    }

    CoolDownPercentage = 0.0f;
}

按键切换武器

这线真的是怎么连都不好看。。。

这个带TAG的Event会发到哪里呢?

/EqZeroCore/Game/GA_QuickbarSlots.GA_QuickbarSlots

这是一个OnSpawn就加到角色上的 Wait Gameplay Event,他会等待对应的Event

到现在我们有多少事件了

MessageRouter

WaitGameplayEvent

技能的阻塞

/EqZeroCore/Game/TagRelationships_ShooterHero.TagRelationships_ShooterHero

这个配置嵌入到GAS的流程

ApplyAbilityBlockAndCancelTags

GetAdditionalActivationTagRequirements

开镜技能完善

UEqZeroGameplayAbility_ADS 核心C++代码

/EqZeroCore/Input/Abilities/GA_ADS.GA_ADS 【配置】

检查所有配置项是否都已经有了

【添加CameraMode】

/EqZeroCore/Camera/CM_ThirdPersonADS.CM_ThirdPersonADS

/EqZeroCore/Camera/ThirdPersonADSOffsetCurve

【IMG 】

看起来是通过这个缩放改输入的速度,不确定

/EqZeroCore/Input/Mappings/IMC_ADS_Speed.IMC_ADS_Speed

这个配置和 settings shared 里面的东西有关,所以现在可能起不了作用。

最后完成配置

emmmmm

又炸了,视角明显不对。Y轴反转,

先去掉IMG看看,不反转了,其他的晚点看吧。

位置不对比较关键

void AEqZeroPlayerCameraManager::DisplayDebug(UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplay, float& YL, float& YPos)
{
    check(Canvas);

    FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager;

    DisplayDebugManager.SetFont(GEngine->GetSmallFont());
    DisplayDebugManager.SetDrawColor(FColor::Yellow);
    DisplayDebugManager.DrawString(FString::Printf(TEXT("EqZeroPlayerCameraManager: %s"), *GetNameSafe(this)));

    Super::DisplayDebug(Canvas, DebugDisplay, YL, YPos);

    const APawn* Pawn = (PCOwner ? PCOwner->GetPawn() : nullptr);

    if (const UEqZeroCameraComponent* CameraComponent = UEqZeroCameraComponent::FindCameraComponent(Pawn))
    {
        CameraComponent->DrawDebug(Canvas);
    }
}

曲线配置错了。改为OK了

上一篇
下一篇