LyraLog22 开火问题测试修正1

子弹扩散好像有问题

EqZero.Weapon.DrawBulletTraceDuration 5.0 — 画出子弹飞行轨迹(红线),持续 5 秒

EqZero.Weapon.DrawBulletHitDuration 5.0 — 画出命中点(红点),持续 5 秒

EqZero.Weapon.DrawBulletHitRadius 10.0 — 调整命中点的显示半径(默认 3.0 uu,可以调大方便观察)

子弹太分散了,散射逻辑好像有问题


#if WITH_EDITOR
void UEqZeroRangedWeaponInstance::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent)
{
    Super::PostEditChangeProperty(PropertyChangedEvent);
    UpdateDebugVisualization();
}

void UEqZeroRangedWeaponInstance::UpdateDebugVisualization()
{
    ComputeHeatRange(Debug_MinHeat, Debug_MaxHeat);
    ComputeSpreadRange(Debug_MinSpreadAngle, Debug_MaxSpreadAngle);
    Debug_CurrentHeat = CurrentHeat;
    Debug_CurrentSpreadAngle = CurrentSpreadAngle;
    Debug_CurrentSpreadAngleMultiplier = CurrentSpreadAngleMultiplier;
}
#endif

这个DEBUG 不知道怎么开,还是打LOG把

发现UEqZeroRangedWeaponInstance的tick没跑,热度没下降

bool UEqZeroRangedWeaponInstance::UpdateSpread(float DeltaSeconds)
{
    const float TimeSinceFired = GetWorld()->TimeSince(LastFireTime);

    // 开火后,超过这个时间,热度下降
    if (TimeSinceFired > SpreadRecoveryCooldownDelay)
    {
        // 根据当前热量决定冷却速度(例如:热量越高,冷却越快)。
        // 但是 HeatToCoolDownPerSecondCurve 曲线目前只有一个 (0, 10)的点
        const float CooldownRate = HeatToCoolDownPerSecondCurve.GetRichCurveConst()->Eval(CurrentHeat);

        // 减少热量,然后修改当前扩散角度
        CurrentHeat = ClampHeat(CurrentHeat - (CooldownRate * DeltaSeconds));
        CurrentSpreadAngle = HeatToSpreadCurve.GetRichCurveConst()->Eval(CurrentHeat);
    }

    float MinSpread;
    float MaxSpread;
    ComputeSpreadRange(MinSpread, MaxSpread);

    // 是否已经恢复到了最小扩散状态(用于判断首发精准度), 最小扩散点是曲线的中点,上面几行
    return FMath::IsNearlyEqual(CurrentSpreadAngle, MinSpread, KINDA_SMALL_NUMBER);
}

发现tick是这里过来的

void UEqZeroWeaponStateComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    /*
     * 调用角色身上所有远程武器实例的 Tick 方法,确保它们能持续更新状态(例如热量、扩散等),以便在 UI 上做出相应反馈。
     */
    if (APawn* Pawn = GetPawn<APawn>())
    {
        if (UEqZeroEquipmentManagerComponent* EquipmentManager = Pawn->FindComponentByClass<UEqZeroEquipmentManagerComponent>())
        {
            if (UEqZeroRangedWeaponInstance* CurrentWeapon = Cast<UEqZeroRangedWeaponInstance>(EquipmentManager->GetFirstInstanceOfType(UEqZeroRangedWeaponInstance::StaticClass())))
            {
                CurrentWeapon->Tick(DeltaTime);
            }
        }
    }
}

UEqZeroWeaponStateComponent 这个通过 GameFeature 配置在 Controller的

组件压根没配呀

准星开火的时候效果相当不对

BUG1:

调试发现 CurrentSpreadAngle 在开火后的数值过小

问题是 HeatToSpreadCurve 配置错误,这个点必须按下回车数值才能确认修改,点空白处数值就回去了,没改上

BUG2:

对于原项目的蓝图,Reticle Widget Base

Set Radius 影响十字的扩散(这个taget是控件)后面的 Set Width/Height Override 影响那个开枪后正方形的框的缩放

这部分调试清楚

BUG3:

红叉的命中动画,这是一个普通的图片 EliminationMarker

但是这个控件是隐藏的,动画也还没播放。

发现是 SEqZeroHitMarkerConfirmationWidget 这个的问题,每次射击他一定把所有东西都画出来。

队伍系统没有

bool UEqZeroWeaponStateComponent::ShouldShowHitAsSuccess(const FHitResult& Hit) const
{
    AActor* HitActor = Hit.GetActor();

    // 暂时没有队伍系统,默认所有人都是敌人

    return true;
}

但是为了射线检测 UEqZeroGameplayAbility_RangedWeapon::TraceBulletsInCartridge 的最后,有默认有一个命中,所以达到啥都有效果。

// 确保 OutHits 中始终有一个条目,这样方向就可以用于追踪器等。
if (OutHits.Num() == 0)
{
    if (!Impact.bBlockingHit)
    {
        // 在轨迹末尾找到伪造的 “影响”
        Impact.Location = EndTrace;
        Impact.ImpactPoint = EndTrace;
    }

    OutHits.Add(Impact);
}

所以这个地方先改成这样

bool UEqZeroWeaponStateComponent::ShouldShowHitAsSuccess(const FHitResult& Hit) const
{
    AActor* HitActor = Hit.GetActor();
    if (HitActor == nullptr)
    {
        return false;
    }

    // 只有打到 Pawn(角色)才算成功命中
    // 后续接入队伍系统后可进一步判断敌我
    return HitActor->IsA<APawn>();
}

爆头伤害生效了吗

Gameplay.Zone.WeakSpot 查查引用

default engine ini 补上,主要是这几个 SurfaceType

[/Script/Engine.PhysicsSettings]
+PhysicalSurfaces=(Type=SurfaceType1,Name="Character")
+PhysicalSurfaces=(Type=SurfaceType2,Name="Concrete")
+PhysicalSurfaces=(Type=SurfaceType3,Name="Glass")

创建 PhysicalMaterialWithTags

/Game/Characters/PhysMat_Player.PhysMat_Player

/Game/Characters/PhysMat_Player_WeakSpot.PhysMat_Player_WeakSpot 这个配置了弱点TAG

对这个

/Game/Characters/Heroes/Mannequin/Rig/PA_Mannequin.PA_Mannequin

骨骼树的

Simple Collision Physical Material

Phys Material Override

这两个关联好创建的 PhysicalMaterialWithTags

OK 伤害明显生效了

发现没有受击动画呀

估计藏在某个Cue里面

GameplayCue.Character.DamageTaken

这是直接在伤害GE里面配置的 Cue的 TAG

好像只是音效和跳字

===

傻了,应该直接搜索受击蒙太奇的引用的

找到开火技能有一个 "Slelect Hit Montage" 没有引用。。。

===

删除 GameplayCue.Character.DamageTaken 这个TAG会影响受击,

所以还是这个Cue的问题

在 Event On Burst 。。。

相关资产

/EqZeroCore/Weapons/GE_Damage_Melee.GE_Damage_Melee

/Game/GameplayEffects/Damage/GameplayEffectParent_Damage_Basic.GameplayEffectParent_Damage_Basic

在Cue中

这样有一个BUG,

开火的蒙太奇,自己看不到 X,对面看得到

近战的受击,双端都能看得到。

GE都是权威发起的,配置也都对过了?

咋整?

emmmmmmmm

AI说要把这个权威干掉

问题原因

  1. 客户端调用 StartRangedWeaponTargeting(),创建了带 客户端预测 Key 的 FScopedPredictionWindow
  2. CallServerSetReplicatedTargetData 把预测 Key 发给服务器
  3. 服务器在 ServerSetReplicatedTargetData 回调中,恢复了这个 客户端的预测 Key 到 ASC 的 ScopedPredictionKey
  4. 蓝图中 HasAuthority → True → 只有服务器调用 ApplyGameplayEffectToTarget
  5. 服务器执行 GE → 触发 Cue → NetMulticast 携带了 客户端预测 Key
  6. 在 owning client(开枪的那个客户端)上:
    • PredictionKey.IsLocalClientKey() = true(这是它自己生成的 Key)
    • 条件变成 false || false = false → Cue 被抑制!
    • 引擎认为:"这个 Key 是你自己客户端生成的,你应该已经在本地预测执行过了"
  7. 但因为蓝图里有 HasAuthority 检查,客户端从未本地预测执行过这个 GE/Cue
  8. 结果:开枪的客户端看不到 Cue,另一个客户端看得到

虽然这个修改可以,但是和Lyra的写法不一致,而且Lyra是没问题的

客户端拿到了一个Key发给服务器,自己也继续走

服务器执行 GE,触发 Cue

void FActiveGameplayEffectsContainer::ExecuteActiveEffectsFrom(FGameplayEffectSpec &Spec, FPredictionKey PredictionKey)
{
    // 

    if (InvokeGameplayCueExecute && SpecToUse.Def->GameplayCues.Num())
    {
        // TODO: check replication policy. Right now we will replicate every execute via a multicast RPC

        UE_LOG(LogGameplayEffects, Log, TEXT("Invoking Execute GameplayCue for %s"), *SpecToUse.ToSimpleString());

        UAbilitySystemGlobals::Get().GetGameplayCueManager()->InvokeGameplayCueExecuted_FromSpec(Owner, SpecToUse, PredictionKey);
    }
}

服务器发 NetMulticast,携带客户端的预测 Key

if (bHasAuthority)  // 服务器 = true
{
    RepInterface->Call_InvokeGameplayCueExecuted_FromSpec(
        PendingCue.FromSpec, 
        PendingCue.PredictionKey);  // ← 客户端的预测 Key 被塞进 Multicast RPC
}

Call_InvokeGameplayCueExecuted_FromSpec 是一个 UFUNCTION(NetMulticast),发给所有端。

(核心):owning client 收到 Multicast,判断是否执行

void UAbilitySystemComponent::NetMulticast_InvokeGameplayCueExecuted_FromSpec_Implementation(
    const FGameplayEffectSpecForRPC Spec, FPredictionKey PredictionKey)
{
    if (IsOwnerActorAuthoritative() || PredictionKey.IsLocalClientKey() == false)
    //  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //  客户端上 = false                  关键判断!
    {
        InvokeGameplayCueEvent(Spec, EGameplayCueEvent::Executed);
    }
}

对于开枪的客户端(owning client):

PredictionKey.IsLocalClientKey() 是 true。不满足第二个条件,所以Cue被跳过了

为什么?

bool IsLocalClientKey() const
{
    return Current > 0 && !bIsServerInitiated;
}

GameplayPrediction.cpp:127(NetSerialize):

// 服务器发出时:
ValidKeyForConnection = (Current > 0) && 
    (bIsServerInitiated || 
     PredictiveConnectionObjectKey == FObjectKey() || 
     PredictiveConnectionObjectKey == FObjectKey(Map));
//   ↑ 这个 Key 的 PredictiveConnectionObjectKey 就是开枪客户端的连接
//   ↑ 对于开枪客户端的连接:Map 匹配 → ValidKeyForConnection = true → 序列化 Current 值
//   ↑ 对于其他客户端的连接:Map 不匹配 → ValidKeyForConnection = false → Current 为 0

所以:

  • 开枪客户端收到 Multicast 时:Current > 0,bIsServerInitiated = false → IsLocalClientKey() = true → Cue 被跳过
  • 其他客户端收到 Multicast 时:Current = 0(Key 无效,不是它生成的)→ IsLocalClientKey() = false → Cue 正常执行

设计意图:

"如果这个 Multicast 携带的 PredictionKey 是我本地生成的,说明我之前应该已经在 PredictivelyExecuteEffectSpec 中本地预测执行过这个 Cue 了,收到服务器的确认时应该跳过,避免重复播放。"

但因为你蓝图加了 HasAuthority,客户端从未执行过 ApplyGameplayEffectToTarget,自然也从未预测执行过 Cue。服务器的确认到了,客户端还是跳过了 → 结果就是开枪的人自己看不到受击效果。

【所以lyra还有其他流程能播放受击蒙太奇动画???】

===

但是Melee为什么没有被抑制???

Melee(C++ 直接调用)

// EqZeroGameplayAbility_Melee.cpp
SourceASC->ApplyGameplayEffectSpecToTarget(*SpecHandle.Data.Get(), TargetASC);
//                                         没有第三个参数 ↑

ApplyGameplayEffectSpecToTarget 的签名:

virtual FActiveGameplayEffectHandle ApplyGameplayEffectSpecToTarget(
    const FGameplayEffectSpec& GameplayEffect,
    UAbilitySystemComponent* Target,
    FPredictionKey PredictionKey = FPredictionKey());  // ← 默认空 Key

Melee 没传 PredictionKey → 默认空 Key → Cue Multicast 携带空 Key → IsLocalClientKey() = false (Current=0) → 所有客户端都执行 Cue ✓

Ranged(蓝图 ApplyGameplayEffect 节点)

蓝图节点 K2_ApplyGameplayEffectSpecToTarget 内部调用:

Data->ApplyGameplayEffectSpec(
    *SpecHandle.Data.Get(),
    ActorInfo->AbilitySystemComponent->GetPredictionKeyForNewAction());
//                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                                     返回当前 FScopedPredictionWindow 的 PredictionKey

蓝图节点自动传了客户端的 PredictionKey → Cue Multicast 携带该 Key → Owning Client 上 IsLocalClientKey() = true → Cue 被抑制 ✗

这下合理了。

===

/Game/Effects/Camera/Damage/NCLE_DamageTaken.NCLE_DamageTaken

这个是角色屏幕上的类似受击UI的一个特效

FEqZeroVerbMessage 相关流程

AI:GAS双端的职责划分

属性修改 → 服务器权威;表现反馈 → 双端都跑(预测系统自动去重)。

操作 该在哪里执行 原因
ActivateAbility 双端 客户端预测激活,服务器权威确认
CommitAbility(消耗/CD) 双端 客户端预测扣费,服务器确认
ApplyGameplayEffectToSelf(对自己) 双端 客户端预测属性变化,服务器同步修正
ApplyGameplayEffectToTarget(对别人) 看情况 ⚠️ 见下文详解
GameplayCue(表现/特效) 双端 引擎通过 PredictionKey 自动去重
Montage / 动画 双端 客户端即时播放,服务器复制给其他人
射线检测 / 瞄准 客户端 本地精度最高,结果通过 TargetData 发给服务器
伤害结算 服务器 属性是服务器权威的

模式 A:无预测(Lyra 的做法)

客户端: 射线检测 → TargetData → 发给服务器

服务器: HasAuthority ✓ → ApplyGE → 属性修改(复制回客户端)

受击反馈: 通过属性变化回调/消息系统触发(不用 GE 上的 Cue)

模式 B:有预测(GE 上配了 Cue)

客户端: 射线检测 → TargetData → 同时:

    ├─ 本地 ApplyGE(预测执行 → Cue 立即播放)

    └─ 发 TargetData 给服务器

服务器: ApplyGE(权威执行 → Multicast Cue → 其他客户端播放)

    开枪客户端收到 Multicast → 识别到预测 Key → 跳过(不重复)

适用场景:GE 上配了 GameplayCue,需要开枪者即时看到受击效果

你的项目就是模式 B,但蓝图加了 HasAuthority 阻止了客户端本地预测 → 破坏了链路。

PS【这里其实是我项目的BUG,可能还要查一查,Lyra用的也是B方案】

预测系统的核心逻辑(三句话版)

  1. 客户端先做,服务器后确认 — 客户端在 ScopedPredictionWindow 内执行的操作会产生一个预测 Key
  2. 服务器做同样的事 — 服务器执行后通过 Multicast 广播结果,携带同一个预测 Key
  3. 客户端自动去重 — 收到 Multicast 后,如果 Key 是自己生成的(IsLocalClientKey() == true),跳过(因为已经本地预测过了)

当前项目的两种伤害方式

近战

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();
    }
}

LocalPredict 动作客户端先跑了,然后在服务器做射线检测,触发GE,然后GE里面有伤害和Cue

Client: ActivateAbility → CommitAbility(预测消耗) → PlayMeleeMontage(播动画)

Server: ActivateAbility → CommitAbility(确认消耗) → PlayMeleeMontage → MeleeCapsuleTrace → ApplyGE

射击

Local Control 触发 StartRangedWeaponTargeting 客户端射线检测

然后两条路

  • CallServerSetReplicatedTargetData 带着TargetData去服务器了
  • 继续向下走

都会跑的流程

CommitAbility

OnRangedWeaponTargetDataReady 这里是蓝图的逻辑

模拟端直接去走 Cue 的逻辑了

    if (bTreatAsInfiniteDuration)
    {
        // This is an instant application but we are treating it as an infinite duration for prediction. We should still predict the execute GameplayCUE.
        // (in non predictive case, this will happen inside ::ExecuteGameplayEffect)

        if (!bSuppressGameplayCues)
        {
            UAbilitySystemGlobals::Get().GetGameplayCueManager()->InvokeGameplayCueExecuted_FromSpec(this, *OurCopyOfSpec, PredictionKey);
        }
    }

这里

    else if (Spec.Def->DurationPolicy == EGameplayEffectDurationType::Instant)
    {
        // This is a non-predicted instant effect (it never gets added to ActiveGameplayEffects)
        ExecuteGameplayEffect(*OurCopyOfSpec, PredictionKey);
    }

这里会去 GE的伤害 求值器

总结:

Client: ActivateAbility → 蓝图调用 StartRangedWeaponTargeting

     → PerformLocalTargeting(客户端射线检测)

     → OnTargetDataReadyCallback

         → CallServerSetReplicatedTargetData(发给服务器)

         → CommitAbility + OnRangedWeaponTargetDataReady(本地预测GE)

Server: ActivateAbility → 收到 TargetData 触发 AbilityTargetDataSetDelegate

     → OnTargetDataReadyCallback

         → 命中确认(WeaponStateComponent::ClientConfirmTargetData)

         → CommitAbility + OnRangedWeaponTargetDataReady(权威GE)
上一篇
下一篇