跳到主要内容

初了解 ModAPI - Day 1

· 阅读需 14 分钟

因为《冒险小世界:剑之试炼》4.2 版本的要求,今天我正式开始了解中国版的 ModAPI。

在学校曾经学过 Python 3,所以自认为有一点基础,然而中国版用的还是 Python 2。唉,没想到居然还是有大公司还揪着 v2 不放,一开始的路线感觉就没走对啊。

不管这些了。还是用曾经学 SAPI 的路线走,在了解基础概念的情况下,逻辑问题都交给 AI 帮我解决,例如 Kimi 或 Deepseek,Kimi 的反应快一些,但准确性差,Deepseek 的反应慢,经常无响应,但是总能抓住问题关键。

两种类的对比

先了解一下 Python 2 的类怎么写。想必这东西跟 JavaScript 应该是类似的。比如在定义上,Python 2 的实例化调用__init__,而 js 就是用constructor()了。

类的定义与构建
class MyClass:
awa = 5

def __init__(self, value):
self.value = value

def printValue():
print(self.value)
类的定义与构建
class MyClass {
awa = 5;

constructor(value) {
this.value = value;
}

printValue() {
console.log(this.value)
}
}

基本上,self应该就是 js 中this的含义了。理解了这些,剩下的就都很简单了。反正对于我来说,不需要理解那么多过于底层的东西,会用就行了。

有几个区别要注意:

第一点,看来 js 中的类必须要用constructor,但是Python 2不一定要用__init__。例如下面也是可以的。

class awa:
qwq = 2
print awa().qwq

第二点,属性调用,Python在调用类的属性的时候似乎不需要括号,比如上面的例子中awa.qwq也是可行的,不过主要还是用awa().qwq会更好一些,也能和 js 的语法同步。(诶,我为什么要追求 js 和 Python 的统一?

学习中国版的脚本系统

先前问过 E 尘大大,应该从什么地方入门,他发给了我这么一期视频:

MODAPI教程第零讲 一基础面向实战教学 - 我的世界

但是这期视频稍微老了一些,所以我想看看官方给的文档,结果才发现入门文档写的那叫一个烂——完全过时!其中光是一开始要求在 MCS 安装的入门模板,在现在就已经分裂成了两个,而且文件路径也不一样,内容也不一样,解析也没有,整个就是一头雾水 —— 我连它如何运行都看不明白。而且在游戏里,我按照它的示意输入“钻石剑”也没有反应。这包真能正常运行吗?算了,还是看视频吧。

后来,还是让我找到这个示例包了,原来中国版有两个版本的教程,第二个版本的教程给了示例包。正好视频也需要这个包,这下就搞定找不到示例包的问题了。

果然,还是示例包给的逻辑清晰。只是 Python 2 依然是硬伤,VSC 装了 Pylance 之后就总是在print语句上报错。唉!

中国版脚本的入口文件是modMain.py,这和国际版在manifest.json的入口文件的定义"entry":"..."是类似的。不过modMain.py是没有办法改名的,必须锁死为这个文件。嗯,好理解的。

接下来学了一下装饰器,比如那个@Mod.Binding(name = "TutorialMod", version = "0.0.1"),其实就是一种函数,大概了解了,但具体怎么运行的还不太清楚,以后用到再说。

然后学了一下服务端和客户端。其实不算学吧,以前我就知道。但是提醒我可以把模块 2 的教学中的服务端和客户端说得更清楚一些。

服务端脚本入口

服务端的入口,也就是这段代码所定义的:

class TutorialMod(object):

...

# InitServer绑定的函数作为服务端脚本初始化的入口函数,通常是用来注册服务端系统system和组件component
@Mod.InitServer()
def TutorialServerInit(self):
print "===== init tutorial server ====="
# 函数可以将System注册到服务端引擎中,实例的创建和销毁交给引擎处理。第一个参数是MOD名称,第二个是System名称,第三个是自定义MOD System类的路径
# 取名名称尽量个性化,不能与其他人的MOD冲突,可以使用英文、拼音、下划线这三种。
serverApi.RegisterSystem("TutorialMod", "TutorialServerSystem", "tutorialScripts.tutorialServerSystem.TutorialServerSystem")

...

主要是这个serverApi.RegisterSystem(nameSpace: str, systemName: str, clsPath: str)起到一个入口的作用。这里:

  • 参数一,nameSpace就是上面的class定义的类名,也就是"TutorialMod"
  • 参数二,systemName是要实例化的类名,也就是示例中那个tutorialServerSystem.pyTutorialServerSystem类。
  • 参数三,则是这个类的路径,但似乎路径并不是以/\分隔的,而是以.分隔的。

客户端脚本也是类似的,看来这就是脚本的注册方法了。

服务端脚本

入口会链接到对应路径的类里面并把它实例化:

# -*- coding: utf-8 -*-

# 获取引擎服务端API的模块
import mod.server.extraServerApi as serverApi
# 获取引擎服务端System的基类,System都要继承于ServerSystem来调用相关函数
ServerSystem = serverApi.GetServerSystemCls()
# 获取组件工厂,用来创建组件
compFactory = serverApi.GetEngineCompFactory()

# 在modMain中注册的Server System类
class TutorialServerSystem(ServerSystem):

# ServerSystem的初始化函数
def __init__(self, namespace, systemName):
# 首先调用父类的初始化函数
super(TutorialServerSystem, self).__init__(namespace, systemName)
print "===== TutorialServerSystem init ====="
# 初始时调用监听函数监听事件
self.ListenEvent()

# 监听函数,用于定义和监听函数。函数名称除了强调的其他都是自取的,这个函数也是。
def ListenEvent(self):
...

# 反监听函数,用于反监听事件,在代码中有创建注册就对应了销毁反注册是一个好的编程习惯,不要依赖引擎来做这些事。
def UnListenEvent(self):
...

...

也就是这个TutorialServerSystem(ServerSystem),代表它继承了ServerSystem类,这样它才能够被中国版的脚本调用。在构建对象的时候,首先调用了一个super继承,然后调用自己的ListenEvent,这样就可以监听事件了。这里中国版给了两个示例:

# 在自定义的ServerSystem中监听引擎的事件ServerChatEvent,回调函数为OnServerChat
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerChatEvent", self, self.OnServerChat)
# 监听引擎的事件 ServerBlockUseEvent, 回调函数为 OnServerBlockUseEvent
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerBlockUseEvent", self, self.OnServerBlockUseEvent)

这里,因为继承了ServerSystem,所以它就可以监听事件了。这个监听事件ListenForEvent的逻辑和 SAPI 的afterEventsbeforeEvents简直太像了,都是监听事件,然后回调什么函数。看一下这里面的 5 个参数:

  • serverApi.GetEngineNamespace()serverApi.GetEngineSystemName():引擎的命名空间和名称,这个东西看过教程后就知道,和服务端客户端之间的联络似乎是有关系的,并不是通用的。
  • "ServerChatEvent":调用的事件,类似于 SAPI 的itemUseentityDied这种事件。
  • self:这个我还真不知道,教程也没提,应该是调用自身的方法?
  • self.OnServerChat:执行的回调函数。类似于 SAPI 的subscribe()里面允许的那个函数,比如subscribe(event=>{console.log("1")}),只不过它这个地方不是执行函数function(),而是引用函数function,这个需要注意的。

还是以这个监听聊天栏为例(国际版你能不能快点把chatSend开放掉?都2.0.0了还实验性?):

# 监听ServerChatEvent的回调函数
def OnServerChat(self, args):
print "==== OnServerChat ==== ", args
# 生成掉落物品
# 当我们输入的信息等于右边这个值时,创建相应的物品
# 创建Component,用来完成特定的功能,这里是为了创建Item物品
playerId = args["playerId"]
comp = compFactory.CreateItem(playerId)
if args["message"] == "钻石剑":
# 调用SpawnItemToPlayerInv接口生成物品到玩家背包,参数参考《MODSDK文档》
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_sword", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石镐":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_pickaxe", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石头盔":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_helmet", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石胸甲":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_chestplate", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石护腿":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_leggings", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石靴子":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_boots", "count":1, 'auxValue': 0}, playerId)
else:
print "==== Sorry man ===="

这里,中国版就是通过返回了一个字典,执行回调函数。这和 js 的回调函数有若干的不同,以至于我还不太适应。比如,对于相同的功能来说,SAPI 就要写成:

world.afterEvents.chatSend.subscribe(event => {
const message = event.message;
const player = event.sender;
const playerContainer = player.getComponent("minecraft:inventory").container
if (message === "钻石剑") playerContainer.addItem(new ItemStack("diamond_sword"))
else if (message === "钻石镐") playerContainer.addItem(new ItemStack("diamond_pickaxe"))
else if (message === "钻石头盔") playerContainer.addItem(new ItemStack("diamond_helmet"))
else if (message === "钻石胸甲") playerContainer.addItem(new ItemStack("diamond_chestplate"))
else if (message === "钻石护腿") playerContainer.addItem(new ItemStack("diamond_leggings"))
else if (message === "钻石靴子") playerContainer.addItem(new ItemStack("diamond_boots"))
else console.log("==== Sorry man ====")
})

其实本来想夸 js 比较简洁来着……但至少就这个例子来说没有比中国版简洁到哪里去。主要是因为 SAPI 的获取物品栏和生成物品都是比较麻烦的。

对比一下可以发现,中国版返回的 ID 只是一个实体的数字 ID,所以并不能像国际版那样快捷地调用实体、方块、物品堆叠的方法,而必须通过接口(旧称组件)来获取这些数据。有很多开发者都强调,尽可能用事件而非接口,因为接口用多了会卡……唉。

接口是通过接口工厂工作的,可以通过返回的数据(比如 ID)经过这个工厂加工之后返回需要的数据,其实按我的理解的话,就类似于国际版的类的方法吧。相比于国际版来说,它是直接返回一个类的实例,然后可以调用这个方法或它的属性。

在后面的实践中,我发现了一个很不便的地方,就是获取它的实体类型。SAPI 有一个属性可以直接获取类型:

entity.typeId === "minecraft:player"

这个非常简洁,而中国版的话就必须得

serverApi.GetEngineCompFactory().CreateEngineType(entity).GetEngineTypeStr() == "minecraft:player"

而且还得查文档。所以中国版的脚本确实是绕啊。算了,看在中国版发展早 + 功能多,我也就不多说什么咯。

不管怎么说,现在应该是已经成功入了门了,这也意味着我以后有更多手段和工具可以更简单地实现更好的效果了,太棒了!现在就可以把《冒险小世界:剑之试炼》的一些遗憾全部通过脚本补齐了 >:)