事件系统
上一节我们理解了 Client-Server 架构。但两个 System 类里目前只有 __init__ 和 Destroy——它们什么也做不了。要让 Mod 真正"活"起来,需要用到 Mod SDK 最核心的机制:事件系统。
游戏中发生的一切——玩家进入世界、破坏方块、攻击生物、吃食物——都会以事件的形式通知你的脚本。
你将学到
- 事件系统的四个操作:监听、反监听、发布、响应
ListenForEvent的完整参数含义- 常用的服务端事件和客户端事件
- 回调函数的参数格式和可设置参数
- 完整实战:玩家右键方块时获得钻石剑
事件系统的工作原理
事件系统可以理解为一个广播电台:
游戏引擎(电台) 你的 Mod(听众)
│ │
│ "玩家破坏了方块" │
├──────────────────────→ │ 收到!执行我的逻辑
│ │
│ "生物受到了伤害" │
├──────────────────────→ │ 收到!执行我的逻辑
│ │
│ "玩家发送了聊天消息" │
├──────────────────────→ │ 收到!执行我的逻辑你不需要每帧去检查"玩家是不是破坏了方块"——你只需要注册监听,引擎会在事件发生时主动通知你。
四个核心操作
| 操作 | 函数 | 说明 |
|---|---|---|
| 监听 | ListenForEvent | 注册回调函数到某个事件上 |
| 反监听 | UnListenForEvent | 取消已注册的监听 |
| 发布 | BroadcastEvent / NotifyToServer / NotifyToClient / BroadcastToAllClient | 触发事件通知所有监听者 |
| 响应 | 回调函数 | 事件触发时引擎自动调用你注册的函数 |
其中监听和响应是最常用的——引擎事件(玩家破坏方块、生物死亡等)由引擎自动发布,你只需要监听并响应。
监听事件:ListenForEvent
这是你用得最多的函数。完整写法:
self.ListenForEvent(
namespace, # 事件来源的命名空间
systemName, # 事件来源的系统名称
eventName, # 事件名称(字符串)
instance, # 回调函数所属的实例(通常是 self)
func # 回调函数
)对于引擎事件(游戏内置的事件),namespace 和 systemName 固定写法:
# 服务端监听引擎事件
self.ListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"事件名称",
self,
self.回调函数名
)
# 客户端监听引擎事件
self.ListenForEvent(
clientApi.GetEngineNamespace(),
clientApi.GetEngineSystemName(),
"事件名称",
self,
self.回调函数名
)简化记忆
监听引擎事件就三步:
- 引擎命名空间 + 引擎系统名
- 事件名称字符串
- self + 回调函数
回调函数的格式
回调函数必须接收一个参数(通常是 dict 类型),包含事件的详细信息:
def OnPlayerAttack(self, args):
playerId = args["playerId"] # 攻击的玩家 ID
victimId = args["victimId"] # 被攻击的实体 ID
damage = args["damage"] # 伤害值
print "玩家 %s 攻击了 %s,伤害 %d" % (playerId, victimId, damage)不同事件的 args 中包含不同的字段,具体参见 API 事件文档。
可设置参数
部分事件的回调参数是可设置的——在回调函数中修改它的值可以改变事件的结果。文档中不同事件用不同的措辞描述,常见的写法有:
cancel(bool)—— 设为True可取消该操作continueXxx(bool)—— 设为False可阻止后续逻辑damage(float)—— 部分攻击事件允许修改伤害值
并非所有参数都可修改
每个事件的每个参数是否可修改,需要查看 API 文档中该参数的说明。文档中会明确标注「允许脚本层修改」或「不支持修改 / 修改无效」。不要假设所有参数都能修改。
示例 1:阻止玩家跳跃(ClientJumpButtonPressDownEvent)
def OnPlayerJump(self, args):
# continueJump:设置是否执行跳跃逻辑
args["continueJump"] = False # 阻止本次跳跃示例 2:取消玩家攻击(PlayerAttackEntityEvent,服务端)
def OnPlayerAttack(self, args):
# cancel:是否取消该次攻击,默认不取消
args["cancel"] = True # 取消本次攻击示例 3:修改攻击伤害(PlayerAttackEntityEvent,服务端)
def OnPlayerAttack(self, args):
# damage:引擎传过来的值是 0,允许脚本层修改为其他数
args["damage"] = 20 # 设置伤害为 20
args["isValid"] = 1 # 必须设为 1,表示脚本已设置伤害值注意区分服务端和客户端
同名事件在服务端和客户端的参数可能不同。例如 PlayerAttackEntityEvent:
- 服务端:
damage可修改,有cancel参数 - 客户端:
damage明确标注「修改无效」,无cancel参数
修改参数只在正确的端生效。务必查阅 API 文档确认。
重要规则
不要往引擎事件的回调参数中添加自定义 key! 如果需要扩展数据,先拷贝再操作:
def OnAddEntity(self, data):
# 错误做法:data["myKey"] = "value"
# 正确做法:
myData = {
"id": data["id"],
"myKey": "myValue"
}
self.DoSomething(myData)常用服务端事件
以下是开发中最常用的服务端事件:
| 事件名 | 触发时机 | 常用参数 |
|---|---|---|
AddServerPlayerEvent | 玩家加入游戏 | id(玩家ID) |
DelServerPlayerEvent | 玩家离开游戏 | id、isTransfer |
ServerChatEvent | 玩家发送聊天消息 | username、message、cancel |
PlayerAttackEntityEvent | 玩家攻击实体 | playerId、victimId、damage、cancel |
PlayerDieEvent | 玩家死亡 | id、attacker |
PlayerRespawnEvent | 玩家复活 | id |
DestroyBlockEvent | 方块被玩家破坏 | x、y、z、blockName、playerId |
ServerBlockUseEvent | 玩家右键自定义方块 | playerId、blockName、x、y、z |
ActorHurtServerEvent | 生物受伤 | entityId、cause、damage |
MobDieEvent | 生物死亡 | id、attacker |
OnScriptTickServer | 服务端 Tick(每秒 30 次) | 无 |
常用客户端事件
| 事件名 | 触发时机 | 常用参数 |
|---|---|---|
OnLocalPlayerStopLoading | 玩家进入世界加载完成 | 无 |
AddEntityClientEvent | 客户端创建新实体 | id、posX、posY、posZ、engineTypeStr |
RemoveEntityClientEvent | 实体被移除 | id |
OnScriptTickClient | 客户端 Tick(每秒 30 次) | 无 |
ClientJumpButtonPressDownEvent | 玩家按下跳跃键 | continueJump(可设置) |
HealthChangeClientEvent | 生物血量变化 | entityId、from、to |
完整事件列表
以上只是最常用的事件。完整的事件列表按分类(世界、玩家、实体、方块、物品、UI、音效等)收录在 API 事件索引 中。
反监听:UnListenForEvent
当你不再需要监听某个事件时(比如在 Destroy 中清理),使用 UnListenForEvent:
def Destroy(self):
self.UnListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerBlockUseEvent",
self,
self.OnBlockUse
)好习惯
在 System 的 Destroy 方法中反监听所有事件,避免内存泄漏。建议在 __init__ 中把所有监听集中写在一个 ListenEvents 方法里,Destroy 中对应写一个 UnListenEvents。
实战:右键方块获得钻石剑
让我们把上面学到的知识串起来——实现一个功能:玩家右键自定义方块时,获得一把钻石剑。
修改 ServerSystem.py:
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
ServerSystem = serverApi.GetServerSystemCls()
class TomatoIslandServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenEvents()
def ListenEvents(self):
# 监听玩家右键方块事件
self.ListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerBlockUseEvent",
self,
self.OnBlockUse
)
# 监听玩家加入事件
self.ListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"AddServerPlayerEvent",
self,
self.OnPlayerJoin
)
def OnPlayerJoin(self, args):
playerId = args["id"]
print "====== 玩家加入: %s ======" % playerId
def OnBlockUse(self, args):
playerId = args["playerId"]
blockName = args["blockName"]
# 只在右键我们的自定义方块时触发
if blockName == "tomato_island:tomato_crop":
# 使用物品组件给玩家一把钻石剑
comp = serverApi.GetEngineCompFactory().CreateItem(playerId)
comp.SpawnItemToPlayerInv(
{"itemName": "minecraft:diamond_sword", "count": 1, "auxValue": 0},
playerId
)
print "====== 给予玩家钻石剑 ======"
def UnListenEvents(self):
self.UnListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerBlockUseEvent",
self,
self.OnBlockUse
)
self.UnListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"AddServerPlayerEvent",
self,
self.OnPlayerJoin
)
def Destroy(self):
self.UnListenEvents()测试
- 保存文件,在 MC Studio 中运行游戏
- 放置你的番茄作物方块(
tomato_island:tomato_crop) - 右键点击方块
- 检查背包——出现了一把钻石剑
注意
ServerBlockUseEvent 在玩家按住右键时每个 tick 都会触发(每秒 30 次)。如果不加限制,背包会瞬间被填满。实际开发中需要加冷却时间或条件判断。
事件的生命周期
System 初始化 (__init__)
│
▼ ListenForEvent 注册监听
│
游戏运行中...
│ 引擎发布事件 → 回调函数被调用
│ 引擎发布事件 → 回调函数被调用
│ ...
│
System 销毁 (Destroy)
│
▼ UnListenForEvent 取消监听小结
| 要点 | 说明 |
|---|---|
| 核心概念 | 事件 = 引擎广播的消息,你注册监听并编写回调 |
| 监听函数 | ListenForEvent(namespace, systemName, eventName, self, callback) |
| 引擎命名空间 | 服务端 serverApi.GetEngineNamespace(),客户端 clientApi.GetEngineNamespace() |
| 回调参数 | 必须接收一个 dict 参数,不同事件有不同字段 |
| 可设置参数 | 部分字段可修改来改变事件结果(如 cancel、damage) |
| 禁止操作 | 不要往引擎事件回调参数中添加自定义 key |
| 反监听 | Destroy 中反监听所有事件,避免内存泄漏 |
接下来我们将学习组件调用——事件告诉你"发生了什么",组件让你"做出改变"。
