LyraLog18 开火准星血条装备栏

上一回我们讲到我们基础的都已经搭好了,现在继续完成我们的开火蓝图

/Game/Weapons/GA_Weapon_Fire.GA_Weapon_Fire

就是这个蓝图

手枪轻微抖动,调试技能确实被触发了,射线能够发射

这个蓝图可真抽象呢???

射击失败的事件监听的蓝图逻辑 GA_Weapon_Fire

然后就是Cue好多。。。

经过测试,大概是这样

GameplayCue.Weapon.Pistol.Fire 是开火的弹道,击中特效,声音

GameplayCue.Weapon.Pistol.Impact 是击中受击蒙太奇,跳字

/EqZeroCore/Weapons/Pistol/GCN_Weapon_Pistol_Fire.GCN_Weapon_Pistol_Fire

先是这个Cue,里面是声音和摄像机震动。还有一个

这样就有声音了,另外Weapon的Fire接口没崩,所以弹道是有的,但是明显方向不对。

问题:

无限子弹,HP UI没有,扣血全靠打印,准星没有瞄准抽象。

准星UI

/EqZeroCore/UserInterface/W_EqZHUDLayout.W_EqZHUDLayout

这里把中心的 HUD.Slot.Reticle 的 TAG挂接点加上

完成 /EqZeroCore/UserInterface/HUD/W_WeaponReticleHost.W_WeaponReticleHost

这个一个SizeBox 套 Overlay 全空

完成 OnWeaponChanged 事件,是蓝图,有点长了,后面写到代码里面。

【主要逻辑是从物品片段上读取准星UI配置。然后加到屏幕上】

物品的瞄准片段 /EqZeroCore/Weapons/Pistol/ID_Pistol.ID_Pistol 这个

配置一下UI

但是在这之前还有写几个类

UCircumferenceMarkerWidget


UCLASS()
class UEqZeroCircumferenceMarkerWidget : public UWidget
{
    GENERATED_BODY()

public:
    UEqZeroCircumferenceMarkerWidget(const FObjectInitializer& ObjectInitializer);

    /*
     * UWidget interface
     */
public:
    virtual void SynchronizeProperties() override;
protected:
    virtual TSharedRef<SWidget> RebuildWidget() override;

    /*
     * UVisual interface
     */
public:
    virtual void ReleaseSlateResources(bool bReleaseChildren) override;

    // ==========================================================================

public:
    // 绘制标记的位置 / 方向列表
    // (0, 0), (90, 90), (-90, -90), (180, 180)
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Appearance)
    TArray<FEqZeroCircumferenceMarkerEntry> MarkerList;

    // 我们画的是准星,这那个圆的半径
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Appearance, meta=(ClampMin=0.0))
    float Radius = 48.0f;

    // 我们画的是准星,上下左右四个正方形。这是其中的一个正方形的图片
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Appearance)
    FSlateBrush MarkerImage;

    // 标线片角部图像是否放置在扩散半径之外
    // 改为 0-1 的浮点对齐(例如,在半径内部 / 之上 / 外部)?
    UPROPERTY(EditAnywhere, Category=Corner)
    uint8 bReticleCornerOutsideSpreadRadius : 1;

public:
    UFUNCTION(BlueprintCallable, Category = "Appearance")
    void SetRadius(float InRadius);

private:
    TSharedPtr<SEqZeroCircumferenceMarkerWidget> MyMarkerWidget;
};

这是画一个slate

重写 RebuildWidget 在里面返回一个 Slate对象

SynchronizeProperties 是编辑器属性修改后的回调

这是那个slate,怎么绘制给看stale的OnPlant

TSharedRef<SWidget> UEqZeroCircumferenceMarkerWidget::RebuildWidget()
{
    MyMarkerWidget = SNew(SEqZeroCircumferenceMarkerWidget)
        .MarkerBrush(&MarkerImage)
        .Radius(this->Radius)
        .MarkerList(this->MarkerList);

    return MyMarkerWidget.ToSharedRef();
}
FSlateRenderTransform SEqZeroCircumferenceMarkerWidget::GetMarkerRenderTransform(const FEqZeroCircumferenceMarkerEntry& Marker, const float BaseRadius, const float HUDScale) const
{
    // 步骤 1:确定实际半径
    float XRadius = BaseRadius;
    float YRadius = BaseRadius;
    if (bReticleCornerOutsideSpreadRadius)
    {
        // 图片中心在圆周外侧半个图片宽度处
        // 即图片的内边缘刚好贴住半径圆
        XRadius += MarkerBrush->ImageSize.X * 0.5f;
        YRadius += MarkerBrush->ImageSize.X * 0.5f;
    }

    // 步骤 2:角度转弧度
    const float LocalRotationRadians = FMath::DegreesToRadians(Marker.ImageRotationAngle);
    const float PositionAngleRadians = FMath::DegreesToRadians(Marker.PositionAngle);

    // 步骤 3:绕图片自身中心旋转
    // Slate 的旋转基准点默认是左上角,要绕中心旋转需要用"先移-旋-移回"技巧:
    //   T(-W/2, -H/2)  →  把图片中心平移到原点
    //   R(θ)           →  绕原点旋转
    //   T(+W/2, +H/2)  →  恢复位置
    FSlateRenderTransform RotateAboutOrigin(
        Concatenate(
            FVector2D(
                -MarkerBrush->ImageSize.X * 0.5f, -MarkerBrush->ImageSize.Y * 0.5f),
                FQuat2D(LocalRotationRadians),
                FVector2D(MarkerBrush->ImageSize.X * 0.5f, MarkerBrush->ImageSize.Y * 0.5f)
        )
    );

    // 步骤 4:平移到圆周上的目标位置
    // 极坐标 → 笛卡尔坐标(Slate Y轴朝下,所以 Y 取负才是"上方")
    //   x = R · sin(θ)   → 0°时 sin=0,无水平偏移
    //   y = -R · cos(θ)  → 0°时 cos=1,-R 即向上
    return TransformCast<FSlateRenderTransform>(
        Concatenate(
            RotateAboutOrigin,
            FVector2D(
                XRadius * FMath::Sin(PositionAngleRadians) * HUDScale, // X 偏移
                -YRadius * FMath::Cos(PositionAngleRadians) * HUDScale // Y 偏移(负号!)
            )
        )
    );
}

int32 SEqZeroCircumferenceMarkerWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
    // 1. 是否启用(父控件禁用则自身也禁用)
    const bool bIsEnabled = ShouldBeEnabled(bParentEnabled);
    const ESlateDrawEffect DrawEffects = bIsEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect; // 禁用时灰显

    // 2. 计算控件本地空间的中心点
    // GetLocalPositionAtCoordinates(0.5, 0.5) = 控件矩形的正中心
    const FVector2D LocalCenter = AllottedGeometry.GetLocalPositionAtCoordinates(FVector2D(0.5f, 0.5f)); 

    // 3. 前置检查:MarkerList 非空 且 笔刷非空
    const bool bDrawMarkers = (MarkerList.Num() > 0) && (MarkerBrush != nullptr);

    if (bDrawMarkers == true)
    {
        // 4. 确定颜色
        // 优先使用用户手动设置的 ColorAndOpacity
        // 否则用父控件颜色调色 * 笔刷自身颜色(颜色叠加)
        const FLinearColor MarkerColor = bColorAndOpacitySet ?
            ColorAndOpacity.Get().GetColor(InWidgetStyle) :
            (InWidgetStyle.GetColorAndOpacityTint() * MarkerBrush->GetTint(InWidgetStyle));

        // 5. Alpha 为 0 时跳过绘制(性能优化)
        if (MarkerColor.A > KINDA_SMALL_NUMBER)
        {
            const float BaseRadius = Radius.Get();

            // 6. 获取 HUD 缩放系数(适配不同分辨率)
            const float ApplicationScale = GetDefault<UUserInterfaceSettings>()->ApplicationScale;

            // 7. 遍历每一个标记,逐个绘制
            for (const FEqZeroCircumferenceMarkerEntry& Marker : MarkerList)
            {
                // 8. 计算该标记的变换矩阵
                //    包含:图片自旋 + 平移到圆周上的位置
                const FSlateRenderTransform MarkerTransform = GetMarkerRenderTransform(Marker, BaseRadius, ApplicationScale);

                // 9. 构建绘制几何体
                //    - 大小 = MarkerBrush->ImageSize(图片像素尺寸)
                //    - 布局偏移 = LocalCenter - ImageSize*0.5  → 先把图片左上角对齐到控件中心
                //    - 渲染变换 = MarkerTransform             → 再做旋转+平移到圆周
                //    - Pivot = (0, 0)                         → 变换基准点是图片左上角
                const FPaintGeometry Geometry(AllottedGeometry.ToPaintGeometry(MarkerBrush->ImageSize,
                    FSlateLayoutTransform(LocalCenter - (MarkerBrush->ImageSize * 0.5f)),
                    MarkerTransform, FVector2D(0.0f, 0.0f)));

                // 10. 提交一个 Box 绘制命令(即绘制一张贴图)
                FSlateDrawElement::MakeBox(OutDrawElements, LayerId, Geometry, MarkerBrush, DrawEffects, MarkerColor);
            }
        }
    }

    return LayerId; // 没有新增层级,直接返回原 LayerId
}

UEqZeroHitMarkerConfirmationWidget

/*
 * 这个画的是击中效果那个红色的叉
 * 
 */
UCLASS()
class UEqZeroHitMarkerConfirmationWidget : public UWidget
{
    GENERATED_BODY()

public:
    UEqZeroHitMarkerConfirmationWidget(const FObjectInitializer& ObjectInitializer);

    /*
     * UWidget interface
     */
protected:
    virtual TSharedRef<SWidget> RebuildWidget() override;
    /*
     * UVisual interface
     */
public:
    virtual void ReleaseSlateResources(bool bReleaseChildren) override;

    // ==========================================================================

public:
    // 显示击中通知的持续时间(以秒为单位)(它们会在这段时间内逐渐变为透明)
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Appearance, meta=(ClampMin=0.0, ForceUnits=s))
    float HitNotifyDuration = 0.4f;

    // 用于绘制单个命中标记的标记图像
    // 建议看蓝图,一个红色的菱形
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Appearance)
    FSlateBrush PerHitMarkerImage;

    // 这是一个 TAG=>图片。例如爆头识别到这个TAG,就显示这个图片盖上去
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Appearance)
    TMap<FGameplayTag, FSlateBrush> PerHitMarkerZoneOverrideImages;

    // 如果存在任何命中结果时要绘制的标记图像。
    // 常规的命中红叉
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Appearance)
    FSlateBrush AnyHitsMarkerImage;

private:
    TSharedPtr<SEqZeroHitMarkerConfirmationWidget> MyMarkerWidget;
};
int32 SEqZeroHitMarkerConfirmationWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
    // 是否启用(父控件禁用则自身也禁用)
    const bool bIsEnabled = ShouldBeEnabled(bParentEnabled);
    const ESlateDrawEffect DrawEffects = bIsEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect;

    // 计算控件本地空间的中心点
    // GetLocalPositionAtCoordinates(0.5, 0.5) = 控件矩形的正中心
    const FVector2D LocalCenter = AllottedGeometry.GetLocalPositionAtCoordinates(FVector2D(0.5f, 0.5f));

    // 根据当前的击中通知不透明度决定是否绘制标记
    const bool bDrawMarkers = (HitNotifyOpacity > KINDA_SMALL_NUMBER);

    if (bDrawMarkers)
    {

        TArray<FEqZeroScreenSpaceHitLocation> LastWeaponDamageScreenLocations;
        if (APlayerController* PC = MyContext.IsInitialized() ? MyContext.GetPlayerController() : nullptr)
        {
            if (UEqZeroWeaponStateComponent* WeaponStateComponent = PC->FindComponentByClass<UEqZeroWeaponStateComponent>())
            {
                WeaponStateComponent->GetLastWeaponDamageScreenLocations(LastWeaponDamageScreenLocations);
            }
        }

        if ((LastWeaponDamageScreenLocations.Num() > 0) && (PerHitMarkerImage != nullptr))
        {
            const FVector2D HalfAbsoluteSize = AllottedGeometry.GetAbsoluteSize() * 0.5f;

            for (const FEqZeroScreenSpaceHitLocation& Hit : LastWeaponDamageScreenLocations)
            {
                // 1. 查是否有这个 HitZone 的专属图片(如爆头),没有则用默认图
                const FSlateBrush* LocationMarkerImage = PerHitMarkerZoneOverrideImages.Find(Hit.HitZone);
                if (LocationMarkerImage == nullptr)
                {
                    LocationMarkerImage = PerHitMarkerImage;
                }

                // 2. 颜色 × HitNotifyOpacity(实现淡出效果)
                FLinearColor MarkerColor = bColorAndOpacitySet ?
                    ColorAndOpacity.Get().GetColor(InWidgetStyle) :
                    (InWidgetStyle.GetColorAndOpacityTint() * LocationMarkerImage->GetTint(InWidgetStyle));
                MarkerColor.A *= HitNotifyOpacity;

                // 3. 关键坐标转换:窗口屏幕坐标 → 控件本地坐标
                //    Hit.Location 是视口坐标,加上 MyCullingRect.TopLeft 转为窗口坐标
                //    再用 AbsoluteToLocal 转为控件内部坐标
                const FVector2D WindowSSLocation = Hit.Location + MyCullingRect.GetTopLeft(); // 在非全屏模式下考虑窗口装饰
                const FSlateRenderTransform DrawPos(AllottedGeometry.AbsoluteToLocal(WindowSSLocation));

                // 图片中心对齐到命中点
                const FPaintGeometry Geometry(AllottedGeometry.ToPaintGeometry(LocationMarkerImage->ImageSize, FSlateLayoutTransform(-(LocationMarkerImage->ImageSize * 0.5f)), DrawPos));
                FSlateDrawElement::MakeBox(OutDrawElements, LayerId, Geometry, LocationMarkerImage, DrawEffects, MarkerColor);
            }
        }

        if (AnyHitsMarkerImage != nullptr)
        {
            FLinearColor MarkerColor = bColorAndOpacitySet ?
                ColorAndOpacity.Get().GetColor(InWidgetStyle) :
                (InWidgetStyle.GetColorAndOpacityTint() * AnyHitsMarkerImage->GetTint(InWidgetStyle));
            MarkerColor.A *= HitNotifyOpacity;

            // 否则在十字准线的中心显示命中通知
            const FPaintGeometry Geometry(
                AllottedGeometry.ToPaintGeometry(
                    AnyHitsMarkerImage->ImageSize,
                    FSlateLayoutTransform(LocalCenter - (AnyHitsMarkerImage->ImageSize * 0.5f))));
            FSlateDrawElement::MakeBox(OutDrawElements, LayerId, Geometry, AnyHitsMarkerImage, DrawEffects, MarkerColor);
        }
    }

    return LayerId;
}

准星UI显示

先把UI显示出来?

找不到刚刚的OnWeaponChanged了。。。

全局搜一下

/EqZeroCore/UserInterface/HUD/W_WeaponReticleHost.W_WeaponReticleHost

加到HUD上

走的 /EqZeroCore/Experiences/LAS_EqZGame_StandardHUD.LAS_EqZGame_StandardHUD

现在能看到准星了。

组件总结

整理一下我们动态挂接的组件

  • GameFeature:
    • Controller
      • InventoryManagerComponent
    • Character
      • EquipmentManagerComponent
  • Experience
    • Controller
      • B_PickRandomCharacter
    • EqGameState
      • DefaultSpawningRules => Eq Player Spawning Manager Component 转发逻辑选择场景重生点
    • Stand Component (通过配置批量加了一批标准的组件)
      • QuickBar Component

简单来说,对于武器来说涉及

物品组件,装备组件,快捷栏组件

现在UI出来了 ,但是没有其他效果。

BUG重生呢后边T-Pose了

重生后武器被clear了。链接不到动画层。

从原版的加物品,加装备流程调试,确定流程就是跑这里

void UEqZeroQuickBarComponent::SetActiveSlotIndex_Implementation(int32 NewIndex)
{
    if (Slots.IsValidIndex(NewIndex) && (ActiveSlotIndex != NewIndex))
    {
        // 卸下当前 -> 切换索引 -> 穿上新装备
        UnEquipItemInSlot();

        ActiveSlotIndex = NewIndex;

        EquipItemInSlot();

        OnRep_ActiveSlotIndex();
    }
}

但是我们的 ActiveSlotIndex 是 0,和重写加的 0 冲突,导致加不上

死亡的时候没有清理

这个蓝图在 K2_OnDeathFinished

调试发现,这个都没跑

void AEqZeroCharacter::OnDeathFinished(AActor*)
{
    GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ThisClass::DestroyDueToDeath);
}

一路查到死亡技能没有EndAbility

他是从

    UAbilityTask_WaitDelay* Task = UAbilityTask_WaitDelay::WaitDelay(this, DeathDuration);
    Task->OnFinish.AddDynamic(this, &ThisClass::OnDeathWaitFinished);
    Task->ReadyForActivation();

死亡后8s后结束技能,触发了结束死亡的流程

void UEqZeroGameplayAbility_AutoRespawn::OnPlayerReset()
{
    // ...

    bShouldFinishRestart = false;
    if (auto ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(ControllerToReset->PlayerState))
    {
        TArray<FGameplayAbilitySpecHandle> OutAbilityHandles;
        ASC->FindAllAbilitiesWithTags(
            OutAbilityHandles,
            FGameplayTagContainer(EqZeroGameplayTags::Ability_Type_StatusChange_Death), true);

        for (const FGameplayAbilitySpecHandle& Handle : OutAbilityHandles)
        {
            if (FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromHandle(Handle))
            {
                if (UEqZeroGameplayAbility* AbilityInstance = Cast<UEqZeroGameplayAbility>(Spec->GetPrimaryInstance()))
                {
                    AbilityInstance->ForceEndAbility(Handle, AbilityInstance->GetCurrentActorInfo(), AbilityInstance->GetCurrentActivationInfo(), true, false);
                }
            }
        }
    }

ForceEndAbility 自己实现一个代替蓝图的K2

BUG: 瞄准不是屏幕正前

???感觉角色转反了

但是RootYawOffset 好像是正常的

瞄准偏移,漏了一个负号

AimYaw = RootYawOffset * -1.f;

血条UI

/Game/UI/Hud/W_Healthbar

原来蓝图的代码,写到C++了

UEqZeroHealthBarWidget

动画抄了过来,扣血的。

从生命值组件的事件监听。

void UEqZeroHealthBarWidget::OnHealthChange(UEqZeroHealthComponent* HealthComponent, float OldValue, float NewValue, AActor* Instigator)
{
    const float OldNormalized = OldValue / 100.0f;
    const float NewNormalized = NewValue / 100.0f;
    NormalizedHealth = NewNormalized;

    // 1. 更新材质参数:Health_Current = 旧值, Health_Updated = 新值
    for (UMaterialInstanceDynamic* MID : TArray<UMaterialInstanceDynamic*>{ BarBorderMID, BarFillMID, BarGlowMID })
    {
        if (MID)
        {
            MID->SetScalarParameterValue(TEXT("Health_Current"), OldNormalized);
            MID->SetScalarParameterValue(TEXT("Health_Updated"), NewNormalized);
        }
    }

    // 2. 重置动画状态
    ResetAnimatedState();

    // 3. 数字已在目标值且是治疗/不变时跳过动画播放
    const bool bAlreadyAtTarget = HealthNumber && FMath::IsNearlyEqual(HealthNumber->GetTargetValue(), NewValue);
    const bool bIsHealingOrSame = NewValue >= OldValue;
    if (bAlreadyAtTarget && bIsHealingOrSame)
    {
        return;
    }

    // 4. 设置 DamageOrHealing 参数并播放对应动画
    const float DamageOrHealingValue = bIsHealingOrSame ? 1.0f : 0.0f;
    for (UMaterialInstanceDynamic* MID : TArray<UMaterialInstanceDynamic*>{ BarBorderMID, BarFillMID, BarGlowMID })
    {
        if (MID)
        {
            MID->SetScalarParameterValue(TEXT("DamageOrHealing"), DamageOrHealingValue);
        }
    }

    UWidgetAnimation* AnimToPlay = bIsHealingOrSame ? OnHealed : OnDamaged;
    PlayAnimation(AnimToPlay, 0.0f, 1, EUMGSequencePlayMode::Forward, 1.0f);

    // 5. 数字先跳到旧值,再插值到新值
    if (HealthNumber)
    {
        HealthNumber->SetCurrentValue(OldValue);
        HealthNumber->InterpolateToValue(NewValue, 1.0f, 4.0f, 0.0f);
    }

    // 6. 否为小数点比较2 = 0,绑定 OnDamaged 动画结束时触发 EventOnEliminated
    if (FMath::IsNearlyZero(NewValue))
    {
        FWidgetAnimationDynamicEvent EliminatedDelegate;
        EliminatedDelegate.BindDynamic(this, &ThisClass::EventOnEliminated);
        BindToAnimationFinished(OnDamaged, EliminatedDelegate);
    }
}

装备栏

common text block

common numeric text block

/EqZeroCore/UserInterface/HUD/W_WeaponAmmoAndName.W_WeaponAmmoAndName

/EqZeroCore/UserInterface/HUD/W_QuickBarSlot.W_QuickBarSlot

/EqZeroCore/UserInterface/HUD/W_QuickBar.W_QuickBar

搓UI

读取一下 UEqZeroQuickBarComponent 的数据,基于事件更新一下子弹信息,UI逻辑。没啥好贴代码的。

Fire的消耗

关于 UEqZeroAbilityCost_ItemTagStack 的配置

/EqZeroCore/Weapons/Pistol/GA_Weapon_Fire_Pistol.GA_Weapon_Fire_Pistol

这个cost配置

void UEqZeroAbilityCost_ItemTagStack::ApplyCost(const UEqZeroGameplayAbility* Ability, const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
    if (ActorInfo->IsNetAuthority())
    {
        if (const UEqZeroGameplayAbility_FromEquipment* EquipmentAbility = Cast<const UEqZeroGameplayAbility_FromEquipment>(Ability))
        {
            if (UEqZeroInventoryItemInstance* ItemInstance = EquipmentAbility->GetAssociatedItem())
            {
                const int32 AbilityLevel = Ability->GetAbilityLevel(Handle, ActorInfo);

                const float NumStacksReal = Quantity.GetValueAtLevel(AbilityLevel);
                const int32 NumStacks = FMath::TruncToInt(NumStacksReal);

                ItemInstance->RemoveStatTagStack(Tag, NumStacks);
            }
        }
    }
}

他的消耗核心逻辑是,拿到技能 SourceObject 这是一个赋予技能的时候我们可以自己关联的值,我们关联了物品实例

获取了物品上有一个 tag=>数字 的容器,给我记录子弹

上一篇
下一篇