Skip to content

UI 脚本交互 ​

在 S6. 制作 UI 界面 中,我们用编辑器创建了一个任务面板的界面布局。但那只是静态布局——按钮点不了,文字不会变。

要让 UI 真正"活"起来,需要用 Python 脚本来控制。本节将教你完整的 UI 脚本交互流程。

你将学到

  • UI 的完整生命周期:注册 → 创建 → 使用 → 销毁
  • 继承 ScreenNode 编写 UI 逻辑类
  • 获取控件引用(GetBaseUIControl)并修改属性
  • 监听按钮点击事件
  • 动态更新文本内容
  • 结合跨端通信实现 UI 数据更新

UI 生命周期 ​

UI 有两种创建方式,适用于不同场景:

RegisterUI        注册 UI 类(只需一次)
    │
    ├──→ CreateUI       创建 HUD 界面(常驻屏幕,不阻挡操作)
    │
    └──→ PushScreen     创建自定义界面(全屏/弹窗,阻挡操作,按 ESC 可关闭)
              │
              ▼
         ScreenNode     UI 逻辑类
           ├── Create()       初始化时调用
           ├── Update()       每帧调用(慎用,注意性能)
           ├── OnActive()     界面激活时调用
           └── OnDeactive()   界面失活时调用
              │
              ├──→ SetRemove      销毁 CreateUI 创建的 HUD
              └──→ PopScreen      关闭 PushScreen 创建的界面
方式适用场景特点
CreateUIHUD 元素(血条、任务面板、小地图、计分板)常驻屏幕,不阻挡游戏操作,需要手动 SetRemove 销毁
PushScreen自定义界面(商店、背包、设置菜单、对话框)进入 UI 栈,阻挡游戏操作,按 ESC 自动关闭,用 PopScreen 销毁

选择建议

  • 不需要阻挡玩家操作的常驻信息展示 → 用 CreateUI(HUD 模式)
  • 需要阻挡玩家操作的交互界面 → 用 PushScreen(界面栈模式)

混用会导致问题:用 CreateUI 创建交互界面,玩家无法按 ESC 关闭;用 PushScreen 创建 HUD,会阻挡玩家移动和操作。

第一步:编写 UI 逻辑类 ​

所有 UI 逻辑类都必须继承 ScreenNode。在 Scripts/client/ 目录下创建 TaskPanelScreen.py:

python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi

ScreenNode = clientApi.GetScreenNodeCls()

class TaskPanelScreen(ScreenNode):

    def __init__(self, namespace, name, param):
        ScreenNode.__init__(self, namespace, name, param)
        self.mEmeraldCount = 0

    def Create(self):
        """UI 创建完成后调用,在这里初始化控件引用"""
        print "====== 任务面板 UI 已创建 ======"

        # 获取控件引用
        self.mTitleLabel = self.GetBaseUIControl("/taskBg/titleText").asLabel()
        self.mDescLabel = self.GetBaseUIControl("/taskBg/descText").asLabel()
        self.mProgressLabel = self.GetBaseUIControl("/taskBg/progressText").asLabel()
        self.mCloseBtn = self.GetBaseUIControl("/taskBg/closeButton").asButton()

        # 设置初始文本
        self.mTitleLabel.SetText("当前任务")
        self.mDescLabel.SetText("收集 10 颗绿宝石")
        self.UpdateProgress(0)

        # 监听按钮点击
        # isSwallow=True:点击事件被按钮吞噬,不会穿透到下层控件或游戏世界
        self.mCloseBtn.AddTouchEventParams({"isSwallow": True})
        self.mCloseBtn.SetButtonTouchUpCallback(self.OnCloseClick)

    def UpdateProgress(self, count):
        """更新进度文本"""
        self.mEmeraldCount = count
        self.mProgressLabel.SetText("进度: %d / 10" % count)

    def OnCloseClick(self, args):
        """关闭按钮点击回调"""
        self.SetScreenVisible(False)  # 隐藏面板

    def Destroy(self):
        """UI 销毁时调用"""
        print "====== 任务面板 UI 已销毁 ======"

关键 API 说明 ​

API说明
GetBaseUIControl(path)通过路径获取控件对象,路径从画布的直接子控件开始,用 / 分隔
.asLabel()将控件转为文本控件对象,可使用 SetText()
.asButton()将控件转为按钮控件对象,可设置点击回调
.asImage()将控件转为图片控件对象,可使用 SetSprite()
SetButtonTouchUpCallback(func)设置按钮松开时的回调函数
AddTouchEventParams({"isSwallow": True})设置按钮吞噬点击事件(防止穿透到游戏)
SetScreenVisible(bool)显示或隐藏整个 UI
SetVisible(bool)显示或隐藏单个控件(在 BaseUIControl 上调用)

控件路径格式

路径从画布(main)的直接子控件开始,不包含 main 本身。例如你的控件结构是:

main
└── taskBg
    ├── titleText
    └── closeButton

则 titleText 的路径是 /taskBg/titleText,closeButton 的路径是 /taskBg/closeButton。

第二步:注册和创建 UI ​

方式一:CreateUI 创建 HUD(任务面板示例) ​

任务面板是常驻屏幕的 HUD 元素,不阻挡游戏操作,使用 CreateUI:

python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi

ClientSystem = clientApi.GetClientSystemCls()

class TomatoIslandClientSystem(ClientSystem):

    def __init__(self, namespace, systemName):
        ClientSystem.__init__(self, namespace, systemName)
        self.mTaskPanelNode = None
        self.ListenEvents()

    def ListenEvents(self):
        self.ListenForEvent(
            clientApi.GetEngineNamespace(),
            clientApi.GetEngineSystemName(),
            "UiInitFinished",
            self,
            self.OnUIInitFinished
        )

    def OnUIInitFinished(self, args):
        """引擎 UI 系统初始化完成后创建 HUD"""
        # 1. 注册 UI(只需一次,RegisterUI 可以在 UiInitFinished 之前调用)
        clientApi.RegisterUI(
            "TomatoIslandMod",                                 # Mod 命名空间
            "taskPanel",                                        # UI 名称
            "Scripts.client.TaskPanelScreen.TaskPanelScreen",  # UI 类的完整路径
            "taskPanel.main"                                    # screenDef:namespace.screenName(JSON 中的命名空间.画布名)
        )

        # 2. 用 CreateUI 创建 HUD(isHud=1)
        clientApi.CreateUI(
            "TomatoIslandMod",
            "taskPanel",
            {"isHud": 1}    # HUD 模式:不阻挡游戏操作
        )

        # 3. 获取 UI 节点引用
        self.mTaskPanelNode = clientApi.GetUI("TomatoIslandMod", "taskPanel")

    def Destroy(self):
        # HUD 用 SetRemove 销毁
        if self.mTaskPanelNode:
            self.mTaskPanelNode.SetRemove()
            self.mTaskPanelNode = None
        self.UnListenForEvent(
            clientApi.GetEngineNamespace(),
            clientApi.GetEngineSystemName(),
            "UiInitFinished",
            self,
            self.OnUIInitFinished
        )

方式二:PushScreen 创建自定义界面(商店示例) ​

商店界面需要阻挡操作,让玩家专注于交易,使用 PushScreen:

python
def OpenShop(self):
    """打开商店界面"""
    # 注册可以提前做(比如在 __init__ 或 OnUIInitFinished 中),只需一次
    clientApi.RegisterUI(
        "TomatoIslandMod",
        "shopScreen",
        "Scripts.client.ShopScreen.ShopScreen",
        "shopScreen.main"       # screenDef:namespace.screenName
    )

    # 用 PushScreen 创建界面(进入 UI 栈,阻挡操作)
    clientApi.PushScreen(
        "TomatoIslandMod",
        "shopScreen"
    )

def CloseShop(self):
    """关闭商店界面"""
    # 传入屏幕名称,确保弹出的是正确的界面(不传参数会弹出栈顶,可能弹错)
    node = clientApi.GetUI("TomatoIslandMod", "shopScreen")
    if node:
        node.SetScreenVisible(False)
        node.SetRemove()

PopScreen 的陷阱

clientApi.PopScreen() 不传参数时弹出的是栈顶屏幕。如果在你的商店界面打开后,引擎或其他 Mod 又 Push 了一个界面(比如暂停菜单),调用无参 PopScreen() 会弹错界面。

更安全的做法是通过 GetUI 获取指定界面的 ScreenNode 实例,调用 SetRemove() 精确销毁。

PushScreen vs CreateUI 总结

python
# HUD(常驻信息展示)
clientApi.CreateUI(ns, name, {"isHud": 1})   # 创建
node.SetRemove()                              # 销毁

# 自定义界面(交互弹窗)
clientApi.PushScreen(ns, name)                # 创建(入栈,ESC 可关闭)
node = clientApi.GetUI(ns, name)              # 获取指定界面
node.SetRemove()                              # 精确销毁(比无参 PopScreen 更安全)

注册参数详解 ​

python
clientApi.RegisterUI(namespace, uiName, uiClassPath, screenDef)
参数说明示例
namespaceMod 命名空间"TomatoIslandMod"
uiNameUI 的唯一名称"taskPanel"
uiClassPathScreenNode 子类的完整路径"Scripts.client.TaskPanelScreen.TaskPanelScreen"
screenDef界面定义:JSON文件名.画布名"taskPanel.main"

创建时机

RegisterUI 可以在 UiInitFinished 之前调用(比如在 ClientSystem.__init__ 中)。但 CreateUI 和 PushScreen 必须在 UiInitFinished 事件触发之后调用,否则 UI 系统尚未初始化,创建会失败。

第三步:结合跨端通信更新 UI ​

UI 运行在客户端,但数据(绿宝石数量)存在服务端。我们用 S5 学到的 ModAttr 来同步数据到 UI。

ServerSystem.py——当绿宝石数量变化时更新 ModAttr:

python
def OnEmeraldChanged(self, playerId, newCount):
    comp = serverApi.GetEngineCompFactory().CreateModAttr(playerId)
    comp.SetAttr("emerald_count", newCount)   # 自动同步到客户端

ClientSystem.py——监听 ModAttr 变化并更新 UI:

python
def OnUIInitFinished(self, args):
    # ...(注册和创建 UI 的代码同上)

    # 注册 ModAttr 变化监听
    playerId = clientApi.GetLocalPlayerId()
    comp = clientApi.GetEngineCompFactory().CreateModAttr(playerId)
    comp.RegisterUpdateFunc("emerald_count", self.OnEmeraldCountChanged)

def OnEmeraldCountChanged(self, data):
    newCount = data["newValue"]
    # 更新 UI
    if self.mTaskPanelNode:
        self.mTaskPanelNode.UpdateProgress(newCount)

完整的数据流 ​

服务端:绿宝石数量变化
    │
    ▼  ModAttr.SetAttr("emerald_count", 5)    ← 自动同步
    │
客户端:RegisterUpdateFunc 触发回调
    │
    ▼  self.mTaskPanelNode.UpdateProgress(5)
    │
UI:mProgressLabel.SetText("进度: 5 / 10")

第四步:显示和隐藏 UI ​

通过按钮切换 ​

在 TaskPanelScreen 中已经实现了关闭按钮。如果需要重新打开,可以在 ClientSystem 中提供一个方法:

python
def ToggleTaskPanel(self):
    if self.mTaskPanelNode:
        visible = not self.mTaskPanelNode.GetScreenVisible()
        self.mTaskPanelNode.SetScreenVisible(visible)

通过服务端事件触发 ​

比如玩家进入特定区域时显示任务面板:

python
# ServerSystem.py —— 服务端检测到玩家进入任务区域
def OnPlayerEnterArea(self, playerId):
    self.NotifyToClient(playerId, "ShowTaskPanel", {})

# ClientSystem.py —— 客户端收到通知后显示面板
def OnShowTaskPanel(self, args):
    self.ToggleTaskPanel()

触发方式建议

推荐通过服务端事件或 UI 内的按钮来控制界面显隐,而不是劫持原版按键(如跳跃键)。劫持原版按键会影响正常游戏操作,容易让玩家困惑。

:::

常用控件操作速查 ​

操作代码
获取控件ctrl = self.GetBaseUIControl("/path/to/ctrl")
转为文本label = ctrl.asLabel()
设置文本label.SetText("内容")
转为图片img = ctrl.asImage()
设置图片img.SetSprite("textures/ui/xxx")
转为按钮btn = ctrl.asButton()
按钮回调btn.SetButtonTouchUpCallback(self.OnClick)
吞噬点击btn.AddTouchEventParams({"isSwallow": True})
显示/隐藏控件ctrl.SetVisible(True)
显示/隐藏整个 UIself.SetScreenVisible(True)
获取子控件child = ctrl.GetChildByName("childName")
按路径获取子控件child = ctrl.GetChildByPath("/a/b/c")
克隆控件self.Clone("/source/path", "/parent/path")
移除子控件self.RemoveChildControl("/parent/path", ctrl)

文件结构总结 ​

behavior_pack_xxx/
└── Scripts/
    ├── modMain.py
    ├── server/
    │   └── ServerSystem.py
    └── client/
        ├── ClientSystem.py
        └── TaskPanelScreen.py       ← UI 逻辑类

resource_pack_xxx/
├── ui/
│   └── taskPanel.json               ← 界面编辑器生成的布局文件
└── textures/
    └── ui/
        └── ...                       ← UI 用到的贴图

小结 ​

要点说明
HUD 创建CreateUI(ns, name, {"isHud": 1}) → 常驻屏幕,不阻挡操作,SetRemove() 销毁
界面创建PushScreen(ns, name) → 进入 UI 栈,阻挡操作,PopScreen() 或 ESC 关闭
UI 类继承 ScreenNode,重写 Create() 和 Destroy()
获取控件GetBaseUIControl("/path") + .asLabel() / .asButton() / .asImage()
修改文本label.SetText("内容")
按钮回调btn.SetButtonTouchUpCallback(func) + AddTouchEventParams
创建时机RegisterUI 可提前调用;CreateUI / PushScreen 必须在 UiInitFinished 之后
数据更新服务端 ModAttr → 客户端 RegisterUpdateFunc → ScreenNode 方法

接下来我们将进入实战:计分板系统——综合运用事件、组件、通信和 UI,完成一个完整的功能模块。