虚幻C++插件胚胎级入门 | Slate Widget开发

发布于:2025-03-10 ⋅ 阅读:(24) ⋅ 点赞:(0)

智能指针

  • 不同类型的智能指针
  • 其独特的属性和句法

smart pointer ease burden of memory allocation and tracking

Smart Pointer Type Use Case
Shared Pointers (TSharedPtr) A Shared Pointer owns the object it references, indefinitely preventing deletion of that object, and ultimately handling its deletion when no Shared Pointer or Shared Reference (see below) references it. A Shared Pointer can be empty, meaning it doesn't reference any object. Any non-null Shared Pointer can produce a Shared Reference to the object it references.
Shared References (TSharedRef) A Shared Reference acts like a Shared Pointer, in the sense that it owns the object it references. They differ with regard to null objects; Shared References must always reference a non-null object. Because Shared Pointers don't have that restriction, a Shared Reference can always be converted to a Shared Pointer, and that Shared Pointer is guaranteed to reference a valid object. Use Shared References when you want a guarantee that the referenced object is non-null, or if you want to indicate shared object ownership.
Weak Pointers (TWeakPtr) Weak Pointers are similar to Shared Pointers, but do not own the object they reference, and therefore do not affect its lifecycle. This property can be very useful, as it breaks reference cycles, but it also means that a Weak Pointer can become null at any time, without warning. For this reason, a Weak Pointer can produce a Shared Pointer to the object it references, ensuring programmers safe access to the object on a temporary basis.
Unique Pointers (TUniquePtr) A Unique Pointer solely and explicitly owns the object it references. Since there can only be one Unique Pointer to a given resource, Unique Pointers can transfer ownership, but cannot share it. Any attempts to copy a Unique Pointer will result in a compile error. When a Unique Pointer goes out of scope, it will automatically delete the object it references.

四种智能指针的区别:选指针就像选遥控器——要共享用 Shared,要专属用 Unique,临时借用选 Weak,强制存在用 Ref。

1. TSharedPtr(共享指针)
你和朋友共用一把钥匙(对象),每复制一把钥匙计数器+1  
只有当所有钥匙都被销毁(计数器归零),房间(对象)才会被拆除  

→ 带计数器的钥匙(可空,可共享)   

2. TSharedRef(共享引用)

3. TWeakPtr(弱指针)
→ 临时借用卡(不阻止房间拆除)
能查看房间有没有被拆,但不会阻止拆除  
需要时可用借用卡换临时钥匙(升级为TSharedPtr),用完即还  
特点:防内存泄漏,适合观察但不影响生命周期的场景  

4. TUniquePtr(独占指针)
→ 一次性密码锁(独占权限)
最适合独享资源比如角色专属武器,安全高效  


钥匙能共享(TSharedPtr)  
引用不能空(TSharedRef)  
借用不负责(TWeakPtr)  
独占最安全(TUniquePtr)  

类型 特性 生命周期场景

TSharedPtr<T>

引用计数智能指针,线程安全(

ESPMode::ThreadSafe

UI 控件、跨模块对象共享

TWeakPtr<T>

弱引用,不增加引用计数(需用 

Pin()

 提升)
防止循环引用(如 Slate 控件间的相互引用)

TUniquePtr<T>

独占所有权(类似 

std::unique_ptr

局部资源管理(如临时纹理加载)

指针和引用

指针(Pointer)和引用(Reference)的类比可以这样理解:


📌 指针(Pointer)—— 快递柜的取件码

  • 特性
    • 存储的是具体地址(如 0x7ffee123),就像快递柜的取件码(如 A-102)。
    • 可以重新指向其他地址(int* p = &a; p = &b;),如同用同一个取件码系统生成新柜子的密码。
    • 可能为空(nullptr),就像输入无效的取件码会导致开箱失败。
    • 需要显式操作符(*)才能访问值,如同输入密码后需要按「确认键」才能开箱。

🏷️ 引用(Reference)—— 贴了标签的柜子

  • 特性
    • 是变量的别名(如 int& r = a;),就像给某个柜子贴上「VIP专用」标签。
    • 一旦绑定无法更改(r = b; 只是修改值,而非绑定新地址),如同标签贴牢后无法撕下。
    • 必须初始化且永不为空,如同贴标签前必须确定对应柜子存在。
    • 直接操作原变量(无需*),如同看到标签就能直接打开对应的柜子。

Slate介绍

-Slate原意石板,类似于UI设计在石板上作画。

-Slate分辨率自适应

-Slate渲染流程:

控件对象转换为图形面片,通过pixelshader和vertexshader来使用GPU绘制,拿回绘图结果显示在SWindow中

注:

Slate UI框架在渲染过程中同时使用了Vertex Shader(顶点着色器)和Pixel Shader(像素着色器),二者在渲染管线中承担不同的关键角色:这种设计使Slate既能保持2D UI的高效渲染(每帧处理数万顶点),又能实现复杂的视觉效果(如渐变、alpha混合、颜色插值、材质驱动的动态UI)

1. Vertex Shader

  • 空间坐标变换:将2D UI元素的顶点数据(位置/UV坐标)从局部坐标系转换到屏幕坐标系
// 典型伪代码示例
float4 VS_Main(float2 Pos : POSITION) : SV_POSITION
{
    return mul(float4(Pos, 0, 1), ViewProjectionMatrix);
}
  • 实现动态形变效果(如按钮点击弹性动画)
  • 数据传递:向像素着色器传递UV坐标、颜色参数等插值数据

2. Pixel Shader

  • 颜色计算:处理控件的基础色、渐变、透明度混合
// 典型伪代码示例
float4 PS_Main(float2 UV : TEXCOORD) : SV_Target
{
    float4 Color = Texture2D.Sample(TextureSampler, UV);
    Color *= WidgetColor;
    return Color;
}
  • 特效实现:处理圆角遮罩、边框模糊、阴影渲染等视觉效果
  • 纹理合成:对图集(Texture Atlas)进行采样,支持九宫格缩放
1. 纹理图集(Texture Atlas)
  • What:将多个小纹理(如UI图标、按钮)合并到一张大纹理中。
  • Why:减少GPU绘制调用(Draw Call),提升渲染性能。
2. 九宫格缩放(9-Slice Scaling)
  • What:将纹理划分为9个区域(四角+四边+中心),缩放时仅拉伸中心区域。
  • Why:保持边角不变形,避免缩放导致的模糊或扭曲。
  • How

LOCTEXT

margin页边白

slate石板

justification对齐

const FText TitleText = LOCTEXT("mimimimimi","miaomiao");

这段代码是用于Unreal Engine中的本地化文本定义:
1. const 修饰符
   - 表示定义的是一个常量,不可被修改
2. FText 类型
   - Unreal Engine中专为UI文本设计的数据类型
   - 相比`FString`具有以下特性:
     自动处理文本本地化
     支持文本格式化验证
     内存管理更安全
3. LOCTEXT 宏
   LOCTEXT(Key, SourceText)
   - UE专有的本地化宏
   - 参数说明:
      "mimimimimi":本地化键值(唯一标识符)
      "miaomiao":默认显示文本(当找不到本地化条目时显示)
4. 运行机制
   - 引擎会在以下位置查找本地化文件:
     Content/Localization/ 目录下的.csv或.po文件
   - 匹配流程:
     1. 根据当前语言环境查找"mimimimimi"键
     2. 找到则显示对应语言的翻译
     3. 未找到则显示默认的"miaomiao"

Build.cs添加模块

关于在Unreal Engine项目中未添加Slate/SlateCore模块却能编译成功,主要原因如下:


▶ 核心原理:模块隐式依赖

Unreal Engine的模块系统具有 依赖传递(Transitive Dependencies) 特性。当以下任一情况成立时,Slate相关模块会被 自动引入

  1. 父模块已声明依赖
    若项目依赖了任意已包含Slate模块的引擎模块(如UMG或ApplicationCore),会自动继承其依赖:
   // 假设你的模块以公共模块作为依赖
   PublicDependencyModuleNames.AddRange(new string[] { 
       "Core", 
       "UMG"  // 该模块已包含SlateCore依赖
   });
   
  1. 引擎内置模板的隐式代码
    通过编辑器创建的UI控件蓝图或Slate控件类模板,引擎会通过.Target.cs配置文件自动处理依赖关系。

▶ 示例

假设项目存在以下任一文件类型时,隐式依赖将被建立:

文件类型 隐式触发机制

.uasset控件蓝图

打包时会自动添加必要模块

.h头文件包含Slate.h

编译前扫描H文件自动添加模块依赖

▶ 为什么应显式声明?

虽然隐式依赖可能让你跳过模块声明,但存在 三大隐患

  1. 平台兼容性风险
    在非Windows平台(如Android/iOS)打包时,缺乏显式声明可能导致链接错误

  2. 热重载失效
    对Slate控件的实时修改可能无法通过Live Coding及时生效

  3. 团队协作问题
    其他成员获取代码库后可能出现"unresolved external symbol"错误


▶ 建议的规范写法

无论如何都应该按官方推荐显式声明:

// YourProject.Build.cs
PublicDependencyModuleNames.AddRange(new string[] {
    "Core",
    "Slate",        // 必需
    "SlateCore",    // 必需
    "UMG"           // 可选(如果使用控件蓝图)
});

可以通过以下命令验证实际依赖链(需要在项目目录执行):

# 生成完整的模块关系图
engine\Build\BatchFiles\GenerateProjectFiles.bat -graph=Modules

类名 A U G S T 

关于带A的类名:
- AMyHUD 开头的 `A` 表示这是一个 Actor 类(比如角色、道具等会出现在游戏场景中的对象)
- 这是虚幻引擎的命名规则:
  - A开头 → Actor 相关类(如 ACharacter 角色)
  - U开头 → 继承自 UObject 的类(如 UWidget  UI控件)
  - S开头 → Slate 控件类(如你代码中的 SMyCompoundWidget)

- G 开头的变量通常是全局单例或核心系统入口点。

	if (GEngine && GEngine->GameViewport && MyCompoundWidget)//判断这三个都是合法的

-T开头的变量

容器类 TArray<T> TMap<K,V>   哈希表实现 TSet<T>        无序集合
智能指针 TSharedPtr<T> TWeakPtr<T> TUniquePtr<T>
...

关于这两个私有变量:

private:
    TSharedPtr<SMyCompoundWidget> MyCompoundWidget;   // 你自己设计的UI(比如由图片+文字+按钮组合成的血条界面)
    TSharedPtr<SWeakWidget> WidgetContainer;          // 相当于"装UI盒子的展示架"

- 它们的作用:
  1. MyCompoundWidget:你自己设计的UI(比如由图片+文字+按钮组合成的血条界面)
  2. WidgetContainer:用来把这个UI安全地显示在游戏画面上


- 假设你在搭积木:
  - A开头的类 → 整个积木模型(比如一个房子)
  - S开头的类 → 积木的零件(比如一扇窗户、一个门)
  - 这两个变量 → 你把设计好的窗户(`MyCompoundWidget`)装到房子的展示架上(`WidgetContainer`)

UserWidget 转为 SWidget

UserWidget 转为 SWidget 的主要作用在于 深度定制 UI 或优化性能,常见于以下场景:


1. 底层控制与扩展性

  • Slate(SWidget) 是 Unreal Engine 的底层 UI 框架,直接操作 

    SWidget 可:

    • 实现 UMG 不支持的复杂交互或渲染逻辑(如自定义绘制、高级动画)。
    • 创建 高度定制化的控件(例如特殊形状按钮、动态图表)。

2. 性能优化

  • UMG(UserWidget) 基于 Slate 但附加了蓝图支持,可能引入开销:
    • 转换为 SWidget可减少蓝图虚拟机(VM)的调用,提升高频更新 UI(如游戏 HUD)的性能
    • 避免 UMG 的层级嵌套损耗,直接通过 Slate 的轻量级结构优化渲染。

3. 混合使用 UMG 与 Slate

  • 当需要在 UMG 界面中嵌入 Slate 控件(如自定义编辑器工具、插件界面),需将 

    UserWidget转为 SWidget实现无缝集成。


4. 跨模块复用

  • SWidget是纯 C++ 实现,不依赖 UObject 系统,适合在非游戏模块(如编辑器插件、工具链)中复用 UI 组件。


建议结合项目需求权衡选择:UMG 适合快速开发,Slate 适合高性能/高定制化场景

SWidget 实现深度定制

以下是 通过 SWidget 实现深度定制 的具体示例及其实现原理:


案例1. 自定义渲染 - 动态圆形进度条

需求:实现一个可动态变化的环形进度条,内圈带粒子光效
UMG 限制:ProgressBar控件仅支持矩形/基础圆形
Slate 方案

class SRadialProgress : public SCompoundWidget {
    // 重写绘制逻辑
    virtual int32 OnPaint(...) const override {
        // 1. 使用Slate绘制API画外圈圆环
        FSlateDrawElement::MakeArc(...); 
        
        // 2. 根据ProgressValue计算弧度并绘制填充弧
        float Angle = FMath::Lerp(0, 360, ProgressValue); 
        FSlateDrawElement::MakeArc(...); 
        
        // 3. 添加动态材质实例实现内圈光效
        UMaterialInstanceDynamic* GlowMaterial = ...;
        FSlateDrawElement::MakeMaterial(...); 
    }
    
    // 暴露属性绑定
    void SetProgress(float InProgress) { 
        ProgressValue = FMath::Clamp(InProgress, 0, 1);
    }
};

案例2. 高级交互 - 技能拖拽组合系统

需求:拖拽技能图标到战场网格时,实时显示技能范围预览
UMG 限制:OnDragDetected事件无法精细控制拖拽过程
Slate 方案

class SSkillDragWidget : public SCompoundWidget {
    // 重写拖拽响应
    virtual FReply OnMouseButtonDown(...) override {
        return FReply::Handled().DetectDrag(SharedThis(this), EKeys::LeftMouseButton);
    }

    virtual FReply OnDragDetected(...) override {
        // 1. 创建拖拽操作代理
        TSharedPtr<FDragSkillOperation> DragOp = ...;
        
        // 2. 实时计算鼠标位置对应的战场坐标
        DragOp->SetOnDragged(FOnDrag::CreateLambda([this](...){
            FVector2D GridPos = CalculateGridPosition(MousePos);
            UpdateRangePreview(GridPos); // 更新范围网格材质
        }));
        
        return FReply::Handled().BeginDragDrop(DragOp.ToSharedRef());
    }
};

案例3. 动态布局 - 战利品掉落散开效果

需求:物品掉落时随机散开并缓动归位
UMG 限制:CanvasPanel布局计算开销大
Slate 方案

class SLootSpreadPanel : public SCompoundWidget {
    virtual void Tick(...) override {
        // 1. 为每个子控件计算缓动位置
        for (auto& Child : Children) {
            FVector2D TargetPos = GetGridPosition(Child.Index);
            Child.CurrentPos = FMath::VInterpTo(Child.CurrentPos, TargetPos, DeltaTime, 0.2f);
        }
    }

    virtual void OnArrangeChildren(...) const override {
        // 2. 自定义布局算法
        for (int32 i=0; i<Children.Num(); ++i) {
            const FVector2D& Pos = Children[i].CurrentPos;
            ArrangeChild(Children[i], AllottedGeometry.MakeChild(ChildSize, FSlateLayoutTransform(Pos)));
        }
    }
};

案例4. 复合控件 - 编辑器节点图

需求:构建类似Blueprint编辑器的可缩放/连接节点图
Slate 优势:直接操作SGraphEditor等底层视图组件

class SNodeEditor : public SCompoundWidget {
    void Construct(...) {
        // 1. 创建画布和缩放控制器
        GraphEditor = SNew(SGraphEditor)
            .GraphObj(NodeGraph)
            .ZoomToFit(true);

        // 2. 添加自定义节点工厂
        GraphEditor->GetNodeFactory()->RegisterFactory(MakeShared<FCustomNodeFactory>());

        // 3. 实现节点连线逻辑
        GraphEditor->SetOnCreateConnection(FOnCreateConnection::CreateLambda(...));
    }
};

案例5. 性能敏感场景 - MMO血条系统

需求:同时渲染200+个动态血条且保持60FPS
优化策略

// 自定义SHealthBarWidget批量渲染
virtual int32 OnPaint(...) const override {
    // 1. 合并所有血条的几何体数据
    TArray<FSlateVertex> BatchVertices;
    TArray<SlateIndex> BatchIndices;
    
    for (const FHealthBarData& Data : Instances) {
        // 2. 单次绘制调用渲染全部实例
        GenerateHealthBarGeometry(Data, BatchVertices, BatchIndices);
    }
    
    // 3. 提交合并后的绘制指令
    FSlateDrawElement::MakeCustomVerts(...);
}

这些案例展示了如何通过 直接操作SWidget 突破UMG的限制,实现引擎未内置的复杂UI效果。但需注意:此类深度定制通常需要 10-20倍于UMG 的开发时间,建议仅在核心功能需要时采用。

公有模块和私有模块


1. 常规配置模式:
   - Public 依赖 < 一般不超过 10 个
   - Private 依赖 < 建议保持在 3-5 个内

2.用一个简单的比喻:

🌰 假设我们有两个邻居:

  • 你家(你的模块)
  • 邻居家(其他模块)

情况一:Public 依赖(公开借东西)

PublicDependencyModuleNames.Add("电钻"); // 公开说借了电钻
  1. 邻居来你家串门时 👉 自动知道可以找你借电钻
  2. 邻居家装修时 👉 必须也安装电钻插座(强制依赖)
  3. 所有邻居的邻居 👉 都会知道你有电钻

情况二:Private 依赖(悄悄借东西)

PrivateDependencyModuleNames.Add("梯子"); // 悄悄借了梯子
  1. 邻居来你家串门时 👉 完全不知道你有梯子
  2. 邻居家装修时 👉 不需要安装梯子挂钩(无依赖)
  3. 只有你自己 👉 在阁楼维修时才会用梯子

真实代码示例

假设你在做《厨房模拟器》:

// 公有依赖 (所有用你模块的人都需要)
PublicDependencyModuleNames.Add("锅具系统"); // 其他模块需要调用你的炒菜接口

// 私有依赖 (只有你自己需要)
PrivateDependencyModuleNames.Add("洗碗机"); // 内部自动洗碗的实现细节

👉 结果:

  • 当其他开发者调用你的 炒菜()接口时,必须安装"锅具系统"
  • 但没人知道你用了"洗碗机"实现洗碗功能
  • 如果错误地把"洗碗机"放在 Public,会导致所有使用你模块的项目被迫安装洗碗机插件