Skip to content

事件系统 ​

上一节我们理解了 Client-Server 架构。但两个 System 类里目前只有 __init__ 和 Destroy——它们什么也做不了。要让 Mod 真正"活"起来,需要用到 Mod SDK 最核心的机制:事件系统。

游戏中发生的一切——玩家进入世界、破坏方块、攻击生物、吃食物——都会以事件的形式通知你的脚本。

你将学到

  • 事件系统的四个操作:监听、反监听、发布、响应
  • ListenForEvent 的完整参数含义
  • 常用的服务端事件和客户端事件
  • 回调函数的参数格式和可设置参数
  • 完整实战:玩家右键方块时获得钻石剑

事件系统的工作原理 ​

事件系统可以理解为一个广播电台:

游戏引擎(电台)          你的 Mod(听众)
    │                        │
    │  "玩家破坏了方块"       │
    ├──────────────────────→ │  收到!执行我的逻辑
    │                        │
    │  "生物受到了伤害"       │
    ├──────────────────────→ │  收到!执行我的逻辑
    │                        │
    │  "玩家发送了聊天消息"   │
    ├──────────────────────→ │  收到!执行我的逻辑

你不需要每帧去检查"玩家是不是破坏了方块"——你只需要注册监听,引擎会在事件发生时主动通知你。

四个核心操作 ​

操作函数说明
监听ListenForEvent注册回调函数到某个事件上
反监听UnListenForEvent取消已注册的监听
发布BroadcastEvent / NotifyToServer / NotifyToClient / BroadcastToAllClient触发事件通知所有监听者
响应回调函数事件触发时引擎自动调用你注册的函数

其中监听和响应是最常用的——引擎事件(玩家破坏方块、生物死亡等)由引擎自动发布,你只需要监听并响应。

监听事件:ListenForEvent ​

这是你用得最多的函数。完整写法:

python
self.ListenForEvent(
    namespace,      # 事件来源的命名空间
    systemName,     # 事件来源的系统名称
    eventName,      # 事件名称(字符串)
    instance,       # 回调函数所属的实例(通常是 self)
    func            # 回调函数
)

对于引擎事件(游戏内置的事件),namespace 和 systemName 固定写法:

python
# 服务端监听引擎事件
self.ListenForEvent(
    serverApi.GetEngineNamespace(),
    serverApi.GetEngineSystemName(),
    "事件名称",
    self,
    self.回调函数名
)

# 客户端监听引擎事件
self.ListenForEvent(
    clientApi.GetEngineNamespace(),
    clientApi.GetEngineSystemName(),
    "事件名称",
    self,
    self.回调函数名
)

简化记忆

监听引擎事件就三步:

  1. 引擎命名空间 + 引擎系统名
  2. 事件名称字符串
  3. self + 回调函数

回调函数的格式 ​

回调函数必须接收一个参数(通常是 dict 类型),包含事件的详细信息:

python
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)

python
def OnPlayerJump(self, args):
    # continueJump:设置是否执行跳跃逻辑
    args["continueJump"] = False   # 阻止本次跳跃

示例 2:取消玩家攻击(PlayerAttackEntityEvent,服务端)

python
def OnPlayerAttack(self, args):
    # cancel:是否取消该次攻击,默认不取消
    args["cancel"] = True          # 取消本次攻击

示例 3:修改攻击伤害(PlayerAttackEntityEvent,服务端)

python
def OnPlayerAttack(self, args):
    # damage:引擎传过来的值是 0,允许脚本层修改为其他数
    args["damage"] = 20            # 设置伤害为 20
    args["isValid"] = 1            # 必须设为 1,表示脚本已设置伤害值

注意区分服务端和客户端

同名事件在服务端和客户端的参数可能不同。例如 PlayerAttackEntityEvent:

  • 服务端:damage 可修改,有 cancel 参数
  • 客户端:damage 明确标注「修改无效」,无 cancel 参数

修改参数只在正确的端生效。务必查阅 API 文档确认。

重要规则

不要往引擎事件的回调参数中添加自定义 key! 如果需要扩展数据,先拷贝再操作:

python
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:

python
def Destroy(self):
    self.UnListenForEvent(
        serverApi.GetEngineNamespace(),
        serverApi.GetEngineSystemName(),
        "ServerBlockUseEvent",
        self,
        self.OnBlockUse
    )

好习惯

在 System 的 Destroy 方法中反监听所有事件,避免内存泄漏。建议在 __init__ 中把所有监听集中写在一个 ListenEvents 方法里,Destroy 中对应写一个 UnListenEvents。

实战:右键方块获得钻石剑 ​

让我们把上面学到的知识串起来——实现一个功能:玩家右键自定义方块时,获得一把钻石剑。

修改 ServerSystem.py:

python
# -*- 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()

测试 ​

  1. 保存文件,在 MC Studio 中运行游戏
  2. 放置你的番茄作物方块(tomato_island:tomato_crop)
  3. 右键点击方块
  4. 检查背包——出现了一把钻石剑

注意

ServerBlockUseEvent 在玩家按住右键时每个 tick 都会触发(每秒 30 次)。如果不加限制,背包会瞬间被填满。实际开发中需要加冷却时间或条件判断。

事件的生命周期 ​

System 初始化 (__init__)
    │
    ▼  ListenForEvent 注册监听
    │
游戏运行中...
    │  引擎发布事件 → 回调函数被调用
    │  引擎发布事件 → 回调函数被调用
    │  ...
    │
System 销毁 (Destroy)
    │
    ▼  UnListenForEvent 取消监听

小结 ​

要点说明
核心概念事件 = 引擎广播的消息,你注册监听并编写回调
监听函数ListenForEvent(namespace, systemName, eventName, self, callback)
引擎命名空间服务端 serverApi.GetEngineNamespace(),客户端 clientApi.GetEngineNamespace()
回调参数必须接收一个 dict 参数,不同事件有不同字段
可设置参数部分字段可修改来改变事件结果(如 cancel、damage)
禁止操作不要往引擎事件回调参数中添加自定义 key
反监听Destroy 中反监听所有事件,避免内存泄漏

接下来我们将学习组件调用——事件告诉你"发生了什么",组件让你"做出改变"。