概述
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;
}