LyraLog26 继续修开火

继续修我们的开火。

仍然有很多问题。

/Game/Weapons/B_Weapon.B_Weapon

输入调用到这里的fire函数,这里还有很多蓝图刚好没崩,要仔细搞一下。

回顾1-对于预测的key

void UEqZeroGameplayAbility_RangedWeapon::LogNetModeAndPredictionKey(const FString& FunctionName) const
{
#if 1
    UAbilitySystemComponent* MyAbilityComponent = CurrentActorInfo->AbilitySystemComponent.Get();

    auto NetModeToString = [](ENetMode Mode) -> const TCHAR*
    {
        switch (Mode)
        {
        case NM_Standalone:      return TEXT("NM_Standalone");
        case NM_DedicatedServer: return TEXT("NM_DedicatedServer");
        case NM_ListenServer:    return TEXT("NM_ListenServer");
        case NM_Client:          return TEXT("NM_Client");
        default:                 return TEXT("NM_Unknown");
        }
    };

    UE_LOG(LogTemp, Error, TEXT("[%s] PredictionKey: %s | NetRole: %s | NetMode: %s"),
        *FunctionName,
        *MyAbilityComponent->ScopedPredictionKey.ToString(),
        // *UEnum::GetValueAsString(MyAbilityComponent->GetOwnerRole()), // 拿到PlayerState了,所以客户端变成模拟了。
        *UEnum::GetValueAsString(MyAbilityComponent->GetAvatarActor()->GetLocalRole()),
        NetModeToString(MyAbilityComponent->GetOwner()->GetNetMode()));
#endif
}

DS 单玩家开一枪

LogTemp: Error: [StartTargeting_BeforeScopedPrediction] PredictionKey: [13/0] | NetRole: ROLE_AutonomousProxy | NetMode: NM_Client
LogTemp: Error: [StartTargeting_InsideScopedPrediction] PredictionKey: [13/0] | NetRole: ROLE_AutonomousProxy | NetMode: NM_Client
LogTemp: Error: [OnTargetDataReady_BeforeScopedPrediction] PredictionKey: [13/0] | NetRole: ROLE_AutonomousProxy | NetMode: NM_Client
LogTemp: Error: [OnTargetDataReady_InsideScopedPrediction] PredictionKey: [14/13] | NetRole: ROLE_AutonomousProxy | NetMode: NM_Client
LogTemp: Error: [StartTargeting_AfterOnTargetDataReady_StillInsideScope] PredictionKey: [13/0] | NetRole: ROLE_AutonomousProxy | NetMode: NM_Client

LogTemp: Error: [OnTargetDataReady_BeforeScopedPrediction] PredictionKey: [14/0] | NetRole: ROLE_Authority | NetMode: NM_DedicatedServer
LogTemp: Error: [OnTargetDataReady_InsideScopedPrediction] PredictionKey: [14/0] | NetRole: ROLE_Authority | NetMode: NM_DedicatedServer

[14/13] 通过 CallServerSetReplicatedTargetData 发到了服务器。

所以服务器这段代码的Key是 [14/0],这里0是作用域上一层的key。

一段代码,同时在客户端和服务器跑,那么在客户端和服务器的预测key是一样的。

两个细节

void UEqZeroGameplayAbility_RangedWeapon::StartRangedWeaponTargeting()
{
    // 预测窗口
    // 它告诉系统:接下来的操作(如造成伤害、消耗子弹)是我客户端先"猜"的,请在服务器确认前先暂时这么显示。
    // 如果没有客户端的 CallServerSetReplicatedTargetData 会有问题
    // 注意:不加可能也没问题,因为在Activate里面的,但是这里是蓝图调用的,外部环境不确定。保险
    FScopedPredictionWindow ScopedPrediction(MyAbilityComponent, CurrentActivationInfo.GetActivationPredictionKey());

    // 【1】
    // 射线检测,最后拿到HitResutl包装 TargetData 结构

    // 【2】
    // 本地组件记录一下 TargetData 可以做一些预表现(这里其实简单算一些东西,放进去就好了,UI那边会读取这个容器)

    // 【3】
    // 调用 OnTargetDataReadyCallback (客户端)
}

这里这个窗口但是不改变key是为什么?

因为这是蓝图 activate -> local control -> StartRangedWeaponTargeting 调用的。虽然在激活作用域内。但是这个函数不能默认外界的情况。这样写保险

OnTargetDataReadyCallback 里面是客户端服务器一起跑的逻辑。

在开头 就有 CallServerSetReplicatedTargetData,会带着此时的key去服务器,这时候服务器也开始运行 OnTargetDataReadyCallback (我们在技能active绑定了他)

void UEqZeroGameplayAbility_RangedWeapon::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& InData, FGameplayTag ApplicationTag)
{
    // 没有第二个参数的构造会生成新的key
    FScopedPredictionWindow ScopedPrediction(MyAbilityComponent);

    // 客户端 RPC 服务器,你也跑一下 target data ready callback
    if (CurrentActorInfo->IsLocallyControlled())
    {
        MyAbilityComponent->CallServerSetReplicatedTargetData(...)
    }

    bool bIsTargetDataValid = true;
#if WITH_SERVER_CODE

    // 命中确认,但是Lyra没有做验证,之前认可

    // 业务自己做的回调,这样就有发送前的客户端先行表现。确认命中后的表现。
    WeaponStateComponent->ClientConfirmTargetData(...)

#endif //WITH_SERVER_CODE

    if (bIsTargetDataValid && CommitAbility(...))
    {
        // 触发 伤害,特效
    }
    else
    {
        // 失败了,commit 失败,或者 服务器bIsTargetDataValid 校验后,不准commit 
        K2_EndAbility();
    }

    // 标记数据已被处理,防止重复使用
    MyAbilityComponent->ConsumeClientReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey());
}

可以看这个

if (bIsTargetDataValid && CommitAbility(...))

客户端按默认值直接提交。

如果服务器的 WITH_SERVER_CODE 做了合法性验证,这里false,服务器就不commit。

【服务器要自己调用EndAbility,不存在什么不Commit就会回滚的逻辑XXX 理解错了】

具体写这里面 https://www.cwlgame.cn/2026/03/06/gasyucedehuigun-chugao/

我们要做的是这个 commit 后触发的Cue的蓝图。

另外这里其实并没有要回滚的东西,

继续完成手枪开火效果

前面我们搞到了,开火技能的下过

/Game/Weapons/GA_Weapon_Fire.GA_Weapon_Fire

触发 GameplayCue.Weapon.Pistol.Fire

我们主要修 /EqZeroCore/Weapons/Pistol/GCN_Weapon_Pistol_Fire.GCN_Weapon_Pistol_Fire 这个Cue

【我们把命中的HitResult make了一个cue的参数给 cue的输入】

我们拿到

武器对象,命中的Location,Rotation,物理材质的surface type。

调用武器Actor->Fire。

(其他都是写音频的东西,炸了很多东西)

fire的开头存了一堆变量

包括射线检测的,点,法线,物理材质,拿到Muzzle枪口的socket点,

我们是否应该伪造弹丸数据(目前仅适用于霰弹枪等多弹丸武器)最后一个bool。(会在地上出现子弹壳)

手枪默认走下面。。。

但是子弹壳的逻辑。在几个Array里面加了点东西,这样后面我们做击中点效果的时候就有数据了,我们到霰弹枪再看吧。

开火特效

生成并激活武器射击特效。我们需要单独的 Actor,这样在切换武器时才不会破坏这些特效。包含这些特效的 Actor 会在所有系统崩溃时被销毁。

/Game/Effects/Blueprints/B_WeaponFire.B_WeaponFire

有一个Actor 叫 Weapon Fire

这里有几个变量

Shell Eject System 蛋壳抛出

Muzzle Flash System 枪口闪光

Tracer System 追踪

Shell Eject Mesh 蛋壳抛出的mesh

生成一个Actor挂到武器这个Mesh上,然后把当前的 击中点,击中法线,表面物理材质,复制给

Weapon Fire 这个Actor里面的数组(Weapon Fire 连出线,在搜 Impact Position 变量)

然后调用Weapon Fire的 Fire函数。里面是【蛋壳抛出,枪口闪光烟雾,弹道效果的 niagara】

另外SpawnActor后面为什么这么多参数?

Actor的属性,同时勾上 Expose OnSpawn 和 Instance Editable

===

/Game/Effects/Blueprints/B_WeaponImpacts

spawn一个actor挂到mesh,然后fire里面做效果

不是很懂。。。

贴花

总结一下:

基于Niagara,做了一下弹道,枪口闪光,蛋壳抛出。

击中面的效果

贴花

====

然后是音效。

回到GA_WeaponFire这一级

第一段是激活技能,C++部分射线检测,发送TargetData给服务器确认,commit技能。如果成功了,走后面的回调包括伤害和受击

这个效果包括拿第一个HitResult触发 Cue,就是上面那个 弹道,抛壳,闪光,

Apply GE,里面包括伤害计算,附带了一个受击的Cue

添加

GameplayCue.Weapon.Rifle.Impact

/Game/GameplayCueNotifies/GCN_Weapon_Impact.GCN_Weapon_Impact

完成这个Cue

这里面是纯配置了,音效,例子,摄像机震动

配置什么意思?

开火失败蒙太奇

UE_DEFINE_GAMEPLAY_TAG(TAG_ABILITY_PLAY_MONTAGE_FAILURE_MESSAGE, "Ability.PlayMontageOnActivateFail.Message");

这个TAG是从GAS的技能失败过来的

其实是在我们自己的GA基类写的,技能失GA败统一广播一下失败TAG,但是这个消息又是全局的。

所以要判断一下是不是自己

最后一小段

OK

===

接下来是一些音效的部分

前面我们做了

手枪的音效是这个Cue配置的,在class default

/EqZeroCore/Weapons/Pistol/GCN_Weapon_Pistol_Fire.GCN_Weapon_Pistol_Fire

步枪是调用 Weapon Actor 的 Spawn Sound Attached 来做的,

【需要审核。这是一种解决 AR 未设置为 GCNL 的方法,这种情况会导致射速量化。这使得声音能够连续播放,并且只需接收来自 GCN 的触发信号。】

AR(Audio Radius):指音频组件的触发半径。在高射速武器(如机枪)连续开火时,如果每次开火都生成一个新的音频组件,会导致音频触发频率过高,UE 会对其进行量化(Quantization)处理,从而造成声音卡顿、断续或丢失

它不再每次开火都生成新的音频,而是复用一个已有的 Audio Component。这里可以看到没有就创建一个出来。有就触发Trigger。

其他有一些音频参数,蓝图还有大量缺失。后面继续修了。。。

瞎点到了手雷mesh实现一下吧

/EqZeroCore/Input/Abilities/GA_Grenade.GA_Grenade

UI注册先不看

先配置一个CD

/EqZeroCore/Weapons/Grenade/GE_Grenade_Cooldown.GE_Grenade_Cooldown

对于GE来说

duration 代表这个持续5s

period 代表这个执行周期

例如【duration=5,period=1】代表GE持续5s,每1s执行一次效果。第0秒是否执行有个bool的配置。

这里持续5s,只在最后第 5s 跳一次。(但是这个GE是配给CD的)

这里的 period 有什么意义呢?正常不是duration就行了吗?

大部分CD,确实只需要period。

对于GA的配置,出来相关的TAG,就是这个CD和 replication policy

这里rep policy不太理解,EGameplayAbilityReplicationPolicy

GA的编辑器,的IsDataValid是这样写的

    const bool bIsReplicated = GetReplicationPolicy() == EGameplayAbilityReplicationPolicy::ReplicateYes;
    if (bIsReplicated)
    {
        if (GetInstancingPolicy() != EGameplayAbilityInstancingPolicy::InstancedPerActor)
        {
            Context.AddError(FText::Format(LOCTEXT("ReplicatedInstancePolicyNotSupported", "Instancing Policy '{0}' is not supported for Replication.  Either change the Replication Policy or the Instancing Policy"), UEnum::GetDisplayValueAsText(GetInstancingPolicy())));
            Result = EDataValidationResult::Invalid;
        }
    }

。。。

对于技能

C++写吧,这个线蓝图太抽象了。。。

手雷Actor

创建一个static mesh 挂上去,配置全忽略,

其中有两个物理配置

  1. Linear Damping (线性阻尼)
    作用对象:物体的位移(直线运动)。

效果:数值越大,物体在移动时受到的阻力越大,停下的速度就越快。

手雷场景:如果你觉得手雷扔出去像在冰面上滑行一样飞太远,可以调高这个值,让它在空中由于“空气阻力”更快减速。

  1. Angular Damping (角阻尼)
    作用对象:物体的旋转运动。

效果:数值越大,物体旋转得越“吃力”,且停止旋转的速度越快。

手雷场景:手雷落地后通常会翻滚。如果你希望它着地后迅速停止翻滚,而不是像个陀螺一样转个不停,就需要调高角阻尼。

好多蓝图。。。。

应该是抛物线过去的后续逻辑

输入配置

/EqZeroCore/Game/AbilitySet_ShooterHero.AbilitySet_ShooterHero

技能集 GA=> Input TAG

/EqZeroCore/Input/Actions/InputData_EqZGame_AddOns.InputData_EqZGame_AddOns

输入的 InputAction => Input Tag

每次配这个都很抽象。InputConfig 一路跑到 BindAbilityActions 绑定了 增强输入到 InputTag 的回调。

配置按一下,记得手雷Spawn的Actor和动画蒙太奇配置一下啊。

丢了个空气出来。。。。

跑通手雷都出来的流程

BeginPlay打印一下只有 Server,意识到Actor的 Replicates和 Replicata Movement要勾上。

而且低头丢和角色有碰撞。

检查一下碰撞。Lyra原版手雷确实和角色有碰撞的。

左雷 右 角色胶囊体。

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

    if (!CheckCooldown(Handle, ActorInfo) || !CheckCost(Handle, ActorInfo))
    {
        EndAbility(Handle, ActorInfo, ActivationInfo, true, false);
        return;
    }

    CommitAbility(Handle, ActorInfo, ActivationInfo);

    // 仅 Server 端生成手雷并缓存冷却时间
    if (HasAuthority(&ActivationInfo))
    {
        if (GrenadeClass)
        {
            FActorSpawnParameters SpawnParams;
            SpawnParams.Owner = GetEqZeroCharacterFromActorInfo();
            SpawnParams.Instigator = GetEqZeroCharacterFromActorInfo();
            SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

            SpawnLocation = GetSpawnLocation();
            SpawnRotation = GetSpawnRotation();

            UE_LOG(LogTemp, Log, TEXT("Spawning grenade at location: %s, rotation: %s"), *SpawnLocation.ToString(), *SpawnRotation.ToString());

            GetWorld()->SpawnActor<AActor>(
                GrenadeClass,
                SpawnLocation,
                SpawnRotation,
                SpawnParams);
        }

        CooldownTime = GetCooldownTimeRemaining();
        // UE_LOG(LogTemp, Log, TEXT("Cooldown time for grenade: %f seconds"), CooldownTime);
    }

    // 播放投掷动画,完成后广播冷却时间并结束技能
    if (GrenadeMontage)
    {
        UAbilityTask_PlayMontageAndWait* MontageTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
            this, NAME_None, GrenadeMontage);
        MontageTask->OnCompleted.AddDynamic(this, &UZeroAbility_Grenade::OnMontageCompleted);
        MontageTask->ReadyForActivation();
    }
    else
    {
        OnMontageCompleted(); // 广播CD给UI
    }
}

技能激活,spawn 一个手雷,手雷Actor有一个 ProjectileMovement 做投掷

还缺一些效果。注意Actor要去勾一下同步,不然客户端看不到。

void AGrenade::Detonate_Implementation()
{
    if (bExploded)
    {
        return;
    }

    bExploded = true;
    GetWorldTimerManager().ClearTimer(Timer);
    HandleDetonationPresentation(); // 清理工作,mesh隐藏,特效关闭,ProjectileMovement停止

    // 后面是权威的逻辑了
    if (!HasAuthority())
    {
        return;
    }

    UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetInstigator());
    if (!SourceASC || !GrenadeDamageEffectClass)
    {
        Destroy();
        return;
    }

    TArray<AActor*> PawnsInRadius;
    if (!CollectPawnsInRadius(PawnsInRadius)) // 做一个圆检测,找敌人的Pawn
    {
        Destroy();
        return;
    }

    ApplyDamageToPawns(SourceASC, PawnsInRadius); // 对所有pawn做伤害
    Destroy();
}

更多的特效还给整理一下。TODO

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇