Lyra 用Slate画准星的总结

概述

https://www.cwlgame.cn/2024/06/25/ue-slate/

绘制测试

案例1 - 绘制到控件中心

// 测试图片是 立着的 长方形
// --- A: 在控件正中心画一个不旋转、不偏移的 Marker(基准参照) ---
{
    const FVector2D ScaledSize = MarkerBrush->ImageSize * TestScale;
    // 图片左上角 = 中心 - 半尺寸,使图片中心对齐控件中心
    const FVector2D TopLeft = FVector2D(0.f, 0.f); // LocalCenter - ScaledSize * 0.5f;

    const FPaintGeometry CenterGeo = AllottedGeometry.ToPaintGeometry(
        ScaledSize,                          // 绘制尺寸
        FSlateLayoutTransform(TopLeft)        // 布局偏移(左上角位置)
    );
    FSlateDrawElement::MakeBox(OutDrawElements, LayerId, CenterGeo, MarkerBrush, DrawEffects,
        FLinearColor(0.0f, 0.5f, 1.0f, 0.8f)); // 半透明蓝色,作为参照
}

坐标系

(0,0) ────────────► X+ (向右)
  │
  │
  │    控件区域
  │
  ▼
  Y+ (向下)

scale = 1.f

TopLeft = 0,0 就是图片左上角对着控件区域的左上角,

TopLeft = LocalCenter 是图片左上角对着中心

TopLeft = LocalCenter - ScaledSize * 0.5f; 图片中心对着

图片是 6 * 10 图片没有铺满,蓝色的区域其实不是整个图片。

问:为什么是左上角顶过去

RenderTransform,也就没有 Pivot 的概念。它只做纯布局偏移

ayoutTransform 就是直接定义左上角位置的——这是 Slate 布局的基本规则,跟 Pivot 无关。

案例2 - 平移

外部size box 只有 128 x 128

const float TestOffsetX        = 32.0f;    // 额外 X 偏移(像素):正值=向右
const float TestOffsetY        = 64.0f;    // 额外 Y 偏移(像素):正值=向下

// --- B: 只做平移,不旋转 —— 观察 TestOffsetX / TestOffsetY 的效果 ---
{
    const FVector2D ScaledSize = MarkerBrush->ImageSize * TestScale;
    const FVector2D TopLeft = FVector2D(TestOffsetX, TestOffsetY);

    const FPaintGeometry OffsetGeo = AllottedGeometry.ToPaintGeometry(
        ScaledSize,
        FSlateLayoutTransform(TopLeft)
    );
    FSlateDrawElement::MakeBox(OutDrawElements, LayerId, OffsetGeo, MarkerBrush, DrawEffects,
        FLinearColor(0.0f, 1.0f, 0.0f, 0.6f)); // 半透明绿色
}

竖着1/4,横着切一刀。图片左上角的位置

案例3 - 坐标旋转

记得蓝色长方体的中心是,控件的中心。

const float TestPositionAngle  = 30.0f;    // 圆周位置角度(度):0=正上方, 90=右, 180=下, 270=左

// --- C: 放到圆周上 —— 观察 TestPositionAngle + BaseRadius 的效果 ---
{
    const float PosRad = FMath::DegreesToRadians(TestPositionAngle);
    // 极坐标 → 笛卡尔:sin 控制 X,-cos 控制 Y(0°=上)
    float TestRadius = 64.f; // BaseRadius;
    const FVector2D CircleOffset(
        TestRadius * FMath::Sin(PosRad) * ApplicationScale,
        -TestRadius * FMath::Cos(PosRad) * ApplicationScale
    );

    const FVector2D ScaledSize = MarkerBrush->ImageSize * TestScale;
    const FVector2D TopLeft = CircleOffset; //LocalCenter - ScaledSize * 0.5f + CircleOffset;

    const FPaintGeometry CircleGeo = AllottedGeometry.ToPaintGeometry(
        ScaledSize,
        FSlateLayoutTransform(TopLeft)
    );
    FSlateDrawElement::MakeBox(OutDrawElements, LayerId, CircleGeo, MarkerBrush, DrawEffects,
        FLinearColor(1.0f, 1.0f, 0.0f, 0.8f)); // 半透明黄色
}

30度数,半径64

黄色是slate画的,其他的是我标记的辅助线,sin30 这个 1/2的关系比较直观的表现。

他是一个这样的极坐标系。顺时针正方向。

带到slate坐标系,X = R sin,Y = - R Cos

=====

如果我们就是想用数学课的标准极坐标系呢?

const FVector2D CircleOffset(
    TestRadius * FMath::Cos(PosRad) * ApplicationScale,
    -TestRadius * FMath::Sin(PosRad) * ApplicationScale
);

也是OK的,理解不一样,位置不一样,配置不一样。不用太纠结,不过Lyra用的是第一种

案例4 - 自身旋转

const float TestRotationAngle  = 45.0f;    // 图片自身旋转角度(度):正值=顺时针

// --- D2: 正确做法 —— 手动 Concatenate 绕中心旋转 ---
{
    const float RotRad = FMath::DegreesToRadians(-45.f); // 负值 = 逆时针旋转作为对比
    const FVector2D HalfSize = MarkerBrush->ImageSize * 0.5f;

    //   Concatenate 执行顺序:从左到右
    //   ① FVector2D(-HalfSize)  →  平移:把图片中心挪到左上角原点
    //   ② FQuat2D(RotRad)        →  旋转:绕原点(此时就是图片中心)旋转
    //   ③ FVector2D(+HalfSize)   →  平移:挪回去
    FSlateRenderTransform RotateAroundCenter(Concatenate(
        FVector2D(-HalfSize.X, -HalfSize.Y),  // ① 移到原点
        FQuat2D(RotRad),                        // ② 旋转
        FVector2D(HalfSize.X, HalfSize.Y)      // ③ 移回来
    ));

    const FPaintGeometry CorrectGeo(AllottedGeometry.ToPaintGeometry(
        MarkerBrush->ImageSize,
        FSlateLayoutTransform(LocalCenter - HalfSize),  // 布局:图片中心对齐控件中心
        RotateAroundCenter,                              // 渲染变换:绕自身中心旋转
        FVector2D(0.0f, 0.0f)                            // Pivot 这里无所谓,因为旋转已经在 Concatenate 里处理了
    ));
    FSlateDrawElement::MakeBox(OutDrawElements, LayerId, CorrectGeo, MarkerBrush, DrawEffects,
        FLinearColor(1.0f, 0.2f, 0.2f, 1.0f)); // 不透明红色 —— 绕中心旋转,位置稳定
}

// --- D3: 更简单的方式 —— 用 Pivot 参数代替手动 Concatenate ---
{
    const float RotRad = FMath::DegreesToRadians(TestRotationAngle);

    // 直接用 FQuat2D,但把 Pivot 设成 (0.5, 0.5) = 图片中心
    // 引擎会自动帮你做"移到中心→旋转→移回"
    FSlateRenderTransform SimpleRotate{FQuat2D(RotRad)};

    const FVector2D HalfSize = MarkerBrush->ImageSize * 0.5f;
    const FVector2D TopLeft = LocalCenter - HalfSize;
    const FPaintGeometry SimpleGeo(AllottedGeometry.ToPaintGeometry(
        MarkerBrush->ImageSize,
        FSlateLayoutTransform(TopLeft),
        SimpleRotate,
        FVector2D(0.5f, 0.5f)  // ← Pivot = 图片中心!引擎自动绕这个点旋转
    ));
    FSlateDrawElement::MakeBox(OutDrawElements, LayerId, SimpleGeo, MarkerBrush, DrawEffects,
        FLinearColor(0.2f, 1.0f, 0.2f, 0.6f)); // 半透明绿色 —— 效果和 D2 红色完全重合
}

注意这个菱形,我改成10x10好看到,淡淡和和周围深一点的灰,这个是图片范围,里面长方形条只是,图片中的可视部分

还是pivot的旋转好理解。

关于D2:有一些疑问?

Concatenate 是一个把多个变换组合的接口。

FSlateRenderTransform RotateAroundCenter(Concatenate(
    FVector2D(-HalfSize.X, -HalfSize.Y),  // ① 移到原点
    FQuat2D(RotRad),                        // ② 旋转
    FVector2D(HalfSize.X, HalfSize.Y)      // ③ 移回来
));

const FPaintGeometry CorrectGeo(AllottedGeometry.ToPaintGeometry(
    MarkerBrush->ImageSize,
    FSlateLayoutTransform(LocalCenter - HalfSize),  // 布局:图片中心对齐控件中心
    RotateAroundCenter,                              // 渲染变换:绕自身中心旋转
    FVector2D(0.0f, 0.0f)                            // 必须是 (0,0),Concatenate 要求
));

这里 pivot为什么是0,0

引擎在应用 RenderTransform 时,内部会用 Pivot 做一层包裹:

实际变换 = T(-PivotSize) → RenderTransform → T(+PivotSize)

代码在哪里?下次一定。

Engine\Source\Runtime\SlateCore\Public\Layout\Geometry.h FGeometry 构造函数的 AccumulatedRenderTransform 变量初始化。

如果有pivot和FSlateRenderTransform第一步第三部重复了。

那么为什么不直接用pivot旋转呢?就是D3的写法。

测试也是OK的,但是Lyra用的是D2的写法。

完成准星

marker是编辑器配置的

USTRUCT(BlueprintType)
struct FEqZeroCircumferenceMarkerEntry
{
    GENERATED_BODY()

    /*
     * 极坐标
     *          0
     *
     * -90              90
     *
     *         180
     */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(ForceUnits=deg))
    float PositionAngle = 0.0f;

    // 图片自己的旋转
    UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(ForceUnits=deg))
    float ImageRotationAngle = 0.0f;
};

数据是

【0, 0】【-90,-90】【90, 90】【180,180】

FSlateRenderTransform SEqZeroCircumferenceMarkerWidget::GetMarkerRenderTransform(const FEqZeroCircumferenceMarkerEntry& Marker, const float BaseRadius, const float HUDScale) const
{
    // 步骤 1:确定实际半径
    float XRadius = BaseRadius;
    float YRadius = BaseRadius;
    if (bReticleCornerOutsideSpreadRadius)
    {
        // 这是配置,默认True
        // 如果标记要贴在圆周外侧,那么半径需要加上标记图片尺寸的一半,才能让标记的边缘贴在圆周上。
        // 形象的理解就是准星向外推,圆和准星正方形的交接不是准星中心点,而是准星边缘。
        XRadius += MarkerBrush->ImageSize.X * 0.5f;
        YRadius += MarkerBrush->ImageSize.X * 0.5f;
    }

    // 步骤 2:角度转弧度

    // 图片自身配置的旋转
    const float LocalRotationRadians = FMath::DegreesToRadians(Marker.ImageRotationAngle);

    /*
     * 图片 极坐标 下的旋转
     *          0
     *
     * -90              90
     *
     *         180
     */
    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 偏移(负号!)
            )
        )
    );
}

击中效果

射线检测的 Hit.Location 通过 UGameplayStatics::ProjectWorldToScreen 转换为屏幕的点。传递给了 FEqZeroScreenSpaceHitLocation

// 在准星中会显示远程武器命中的命中标记
// 当对敌人造成伤害时,会显示一个 “成功” 的命中标记。
struct FEqZeroScreenSpaceHitLocation
{
    /** 视口屏幕空间中的命中位置 */
    FVector2D Location; 
    FGameplayTag HitZone;
    bool bShowAsSuccess = false;
};

最后到了slate这边。

基于前面的分析,这部分的代码只是一些简单的画图片

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 (true) 
    {

        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;
}
上一篇
下一篇