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 创建的界面| 方式 | 适用场景 | 特点 |
|---|---|---|
CreateUI | HUD 元素(血条、任务面板、小地图、计分板) | 常驻屏幕,不阻挡游戏操作,需要手动 SetRemove 销毁 |
PushScreen | 自定义界面(商店、背包、设置菜单、对话框) | 进入 UI 栈,阻挡游戏操作,按 ESC 自动关闭,用 PopScreen 销毁 |
选择建议
- 不需要阻挡玩家操作的常驻信息展示 → 用
CreateUI(HUD 模式) - 需要阻挡玩家操作的交互界面 → 用
PushScreen(界面栈模式)
混用会导致问题:用 CreateUI 创建交互界面,玩家无法按 ESC 关闭;用 PushScreen 创建 HUD,会阻挡玩家移动和操作。
第一步:编写 UI 逻辑类
所有 UI 逻辑类都必须继承 ScreenNode。在 Scripts/client/ 目录下创建 TaskPanelScreen.py:
# -*- 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:
# -*- 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:
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 总结
# HUD(常驻信息展示)
clientApi.CreateUI(ns, name, {"isHud": 1}) # 创建
node.SetRemove() # 销毁
# 自定义界面(交互弹窗)
clientApi.PushScreen(ns, name) # 创建(入栈,ESC 可关闭)
node = clientApi.GetUI(ns, name) # 获取指定界面
node.SetRemove() # 精确销毁(比无参 PopScreen 更安全)注册参数详解
clientApi.RegisterUI(namespace, uiName, uiClassPath, screenDef)| 参数 | 说明 | 示例 |
|---|---|---|
namespace | Mod 命名空间 | "TomatoIslandMod" |
uiName | UI 的唯一名称 | "taskPanel" |
uiClassPath | ScreenNode 子类的完整路径 | "Scripts.client.TaskPanelScreen.TaskPanelScreen" |
screenDef | 界面定义:JSON文件名.画布名 | "taskPanel.main" |
创建时机
RegisterUI 可以在 UiInitFinished 之前调用(比如在 ClientSystem.__init__ 中)。但 CreateUI 和 PushScreen 必须在 UiInitFinished 事件触发之后调用,否则 UI 系统尚未初始化,创建会失败。
第三步:结合跨端通信更新 UI
UI 运行在客户端,但数据(绿宝石数量)存在服务端。我们用 S5 学到的 ModAttr 来同步数据到 UI。
ServerSystem.py——当绿宝石数量变化时更新 ModAttr:
def OnEmeraldChanged(self, playerId, newCount):
comp = serverApi.GetEngineCompFactory().CreateModAttr(playerId)
comp.SetAttr("emerald_count", newCount) # 自动同步到客户端ClientSystem.py——监听 ModAttr 变化并更新 UI:
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 中提供一个方法:
def ToggleTaskPanel(self):
if self.mTaskPanelNode:
visible = not self.mTaskPanelNode.GetScreenVisible()
self.mTaskPanelNode.SetScreenVisible(visible)通过服务端事件触发
比如玩家进入特定区域时显示任务面板:
# 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) |
| 显示/隐藏整个 UI | self.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,完成一个完整的功能模块。
