实战:计分板系统
经过前七节的学习,你已经掌握了事件、组件、自定义事件、通信和 UI 脚本交互。现在是时候把它们全部串起来,完成一个完整的实战功能——为番茄空岛模组实现一个绿宝石计分板,实时显示在 HUD 上。
你将学到
- 用
InventoryItemChangedServerEvent检测物品变化 - 用
SetCommand操作原版计分板 - 用
ModAttr将计分数据同步到客户端 - 用 HUD 界面实时显示绿宝石数量
- 完整的"事件 → 逻辑 → 通信 → UI"链路
功能目标
在番茄空岛模组中,玩家的通关条件是攒够 10 颗绿宝石。我们要实现:
- 服务端:监听物品变化,实时统计每个玩家的绿宝石数量
- 计分板:用原版 scoreboard 命令记录分数,方便命令方块等系统读取
- 客户端 HUD:在屏幕上方常驻显示"绿宝石:3 / 10"
- 通关检测:达到 10 颗时发送祝贺消息
整体数据流如下:
物品变化事件 服务端逻辑 客户端 HUD
│ │ │
│ InventoryItemChanged │ │
├─────────────────────→ │ 遍历背包统计绿宝石 │
│ │ SetCommand 更新计分板│
│ │ ModAttr 设置分数 │
│ ├────────────────────→ │ RegisterUpdateFunc
│ │ │ 更新 label 文本第一步:设计 UI 布局
首先在资源包中创建一个简单的 HUD 界面,用来显示绿宝石数量。
在 resource_pack_xxx/ui/ 目录下创建 scoreboard_hud.json:
{
"namespace": "scoreboard_hud",
"main": {
"type": "screen",
"controls": [
{ "score_panel@scoreboard_hud.score_panel": {} }
]
},
"score_panel": {
"type": "panel",
"size": [120, 24],
"anchor_from": "top_middle",
"anchor_to": "top_middle",
"offset": [0, 8],
"controls": [
{ "bg@scoreboard_hud.score_bg": {} },
{ "label@scoreboard_hud.score_label": {} }
]
},
"score_bg": {
"type": "image",
"texture": "textures/ui/Black",
"alpha": 0.5,
"size": ["100%", "100%"]
},
"score_label": {
"type": "label",
"text": "绿宝石:0 / 10",
"color": [0.33, 1.0, 0.5],
"font_size": "normal",
"anchor_from": "center",
"anchor_to": "center",
"size": ["default", "default"]
}
}这个布局非常简单:屏幕顶部居中放一个带半透明黑底的文本标签。
控件路径
从 main 画布到 score_label 的控件路径是 /score_panel/label。注意 JSON 中的 "label@scoreboard_hud.score_label" 引用,@ 前面的 label 才是控件在父容器中的名称。
第二步:编写服务端逻辑
服务端负责三件事:监听物品变化、统计绿宝石数量、同步数据。
ServerSystem.py——在 Scripts/server/ 目录下编写:
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
ServerSystem = serverApi.GetServerSystemCls()
CompFactory = serverApi.GetEngineCompFactory()
class TomatoServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.emeraldCounts = {} # {playerId: int} 缓存每个玩家的绿宝石数
self._ListenEvents()
self._InitScoreboard()
def _ListenEvents(self):
# 监听背包物品变化
self.ListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"InventoryItemChangedServerEvent",
self, self.OnInventoryChanged
)
# 监听玩家加入
self.ListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"AddServerPlayerEvent",
self, self.OnPlayerJoin
)
def _InitScoreboard(self):
"""创建计分板记分项(如果不存在)"""
levelId = serverApi.GetLevelId()
cmdComp = CompFactory.CreateCommand(levelId)
cmdComp.SetCommand("/scoreboard objectives add emeralds dummy 绿宝石")
# ---- 事件回调 ----
def OnPlayerJoin(self, args):
playerId = args["id"]
self.emeraldCounts[playerId] = 0
# 初始化该玩家的计分板分数
levelId = serverApi.GetLevelId()
cmdComp = CompFactory.CreateCommand(levelId)
cmdComp.SetCommand(
"/scoreboard players set @s emeralds 0",
playerId
)
# 初始同步到客户端
self._SyncToClient(playerId, 0)
def OnInventoryChanged(self, args):
playerId = args["playerId"]
newItem = args["newItemDict"]
oldItem = args["oldItemDict"]
# 快速过滤:只关心绿宝石相关的槽位变化
isEmeraldNew = newItem and newItem.get("itemName") == "minecraft:emerald"
isEmeraldOld = oldItem and oldItem.get("itemName") == "minecraft:emerald"
if not isEmeraldNew and not isEmeraldOld:
return
# 重新统计该玩家所有绿宝石
count = self._CountEmeralds(playerId)
oldCount = self.emeraldCounts.get(playerId, 0)
if count == oldCount:
return # 数量没变,不重复同步
self.emeraldCounts[playerId] = count
self._UpdateScoreboard(playerId, count)
self._SyncToClient(playerId, count)
# 通关检测
if count >= 10 and oldCount < 10:
self._OnWin(playerId)
# ---- 辅助方法 ----
def _CountEmeralds(self, playerId):
"""遍历背包统计绿宝石总数"""
itemComp = CompFactory.CreateItem(playerId)
items = itemComp.GetPlayerAllItems(
serverApi.GetMinecraftEnum().ItemPosType.INVENTORY
)
total = 0
if items:
for item in items:
if item and item.get("itemName") == "minecraft:emerald":
total += item.get("count", 0)
return total
def _UpdateScoreboard(self, playerId, count):
"""用命令更新原版计分板"""
levelId = serverApi.GetLevelId()
cmdComp = CompFactory.CreateCommand(levelId)
cmdComp.SetCommand(
"/scoreboard players set @s emeralds {}".format(count),
playerId
)
def _SyncToClient(self, playerId, count):
"""通过 ModAttr 同步到客户端"""
attrComp = CompFactory.CreateModAttr(playerId)
attrComp.SetAttr("emeraldCount", count)
def _OnWin(self, playerId):
"""玩家达成通关条件"""
levelId = serverApi.GetLevelId()
cmdComp = CompFactory.CreateCommand(levelId)
cmdComp.SetCommand(
'/title @s title §a§l恭喜通关!',
playerId
)
cmdComp.SetCommand(
'/title @s subtitle §e你收集了 10 颗绿宝石',
playerId
)InventoryItemChangedServerEvent 的触发时机
这个事件在物品即将变化时触发,此时槽位中仍然是旧物品。但因为我们用 GetPlayerAllItems 重新遍历整个背包来统计,所以需要注意:单次移动/堆叠操作可能触发多次事件(顺序不保证)。
我们的解决方案是:用 emeraldCounts 字典缓存上一次的数量,只在数量真正变化时才同步,避免重复推送。
关键设计解读
| 设计 | 原因 |
|---|---|
用 InventoryItemChangedServerEvent 而非 ActorAcquiredItemServerEvent | 前者覆盖所有来源(捡起、交易、合成、命令给予等),后者对交易只触发客户端事件 |
用 GetPlayerAllItems 全量统计而非增量计算 | 物品堆叠/拆分会触发多次事件且顺序不确定,全量统计更可靠 |
用 SetCommand 写计分板 | ModSDK 没有直接设置计分板分数的组件 API,只能通过命令 |
用 ModAttr 同步而非自定义事件 | ModAttr 自动同步,玩家断线重连后也能恢复数据 |
第三步:编写客户端 HUD 逻辑
客户端需要做两件事:创建 HUD 界面、接收 ModAttr 更新文本。
ScoreboardHud.py——在 Scripts/client/ 目录下创建 UI 类:
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
ScreenNode = clientApi.GetScreenNodeCls()
class ScoreboardHudScreen(ScreenNode):
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
def Create(self):
"""界面创建时获取控件引用"""
self.scoreLabel = self.GetBaseUIControl(
"/score_panel/label"
).asLabel()
# 首次显示默认文本
self.scoreLabel.SetText("绿宝石:0 / 10")
def UpdateScore(self, count):
"""外部调用:更新分数文本"""
if self.scoreLabel:
self.scoreLabel.SetText(
"绿宝石:{} / 10".format(count)
)ClientSystem.py——在客户端系统中注册 UI 并监听数据更新:
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
ClientSystem = clientApi.GetClientSystemCls()
CompFactory = clientApi.GetEngineCompFactory()
class TomatoClientSystem(ClientSystem):
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.hudScreen = None
self._ListenEvents()
self._RegisterUI()
def _ListenEvents(self):
self.ListenForEvent(
clientApi.GetEngineNamespace(),
clientApi.GetEngineSystemName(),
"UiInitFinished",
self, self.OnUiReady
)
def _RegisterUI(self):
"""注册 UI(可在 UiInitFinished 之前调用)"""
clientApi.RegisterUI(
"tomato_island",
"scoreboardHud",
"Scripts.client.ScoreboardHud.ScoreboardHudScreen"
)
def OnUiReady(self, args):
"""UI 系统就绪后创建 HUD"""
self.hudScreen = clientApi.CreateUI(
"tomato_island",
"scoreboardHud",
{"isHud": 1}
)
if self.hudScreen:
# 注册 ModAttr 更新回调
localPlayerId = clientApi.GetLocalPlayerId()
attrComp = CompFactory.CreateModAttr(localPlayerId)
attrComp.RegisterUpdateFunc(
"emeraldCount",
self.OnEmeraldCountUpdate
)
def OnEmeraldCountUpdate(self):
"""ModAttr 变化时自动调用"""
localPlayerId = clientApi.GetLocalPlayerId()
attrComp = CompFactory.CreateModAttr(localPlayerId)
count = attrComp.GetAttr("emeraldCount", 0)
if self.hudScreen:
self.hudScreen.UpdateScore(count)
def Destroy(self):
self.UnListenForEvent(
clientApi.GetEngineNamespace(),
clientApi.GetEngineSystemName(),
"UiInitFinished",
self, self.OnUiReady
)CreateUI 参数说明
CreateUI 的第三个参数传 {"isHud": 1} 表示创建 HUD 类型界面。HUD 常驻屏幕,不阻挡玩家操作,适合计分板这类持续显示的信息。与之对应的 PushScreen 会打开全屏交互界面,阻挡操作。详见 S7. UI 脚本交互。
第四步:串联完整数据流
现在来回顾从物品变化到 UI 更新的完整链路:
1. 玩家捡起/交易获得绿宝石
│
▼
2. InventoryItemChangedServerEvent 触发
│
▼
3. OnInventoryChanged 过滤非绿宝石变化
│
▼
4. _CountEmeralds 遍历背包统计总数
│
▼
5. _UpdateScoreboard 用 SetCommand 写入原版计分板
│
▼
6. _SyncToClient 用 ModAttr.SetAttr 设置 emeraldCount
│
▼ (引擎自动同步)
▼
7. OnEmeraldCountUpdate 被自动调用
│
▼
8. hudScreen.UpdateScore(count) 更新 HUD 文本为什么用两套系统?
你可能会问:既然已经有 ModAttr 同步数据了,为什么还要用原版计分板?
| 系统 | 用途 | 优势 |
|---|---|---|
| 原版计分板 | 命令方块 / 目标选择器可以读取 | 可用 @a[scores={emeralds=10..}] 做条件判断 |
| ModAttr | 同步到客户端 UI | 自动同步、断线重连恢复、客户端可直接读取 |
两者配合使用:计分板给原版命令系统用,ModAttr 给 UI 脚本用。
计分板 API 速查
ModSDK 的计分板相关 API 非常有限,这里做一个完整整理:
读取 API(组件方式)
| API | 端 | 说明 |
|---|---|---|
CreateGame(levelId).GetAllScoreboardObjects() | 服务端 / 客户端 | 获取所有记分项列表 |
CreateGame(levelId).GetAllPlayerScoreboardObjects() | 服务端 / 客户端 | 获取所有玩家的计分板分数 |
写入操作(只能用命令)
| 操作 | 命令 |
|---|---|
| 创建记分项 | /scoreboard objectives add <名称> dummy [显示名] |
| 删除记分项 | /scoreboard objectives remove <名称> |
| 设置分数 | /scoreboard players set <玩家> <记分项> <分数> |
| 增加分数 | /scoreboard players add <玩家> <记分项> <分数> |
| 减少分数 | /scoreboard players remove <玩家> <记分项> <分数> |
| 重置分数 | /scoreboard players reset <玩家> [记分项] |
| 侧边栏显示 | /scoreboard objectives setdisplay sidebar <记分项> |
所有写入操作都通过 CompFactory.CreateCommand(levelId).SetCommand(cmd) 执行。
客户端读取限制
客户端的 GetAllScoreboardObjects() 和 GetAllPlayerScoreboardObjects() 只能读取到已设置侧边栏显示的记分项。如果需要读取未显示在侧边栏的记分项,必须在服务端操作。
没有计分板变化事件
ModSDK 没有提供计分板分数变化的事件。如果你需要响应分数变化,有两种方案:
- 推荐:像本教程一样,自己管理数据源,所有分数变更都经过你的代码
- 轮询
GetAllPlayerScoreboardObjects()定时检查(不推荐,性能差)
文件结构总结
behavior_pack_xxx/
├── Scripts/
│ ├── server/
│ │ └── ServerSystem.py # 监听物品变化 + 计分板 + ModAttr
│ └── client/
│ ├── ClientSystem.py # 注册/创建 HUD + ModAttr 回调
│ └── ScoreboardHud.py # ScreenNode 子类,更新文本
resource_pack_xxx/
└── ui/
└── scoreboard_hud.json # HUD 布局:顶部居中文本标签扩展思路
掌握了核心链路后,你可以轻松扩展更多功能:
| 扩展 | 思路 |
|---|---|
| 多记分项 | 在 ModAttr 中用字典存储 {"emeralds": 3, "kills": 5},HUD 显示多行 |
| 排行榜 | 服务端用 GetAllPlayerScoreboardObjects() 排序,用 BroadcastToAllClient 广播 |
| 进度条 | 将 score_label 替换为进度条控件(fill 类型 image),根据比例设置宽度 |
| 击杀计分 | 监听 MobDieEvent,检查 attacker 是否为玩家,累加击杀数 |
| 交易计分 | 监听 InventoryItemChangedServerEvent 已经覆盖——交易获得的物品也会触发此事件 |
击杀计分示例
def OnMobDie(self, args):
attackerId = args.get("attacker", "")
dieEntityId = args["id"]
if not attackerId:
return
# 检查死亡实体是否为番茄鸡
engineComp = CompFactory.CreateEngineType(dieEntityId)
engineType = engineComp.GetEngineTypeStr()
if engineType == "tomato_island:tomato_chicken":
# 给击杀者加分
self.killCounts[attackerId] = self.killCounts.get(attackerId, 0) + 1小结
| 要点 | 说明 |
|---|---|
| 物品检测 | InventoryItemChangedServerEvent 覆盖所有物品来源(捡起、交易、合成等) |
| 全量统计 | 用 GetPlayerAllItems 遍历背包统计,避免增量计算被多次事件干扰 |
| 计分板写入 | ModSDK 无组件 API,必须用 SetCommand 执行 /scoreboard 命令 |
| 计分板读取 | GetAllScoreboardObjects() / GetAllPlayerScoreboardObjects(),客户端有侧边栏限制 |
| 数据同步 | ModAttr.SetAttr → 自动同步 → 客户端 RegisterUpdateFunc 被动接收 |
| HUD 创建 | CreateUI + isHud=1,在 UiInitFinished 之后调用 |
| 双系统配合 | 原版计分板给命令系统用,ModAttr 给 UI 脚本用 |
| 通关检测 | 服务端权威判定,只在分数首次达标时触发 |
接下来我们将学习调试与热更新——掌握日志打印、测试窗口和热更新技巧,让脚本开发效率倍增。
