Skip to content

Unreal 学习笔记(三)

UI 框架

  • Slate:底层 C++ UI 框架,功能强大但使用复杂
  • UMG (Unreal Motion Graphics):基于 Slate 的可视化 UI 编辑器

Tools - Debug - Widget Reflector 可以查看 UI 层级结构,直接查看运行时 UI 的底层实现层级结构和源代码

UMG 系统

创建方式与常规蓝图类一样。设计器(Designer)提供了 UI 可视化编辑界面。

UMG Animation:按关键帧控制 Widget 属性

开发要点:

  • 信息传达性
  • 交互体验
  • (自适应)布局调整
  • 动画效果
  • 功能实现

C++ & UMG

需要声明一个继承自 UserWidget 的 C++ 类。

C++
UPROPERTY(BlueprintReadOnly, meta=(BindWidget))
UTextBlock* TextBlock_TestA;
// Transient 说明该属性不会保存或者从磁盘加载,不会被网络复制
UPROPERTY(BlueprintReadOnly, Transient, meta=(BindWidgetAnim))
UWidgetAnim* TestAnim;
UPROPERTY(BlueprintReadOnly, meta=(BindWidgetOptional))
UTextBlock* TextBlock_TestOptional;
UPROPERTY(BlueprintReadOnly, Transient, meta=(BindWidgetAnimOptional))
UWidgetAnim* TestAnim_TestOptional;

将 UMG 蓝图绑定到这个 C++ 类后,在 Designer 视图下,(默认左下角)有一个 BindWidgets 面板,可以将看到必须要在编辑器视图里面放入哪些 Widget。

此处的 Widget 变量命名必须与 C++ 声明是一致的。

事件绑定:一般重载 NativeOnInitialized() 在里面绑定事件,推荐判空指针。

C++
void UMyUserWidget::NativeOnInitialized() {
    Super::NativeOnInitialized();
    
    if (!TextBlock_TestA) {
        return;
    }
    TextBlock_TestA->OnTextChanged.AddDynamic(this, &UMyUserWidget::TextBlock_TestA_OnTextChanged);
}

Gameplay 类里面可以直接指定 HUD 类,在 GameMode 被创建时就可以直接自动加载。

小技巧

容器和Slot:可以更改锚点,配置 UI 自适应变化。

刷新函数绑定:虚幻引擎中的UMG属性绑定

函数绑定的方式比较吃性能,如果没那么频繁的情况下可以用事件驱动更新

虚幻引擎中的UMG优化准则:使用事件驱动更新来替代绑定属性

组件+接口 V.S. 全能玩家类

组件模式

核心思想:利用多态来解耦。

方案:使用接口(Interface)或者继承(Inheritance)。

逻辑:攻击者(子弹/敌人)不需要知道对方具体是谁(是Player还是Enemy),只需要知道对方“可以受伤(Damageable)”即可。

优势:

  • 扩展性强:如果你后来加了一个“木箱子”或者“甚至队友”,只要它们实现了接口,战斗逻辑不需要改动一行代码。
  • 符合UE哲学:Unreal Engine 内置的 Gameplay 框架(AActor::TakeDamage, UInterface, UActorComponent)就是为了这种通用性设计的。

劣势:

  • 前期代码量稍大:需要定义接口、实现接口、GetCompoment等。
  • 阅读门槛:对于不熟悉架构的人来说,代码跳转比较绕(Call Interface -> Implementation)。

全能玩家类

核心思想:显式编程,所见即所得。

方案:直接访问成员变量,通过具体的类型判断(Casting/Tag)来写逻辑。

逻辑:Player和Enemy分别写逻辑,反正只有这两种情况,直接 hp -= damage 最直观。

优势:

  • 极速开发:在只有 1 个玩家和 1 种敌人时,这是写得最快的。
  • 逻辑“一眼清”:不用跳代码,逻辑就在碰撞回调函数里写着。

劣势:

  • 高耦合:子弹必须引用 Player 头文件,也要引用 Enemy 头文件。
  • 维护噩梦:如果有 10 种不同的怪物?或者有队友误伤?或者有可破坏场景?你需要写无数个判断类型。
  • 代码复用率低:扣血、判断死亡的逻辑在 Player 和 Enemy 里各写了一遍。

网络同步问题

在单机游戏里,hp -= damage只是改个内存值。

但在网络游戏里,必须要遵循服务器权威原则,客户端不能直接改血量,客户端必须通知服务器。 这需要维护一大堆 RPC 函数,只要忘了一个就会造成血量不同步。

如果使用Component模式,不管是谁打谁,只在服务器端调用HealthComponent的TakeDamage。 这样就只需要在 HealthComponent 里写一次网络同步逻辑,所有角色都自动支持网络同步。

自动同步:UHealthComponent 内部将 CurrentHealth 标记为 ReplicatedUsing = OnRep_CurrentHealth。

信息流:

  • Server: 收到攻击 -> 扣血 -> 引擎自动把 float 变动同步给所有客户端。
  • Client: 收到 OnRep_CurrentHealth 回调 -> 刷新 UI -> 播放受伤动画/特效。
  • 复杂度:只需要在一个地方写一次同步逻辑,全游戏的所有带生命值的Actor都可以同步了。

组件模式详细架构

  • UHealthComponent: 管理生命值,提供 ReceiveDamage(), Heal() 等函数。
  • IDamageable: 定义ReactToDamage()接口
  • AMyPlayer, AEnemy: 实现IDamageable接口,拥有UHealthComponent组件
  • APlayerController: 持有BattleHUD(不能由GameMode持有,只能由各客户端的玩家控制器持有)
  • UBattleHUD:显示生命值,监听 UHealthComponent 的 OnHealthChanged 事件

时序图: