This commit is contained in:
boybook
2025-12-01 20:59:16 +08:00
parent 12738a142c
commit 760c2dd9ad
5535 changed files with 21070 additions and 2021 deletions

View File

@@ -0,0 +1,19 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 5分钟
---
# 摘要
在本章中,我们将一起初步学习**模组SDK****Mod SDK**的编写。一起了解模组SDK是如何使用系统、组件、事件相配合从而在脚本中完成各种逻辑操作的。
- 在第一节(*了解模组SDK框架的使用方法*我们将一起了解模组SDK框架的构成和用法一起学习模组SDK的**系统****System**)。
- 在第二节(*了解事件驱动*我们讲学习模组SDK的**事件****Event**),了解什么是事件驱动编程。
- 在第三节(*在快捷入口新建服务端系统文件*)中,我们将学习如何通过编辑器快速新建一个系统文件。
- 在第四节(*监听事件并创建组件逻辑*)中,我们将学习如何监听事件和创建**组件****Component**)并执行组件的逻辑。
- 在第五节(*打印信息并运调试运行*)中,我们将学习如何打印消息和调试脚本。
- 在第六节(*从编辑器里导出组件*)中,我们将一起导出我们的脚本作品。
- 在第七节(*挑战:自定义传送点*)中,我们将进行一个挑战,自定义一个传送点。
本章关键词:脚本 模组SDK 模组API 系统 事件 响应 监听 通知 广播 引擎组件 引擎组件工厂 滴答 更新 脚本刻 打印 日志 传送点

View File

@@ -0,0 +1,110 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 进阶
time: 20分钟
---
# 了解模组SDK框架的使用方法
我们前面一起学习了数据驱动JSON文件的写法、功能。通过学习我们可以看到数据驱动文件具有非常重要的作用它们可以定义游戏玩法通过基于游戏中的实体各种各样的组件为我们的游戏赋予各种玩法的基础属性。但是数据驱动也有明显的缺点其中最大的缺点就是无法定义复杂的逻辑。即使某些数据驱动定义可以定义一些事件这些事件也只能在某些特定的限制下执行相应的逻辑比如只能去执行特定的有限的功能调用预先设定好的属性而且不能将各个属性通过灵活的逻辑连接起来。最终只能得到一个线性的事件响应网。这种事件系统被我们认为是一种伪事件系统。
为了实现真正的事件系统,我们需要更灵活的编写方法和标准。所以,我们引入了**脚本****Script**系统。中国版的脚本系统使用Python语言来编写通过脚本使能的组件灵活地响应事件将回调函数绑定到游戏运行时的对应位点上赋予游戏更灵活的逻辑。中国版的这套脚本系统提供的Python脚本接口叫做**模组API****Mod API**而中国版为带有以模组API为标准的脚本的附加包提供的开发包称为**模组SDK****Mod SDK**)。
## 配置开发环境
在开始进行模组SDK开发之前我们需要配置Python环境。否则我们将无法正常进行编写和调试。注意中国版的模组SDK使用的是Python2环境。Python2和Python3之间具有较大的代码差异我们无需使用Python3的相关语法。
### 安装Python2
我们进入[Python官网](https://www.python.org/downloads/)下载最新的Python2版本。由于Python2已停止维护我们搜索最新版本2.7并在下面的往期版本中下载。
![](./images/11.1_download_python.png)
**安装时请务必选择安装环境变量**这样可以方便我们快速在命令行中输入Python的程序可执行文件名。
### 安装IDE
我们推荐PyCharm作为Python的集成开发环境IDE。我们在[JetBrains官网](https://www.jetbrains.com/pycharm/download/)上下载PyCharm并安装。如果你没有PyCharm许可证则请下载社区版Community。如果你欲购买许可证或具备学生身份可以申请许可证并下载专业版Professional
![](./images/11.1_install_pycharm.png)
### 安装补全库
为了使我们的代码具备自动补全和定义查询功能我们需要安装模组SDK的补全库。我们可以使用编辑器来安装补全库。
![](./images/11.1_install_lib.png)
打开编辑器,在菜单栏的“工具”下拉菜单中选择你要安装补全库的版本。你可以根据面向的版本和规划来选择安装稳定版补全库或测试版补全库。
![](./images/11.1_cmd_line.png)
开始安装后将提示一个命令行。如果之前安装过旧版本补全库,将先卸载旧版本再安装新版本。
![](./images/11.1_install_successfully.png)
安装成功后将弹出一个提示窗口,提示你安装完成。当然,你也可以像提示窗口中所说的那样,通过命令提示符手动安装补全库。
### 查看示例包
至此,我们做好了所有准备工作。我们打开文档配套的<a href="../../../mcguide/20-玩法开发/13-模组SDK编程/60-Demo示例.html" rel="noopenner"> 示例包 </a>找到TutorialMod演示模组。我们可以在附加包中找到`tutorialScripts`文件夹。这里是便是该模组的一个脚本Python模块文件夹。如果你对Python较为熟悉可以注意到`__init__.py`这代表着这是一个Python模块。事实上一个包中可以有多个脚本文件夹。但每个文件夹都必须是一个模块也就是具备一个`__init__.py`文件。
![](./images/11.1_open_demo.png)
## 什么是系统
我们打开`modMain.py`文件这个文件是Python脚本的入口文件。基岩脚本的引擎将寻找入口文件作为一个Python模块的初始执行文件。目前我们无法自定义入口文件的文件名因此我们需要保证入口文件的文件名为`modMain.py`。我们一起来查看这个入口文件。
```python
# -*- coding: utf-8 -*-
# 上面这行是让这个文件按utf-8进行编码这样就可以在注释中写中文了
# 这行是import到MOD的绑定类Mod用于绑定类和函数
from mod.common.mod import Mod
# 这行import到的是引擎服务端的API模块
import mod.server.extraServerApi as serverApi
# 这行import到的是引擎客户端的API模块
import mod.client.extraClientApi as clientApi
# 用Mod.Binding来绑定MOD的类引擎从而能够识别这个类是MOD的入口类
@Mod.Binding(name = "TutorialMod", version = "0.0.1")
class TutorialMod(object):
# 类的初始化函数
def __init__(self):
print "===== init tutorial mod ====="
# 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")
# DestroyServer绑定的函数作为服务端脚本退出的时候执行的析构函数通常用来反注册一些内容,可为空
@Mod.DestroyServer()
def TutorialServerDestroy(self):
print "===== destroy tutorial server ====="
# InitClient绑定的函数作为客户端脚本初始化的入口函数通常用来注册客户端系统system和组件component
@Mod.InitClient()
def TutorialClientInit(self):
print "===== init hugo fps client ====="
# 函数可以将System注册到客户端引擎中实例的创建和销毁交给引擎处理。第一个参数是MOD名称第二个是System名称第三个是自定义MOD System类的路径
# 取名名称尽量个性化不能与其他人的MOD冲突可以使用英文、拼音、下划线这三种。
clientApi.RegisterSystem("TutorialMod", "TutorialClientSystem", "tutorialScripts.tutorialClientSystem.TutorialClientSystem")
# DestroyClient绑定的函数作为客户端脚本退出的时候执行的析构函数通常用来反注册一些内容,可为空
@Mod.DestroyClient()
def TutorialClientDestroy(self):
print "===== destroy hugo fps client ====="
```
在入口文件中我们看到了我们的Python模组作为一个类而定义而类中除了`__init__`方法之外定义了四个基本方法。分别为`TutorialServerInit``TutorialServerDestroy``TutorialClientInit``TutorialClientDestroy`。这四个方法的作用便是用于注册和销毁**系统****System**)。
我们的模组是分为服务端和客户端分别执行的,这是因为我的世界游戏是分为服务端和客户端两部分互联互通运行的。试想,如果我们不分服务端和客户端,执行一个操作时会出现什么样的结果。在一个服务器中,一个玩家触发了一定的逻辑,逻辑的结果是在世界(0, 64, 0)处放置一个特定的方块。如果我们的服务端和客户端时分开执行的,那么所有的玩家的客户端都将开始执行这一逻辑。但是我们先前知道,玩家在同一时刻只可能在某一个维度的世界中。所以客户端是分辨不出这个放置方块的指示是应该位于哪一维度的。虽然我们只希望在主世界中放置方块,但是处于下界和末地的客户端也将“模拟地”在这两个维度的对应位置放置一个方块。这个方块事实上在服务端中的下界和末地中并未放置,因此将出现客户端和服务端不同步的严重问题。所以,我们的脚本也需要和游戏的基本逻辑一样,分别在服务端和客户端同时设置一个系统,分别执行不同的逻辑,同时通过一定的功能互相通讯,保持同步。
那么系统是什么呢在模组API中我们在每个端都可以设置一个或多个系统。系统将负责宏观工作例如定义和反定义事件、注册和反注册事件的监听、广播或通知一个事件进行各种更新操作。而通过系统进行事件的广播和监听执行逻辑的过程便称为**事件驱动**。
事实上,所有的系统都是一个继承自系统基类的类。这个系统的基类为`mod.common.system.baseSystem`

View File

@@ -0,0 +1,112 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 进阶
time: 25分钟
---
# 了解事件驱动
接下来我们以TutorialMod演示模组的服务端系统的定义部分来了解什么是**事件驱动****Event-driven**)系统。我们一起来查看演示模组中的`tutorialServerSystem.py`文件。
```python
# -*- 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):
# 在自定义的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)
# 反监听函数,用于反监听事件,在代码中有创建注册就对应了销毁反注册是一个好的编程习惯,不要依赖引擎来做这些事。
def UnListenEvent(self):
self.UnListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerChatEvent", self, self.OnServerChat)
self.UnListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerBlockUseEvent", self, self.OnServerBlockUseEvent)
# 监听ServerBlockUseEvent的回调函数
def OnServerBlockUseEvent(self, args):
# 这里的sdkteam_test:block1替换成你自己的自定义方块的命名空间与方块名
if args["blockName"] == "sdkteam_test:block1":
# 调用给予物品的接口与OnServerChat中相似
playerId = args["playerId"]
comp = compFactory.CreateItem(playerId)
# 这里填钻石剑的物品名
comp.SpawnItemToPlayerInv({"itemName": "minecraft:diamond_sword", "count": 1, 'auxValue': 0}, args["playerId"])
# 监听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 ===="
# 函数名为Destroy才会被调用在这个System被引擎回收的时候会调这个函数来销毁一些内容
def Destroy(self):
print "===== TutorialServerSystem Destroy ====="
# 调用上面的反监听函数来销毁
self.UnListenEvent()
```
## 事件驱动
我们可以看到,为了定义我们的系统的类,我们从**额外API****Extra API**,服务端为`mod.server.extraServerApi`)下的`GetServerSystemCls`方法得到了继承自系统基类`mod.common.system.baseSystem`的服务端系统类`mod.server.system.serverSystem`的引用。我们的`TutorialServerSystem`类的定义将继承自该服务端系统类。
接着,在`TutorialServerSystem`类的`__init__`方法中,我们可以看到在调用完毕超类的初始化方法后紧接着便调用了自定义方法`ListenEvent`。这是用来注册各种事件监听的方法。
`ListenEvent`方法中,我们通过调用系统(`self`)的`ListenForEvent`方法来注册一个事件监听。前两个参数分别是该事件监听要监听的系统的命名空间和系统的名称。如果我们要监听引擎默认给出的模组API原生事件我们需要使用额外API中的`GetEngineNamespace``GetEngineSystemName`方法获取**引擎系统****Engine System**的命名空间和名称。引擎系统也是一个系统是基岩引擎中模组API原生的系统。如果要监听自己的或者其他模组的自定义系统广播的事件则需要正确填写对应的命名空间和名称使两个参数和`modMain.py`中注册系统时提供的命名空间和系统名相一致。
**事件****Event**)是由基岩引擎发出或用户自定义的代码发出的,在游戏发生某种特定的逻辑时触发的一种标识。对一个事件进行**监听****Listen**)指的是将一条回调函数传入一个事件的响应代码中,在该事件被触发时同时执行该回调函数。这条回调函数往往被称为事件的**响应****Response**)。而在事件触发时将这一消息发送给指定的客户端或服务端的行为,根据发送的对象和接收端的数目不同分别被称作**广播****Broadcast**)或**通知****Notify**)。
`ListenEvent`方法的第三个参数便是事件的标识符,告诉引擎我将“哪个”事件的监听设置到了该系统上。第四个参数是该监听绑定到的系统的实例。因为我们的事件监听响应(即回调函数)是在该系统的示例中定义的,所以我们填写该系统自身(`self`)。在第五个参数中写入该监听的响应函数。这个函数会在事件触发时异步触发。
这一系列操作便是一个简单的事件驱动逻辑。通过注册一个事件的监听将一个回调函数作为相应绑定到事件上。当事件被触发并广播或通知到该端时便异步执行该回调函数,从而运行相关的逻辑。
## 组件接口的使用
我们从额外API下的`GetEngineCompFactory`方法可以获得服务端**引擎组件工厂****Engine Component Factory**)的类`mod.server.component.engineCompFactoryServer`的引用。引擎组件工厂是用来创建**引擎组件****Engine Component**的类。引擎组件与常规的ECS系统中的组件不同他们不仅是用来存储数据的更是用来执行逻辑的。每个引擎组件下都具有各种各样的方法每种方法都可以用来执行一个不同的逻辑或完成一个不同的操作。比如引擎组件`game`(服务端为`mod.server.component.gameCompServer`的实例)下的`KillEntity`方法便可以杀死一个指定ID的实体。只需使用如下实例代码便可做到杀死一个实体
```python
import mod.server.extraServerApi as serverApi
comp = serverApi.GetEngineCompFactory().CreateGame(levelId)
comp.KillEntity(entityId)
```
上面的`comp`变量便是`mod.server.component.gameCompServer`引擎组件的一个实例。
通过引擎组件可以做到各种逻辑配合事件驱动系统便可以”精准打击“到游戏中各种逻辑发生点并插入自己的逻辑。这样的运作模式便是模组API如何修改游戏的基本方法

View File

@@ -0,0 +1,101 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 15分钟
---
# 在快捷入口新建服务端系统文件
本节中,我们一起学习如何在新版编辑器中创建脚本文件。
## 使用资源管理器快捷新建脚本系统
我们打开编辑器,点击“**资源管理**”窗格的“**新建**”按钮。
![](./images/11.3_create_main.png)
在“**代码**”选项卡中找到**ModMain**。这个选项将可以帮助我们快速创建一个模组API文件夹其中包括一个代表该文件夹是一个模块的`__init__.py`和一个入口文件`modMain.py`
![](./images/11.3_create_main_2.png)
我们在这里给文件命名,实际上就是在给模组命名。我们模组类的装饰器`@Mod.Binding(name = "ModName", version = "0.0.1")`中的`ModName`处便代表我们的模组名。这里的名字将自动填充至模组入口文件中模组类的装饰器中。同时,脚本文件夹名和模组的类名也将会使用我们这里的模组名,所以请谨慎填写一个不会和其他开发者的模组中的模组名重名的名称,否则将有可能造成加载冲突。
接下来我们就可以分别给我们的模组创建服务端文件和客户端文件了!我们以服务端文件为例。
![](./images/11.3_create_server.png)
还是在“**代码**”选项卡中,我们选择**ServerSystem**。这个选项可以用于创建服务端系统文件`xxxxServerSystem.py`
![](./images/11.3_create_server_2.png)
我们在第一个下拉菜单中选择我们刚才创建的脚本文件夹目录。如果一个附加包中存在多个脚本文件夹,我们就需要进行在下拉菜单中选择正确的我们想要创建的脚本目录。文件名便是将会自动使用到服务端系统的类名上的名称。
![](./images/11.3_created.png)
我们可以看到,服务端系统文件也创建了。同时,编辑器还创建了一个`Part`文件夹,这是新版编辑器自动创建的文件。如果我们的脚本不是用于零件脚本,那么这个文件夹可以忽略。
我们来查看自动创建的`modMain.py`
```python
# -*- coding: utf-8 -*-
from common.mod import Mod
@Mod.Binding(name="DemoTutorialMod", version="0.0.1")
class DemoTutorialMod(object):
def __init__(self):
pass
@Mod.InitServer()
def DemoTutorialModServerInit(self):
pass
@Mod.DestroyServer()
def DemoTutorialModServerDestroy(self):
pass
@Mod.InitClient()
def DemoTutorialModClientInit(self):
pass
@Mod.DestroyClient()
def DemoTutorialModClientDestroy(self):
pass
```
`DemoTutorialServerSystem.py`
```python
# -*- coding: utf-8 -*-
import server.extraServerApi as serverApi
ServerSystem = serverApi.GetServerSystemCls()
class DemoTutorialServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
# ScriptTickServerEvent的回调函数会在引擎tick的时候调用1秒30帧被调用30次
def OnTickServer(self):
"""
Driven by event, One tick way
"""
pass
# 这个Update函数是基类的方法同样会在引擎tick的时候被调用1秒30帧被调用30次
def Update(self):
"""
Driven by system manager, Two tick way
"""
pass
def Destroy(self):
pass
```
我们可以看到,自动创建时并不会在模组类中注册我们的系统,所以我们的系统注册代码需要手动输入。同时,自动创建给我们的服务端系统类写入了两个函数,分别是脚本刻的**滴答****Tick**)函数和系统类的**更新****Update**函数这两个函数分别通过事件响应回调和重载超类方法的形式实现每秒运行30次的代码。这样运行一次的时间我们称为一个**脚本刻****Script Tick**)。

View File

@@ -0,0 +1,122 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 进阶
time: 20分钟
---
# 监听事件并创建组件逻辑
现在我们已经创建好了一个服务端系统,我们来尝试向这个系统中添加逻辑。
在添加逻辑之前我们需要手动将系统在主模组文件中注册。我们仿照之前看到的示例将额外API的`RegisterSystem`函数写在`@Mod.InitServer()`装饰器下的方法中。
```python
@Mod.InitServer()
def DemoTutorialModServerInit(self):
serverApi.RegisterSystem("DemoTutorialMod", "Server", "Script_DemoTutorialMod.DemoTutorialServerSystem.DemoTutorialServerSystem")
```
## 监听实体受伤事件
我们回到服务端系统文件`DemoTutorialServerSystem.py`中。我们一起尝试做一个简单的事件监听,比如,监听实体(活动对象)收到伤害的事件。
通过查阅API文档我们得到了控制实体受到伤害的事件`ActorHurtServerEvent`,其字面意思为“活动对象受伤服务端事件”。我们通过该系统本身的`ListenForEvent`方法来注册这个事件监听。我们在`__init__`方法的末尾加入我们的监听注册函数,同时在该类中定义一个新的方法,比如名为`OnActorHurtServer`,将该方法作为回调函数绑定到事件上。
```python
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ActorHurtServerEvent", self, self.OnActorHurtServer)
```
这样,我们便完成了实体受伤的监听。当实体受到伤害时,`DemoTutorialServerSystem`实例的`OnActorHurtServer`方法将会运行。
## 在事件内触发击退逻辑
我们希望不仅仅是监听该事件,更要在事件发生时执行一些逻辑,比如更改受击实体的击退属性。
我们通过查阅API文档得知`action`组件(`mod.server.component.actionCompServer`)具备更改击退逻辑的方法`SetMobKnockback`。所以我们使用引擎组件工厂创建一个`action`引擎组件,然后调用它的设置击退的方法做到一些逻辑,比如我们想增加击退的威力。我们在`OnActorHurtServer`中写入如下内容。
```python
def OnActorHurtServer(self, args):
comp = serverApi.GetEngineCompFactory().CreateAction(args["entityId"])
comp.SetMobKnockback(0.1, 0.1, 10.0, 1.0, 1.0)
```
![](./images/11.4_set_knockback.png)
这样,我们便成功更改了击退的威力。我们将完整的修改过的代码展示在此处。首先是`modMain.py`
```python
# -*- coding: utf-8 -*-
from mod.common.mod import Mod
import mod.server.extraServerApi as serverApi
import mod.client.extraClientApi as clientApi
@Mod.Binding(name="DemoTutorialMod", version="0.0.1")
class DemoTutorialMod(object):
def __init__(self):
@Mod.InitServer()
def DemoTutorialModServerInit(self):
serverApi.RegisterSystem("DemoTutorialMod", "Server", "Script_DemoTutorialMod.DemoTutorialServerSystem.DemoTutorialServerSystem")
@Mod.DestroyServer()
def DemoTutorialModServerDestroy(self):
pass
@Mod.InitClient()
def DemoTutorialModClientInit(self):
pass
@Mod.DestroyClient()
def DemoTutorialModClientDestroy(self):
pass
```
然后是`DemoTutorialServerSystem.py`
```python
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
ServerSystem = serverApi.GetServerSystemCls()
class DemoTutorialServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ActorHurtServerEvent", self, self.OnActorHurtServer)
def OnActorHurtServer(self, args):
comp = serverApi.GetEngineCompFactory().CreateAction(args["entityId"])
comp.SetMobKnockback(0.1, 0.1, 10.0, 1.0, 1.0)
# ScriptTickServerEvent的回调函数会在引擎tick的时候调用1秒30帧被调用30次
def OnTickServer(self):
"""
Driven by event, One tick way
"""
pass
# 这个Update函数是基类的方法同样会在引擎tick的时候被调用1秒30帧被调用30次
def Update(self):
"""
Driven by system manager, Two tick way
"""
pass
def Destroy(self):
self.UnListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ActorHurtServerEvent", self, self.OnActorHurtServer)
```
![](./images/11.4_in-game.gif)
进入游戏测试,便可以发现`SetMobKnockback`更改击退属性“诚不我欺”。

View File

@@ -0,0 +1,58 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 10分钟
---
# 打印信息并运调试运行
懂得如何将信息打印到控制台上是一个合格的开发者必须具备的本领。在本节中我们将学习如何打印信息并调试运行。
## 学会打印日志并查看控制台
每次打开电脑开发版,我的世界开发工作台便会创建一个“**脚本测试日志**”窗口,这边是我们的脚本调试控制台。
在模组中,我们可以在任何位置凭借自己的意愿将任何模组中的上下文信息打印到控制台上。我们有两种打印方式。
### `print`
我们可以使用原生的`print`函数来打印信息。比如,这边是一个`print`函数的打印示例,它将在初始化模组服务端注册之前打印开始初始化的消息。
```python
@Mod.InitServer()
def DemoTutorialModServerInit(self):
print "===== init tutorial server ====="
serverApi.RegisterSystem("DemoTutorialMod", "Server", "Script_DemoTutorialMod.DemoTutorialServerSystem.DemoTutorialServerSystem")
```
### `mod_log`
我们还可以通过`mod_log`模块来打印信息。下面的示例中通过导入并执行`logger.info`打印了一个信息。
```python
from mod_log import logger
logger.info("print log: %s", "OK")
```
### 两者的不同
我们以下面代码为示例可以看到两种打印方式在控制台表现的不同。
```python
def OnActorHurtServer(self, args):
print "==== 使用print打印的日志 ==== "
logger.info("使用logger打印的日志")
comp = serverApi.GetEngineCompFactory().CreateAction(args["entityId"])
comp.SetMobKnockback(0.1, 0.1, 10.0, 1.0, 1.0)
```
![](./images/11.5_print.png)
## 使用热更新快速修复问题
我们的Python代码在游戏中支持热更新调试。在电脑开发版的游戏运行中时我们修改Python代码将导致文件在开发版中的重载。自动热更新主要用于修改函数内实现比如我可以修改击退威力使其增加为20那么我在游戏中就可以在下一次攻击时延长击退的距离。但是热更新对全局变量、新增类或文件无效。因为这些都是在Python代码运行一开始便初始化的内容热更新文件不会使其再次初始化所以可能会导致无效。
![](./images/11.5_overload.png)
我们将击退为例从10更改为20可以在控制台中看到热更新的信息。灵活掌握代码的热更新将有效大幅度减少开启关闭游戏的次数使调试更加方便。

View File

@@ -0,0 +1,25 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 10分钟
---
# 从编辑器里导出组件
导出组件也是开发者应该学会的开发中必不可少的一环。我们有多种组件导出方法。
## 导出为资源包
打开编辑器,点击“资源管理”窗格中的“导出”可以将组件导出为资源包,文件格式为`.mep`
![](./images/11.6_export.png)
通过这种方式导出的资源包可以在另一个附加包组件中通过同一位置的“导入”功能合并导入。
## 整体导出
在我的世界开发工作台中找到作品。右键或点击“更多”按钮,在展开的菜单中点击“导出”或“导出(含编辑信息)”。
![](./images/11.6_export_2.png)
这种方式可以使整个包导出为一个`.zip`文件。通过这种方式导出的文件可以在我的世界开发工作台中通过“本地导入”功能重新导入为一个组件。

View File

@@ -0,0 +1,362 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 进阶
time: 30分钟
---
# 挑战:自定义传送点
在本节中,我们一起来制作一个传送点模组。我们知道,在我的世界中,地形可以近似认为是无限广阔的,所以,我们在探索的过程中可能经常迷失方向,找不到回到据点的路。因此,我们可以自定义一个传送点模组。通过保存一个传送点来做到在迷路时快速回到传送点所指定的位置。
下面我们一起使用模组SDK来制作一个传送点模组。
## 创建项目
我们在我的世界开发工作台中创建一个“传送点模组”的组件然后使用编辑器“资源管理”窗格中“创建”按钮来快速创建一个Python模组模块将其命名为`TeleportPointMod`
![](./images/11.7_tp_point_server_create.png)
由于我们知道,只有服务端才能获取和控制每个玩家的位置,所以我们只需创建一个服务端系统,将其命名为`TeleportPointServerSystem`。对于每个玩家来说,客户端一般只负责屏幕的渲染和本地的一些计算,是无法直接改变玩家处于整个世界中的位置的,所以我们无需使服务端与客户端交互。因此我们无需创建客户端系统。
![](./images/11.7_tp_point_server_created.png)
创建结束后我们可以在“资源管理”窗格中看到如图的文件排布。我们将整个行为包文件夹在Python的IDE中打开以便之后的代码编写。此时的Python文件内容如下。
`modMain.py`
```python
# -*- coding: utf-8 -*-
from common.mod import Mod
@Mod.Binding(name="TeleportPointMod", version="0.0.1")
class TeleportPointMod(object):
def __init__(self):
pass
@Mod.InitServer()
def TeleportPointModServerInit(self):
pass
@Mod.DestroyServer()
def TeleportPointModServerDestroy(self):
pass
@Mod.InitClient()
def TeleportPointModClientInit(self):
pass
@Mod.DestroyClient()
def TeleportPointModClientDestroy(self):
pass
```
`TeleportPointServerSystem.py`
```python
# -*- coding: utf-8 -*-
import server.extraServerApi as serverApi
ServerSystem = serverApi.GetServerSystemCls()
class TeleportPointServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
# ScriptTickServerEvent的回调函数会在引擎tick的时候调用1秒30帧被调用30次
def OnTickServer(self):
"""
Driven by event, One tick way
"""
pass
# 这个Update函数是基类的方法同样会在引擎tick的时候被调用1秒30帧被调用30次
def Update(self):
"""
Driven by system manager, Two tick way
"""
pass
def Destroy(self):
pass
```
## 注册服务端系统
我们知道,单纯创建了服务端系统文件是无法在主模组文件中同时将其注册的。我们手动将我们的服务端系统注册。我们在`TeleportPointModServerInit`函数中输入注册系统的方法:
```python
@Mod.InitServer()
def TeleportPointModServerInit(self):
serverApi.RegisterSystem('TeleportPointMod', 'TeleportPointServerSystem', 'Script_TeleportPointMod.TeleportPointServerSystem.TeleportPointServerSystem')
```
其中`Script_TeleportPointMod.TeleportPointServerSystem.TeleportPointServerSystem`对应着`Script_TeleportPointMod`文件夹中`TeleportPointServerSystem.py`文件的`TeleportPointServerSystem`类。这样,我们便成功注册了系统。
## 制作传送点模组的主体
传送点模组的主体部分应该都位于服务单系统中,我们只需要考虑实现的逻辑即可。
我们可以允许玩家使用多种方式来触发传送。最简单的一种便是使用聊天栏触发。我们不妨就采取这种方式来制作模组。我们可以设置一些关键词,用于传送点的设置、传送和移除,比如`tppoint set``tppoint apply``tppoint remove`。而每个玩家为其使用一个变量来保存其设置的传送点坐标的数据。为了方便保存我们可以使用一个字典来做到这一点。字典中的键为玩家的ID而值为玩家保存的传送点坐标。当玩家执行传送时我们主需要检索该玩家的ID获取字典保存的值然后将玩家的坐标设定为这个值即可。
我们依次来实现这个逻辑。
### 监听玩家的聊天栏消息
我们通过查阅文档可知,服务端的`ServerChatEvent`事件可以做到响应玩家发送聊天栏信息,于是我们为其设置事件监听。
```python
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerChatEvent", self, self.on_chat)
```
同时加入一个`on_chat`方法,用于充当回调函数。
```python
def on_chat(self, event):
pass
```
### 记录传说点
下面我们按照我们刚才的设想开始写入基本逻辑。我们读取事件数据中的`playerId``message`。它们分别是发送消息的玩家ID和所发送的消息。我们使用字符串的`startswith`方法来判定消息是否以`tppoint set`开头。如果消息以`tppoint set`开头那么就通过玩家ID创建一个`pos`引擎组件。`pos`引擎组件具备`GetFootPos`方法、`GetPos`方法和`SetPos`方法。我们可以使用`GetFootPos`方法来获取玩家脚步坐标并存入一个字典,我们不妨起名为`player_tp_cache``player_tp_cache`字典变量由于是需要在整个类中实现的,所以我们将其放在类的初始化方法里。
```python
class TeleportPointServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerChatEvent", self, self.on_chat)
# 用来暂时缓存玩家的传送点信息,在每次重新开启一次游戏后会被重置
self.player_tp_cache = {}
def on_chat(self, event):
# 获取玩家ID
player_id = event['playerId']
# 获取聊天消息
message = event['message']
if message.startswith('tppoint set'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
teleport_pos = pos_comp.GetFootPos()
# 保存玩家的当前坐标至字典缓存中
self.player_tp_cache[player_id] = teleport_pos
print '已设置玩家传送点'
```
### 删除传送点
依据同样的原理,我们使用字典的`pop`方法来移除一个玩家的键值对。我们加入删除传送点的逻辑。
```python
def on_chat(self, event):
# 获取玩家ID
player_id = event['playerId']
# 获取聊天消息
message = event['message']
if message.startswith('tppoint set'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
teleport_pos = pos_comp.GetFootPos()
# 保存玩家的当前坐标至字典缓存中
self.player_tp_cache[player_id] = teleport_pos
print '已设置玩家传送点'
if message.startswith('tppoint remove'):
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
# 弹出字典中的玩家数据
self.player_tp_cache.pop(player_id)
print '已移除玩家传送点'
else:
print '该玩家没有设置传送点'
```
### 传送至传送点
最后,我们使用`pos`引擎组件的`SetPos`方法来实现传送逻辑。
```python
def on_chat(self, event):
# 获取玩家ID
player_id = event['playerId']
# 获取聊天消息
message = event['message']
if message.startswith('tppoint set'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
teleport_pos = pos_comp.GetFootPos()
# 保存玩家的当前坐标至字典缓存中
self.player_tp_cache[player_id] = teleport_pos
print '已设置玩家传送点'
if message.startswith('tppoint remove'):
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
# 弹出字典中的玩家数据
self.player_tp_cache.pop(player_id)
print '已移除玩家传送点'
else:
print '该玩家没有设置传送点'
if message.startswith('tppoint apply'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
teleport_pos = self.player_tp_cache[player_id]
# 传送玩家
pos_comp.SetPos(teleport_pos)
print '已应用玩家传送点'
else:
print '该玩家没有设置传送点'
```
这样,我们便完成了传送逻辑的编写。额外地,我们还可以通过`msg`组件来为玩家显示消息提示,展示设置、传送或移除成功的提示。我们可以修改如下:
```python
def on_chat(self, event):
# 获取玩家ID
player_id = event['playerId']
# 获取聊天消息
message = event['message']
if message.startswith('tppoint set'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
teleport_pos = pos_comp.GetFootPos()
# 保存玩家的当前坐标至字典缓存中
self.player_tp_cache[player_id] = teleport_pos
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "已设置传送点")
print '已设置玩家传送点'
if message.startswith('tppoint remove'):
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
# 弹出字典中的玩家数据
self.player_tp_cache.pop(player_id)
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "已移除传送点")
print '已移除玩家传送点'
else:
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "请先设置传送点")
print '该玩家没有设置传送点'
if message.startswith('tppoint apply'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
teleport_pos = self.player_tp_cache[player_id]
# 传送玩家
pos_comp.SetPos(teleport_pos)
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "已传送至传送点")
print '已应用玩家传送点'
else:
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "请先设置传送点")
print '该玩家没有设置传送点'
```
我们可以进入游戏查看传送和消息提示情况。
![](./images/11.7_in-game.png)
我们可以看到,传送和消息都和我们所设置的一样正常运作!
我们最后放出完整的代码供大家参考,`modMain.py`如下:
```python
# -*- coding: utf-8 -*-
from mod.common.mod import Mod
import mod.server.extraServerApi as serverApi
@Mod.Binding(name="TeleportPointMod", version="0.0.1")
class TeleportPointMod(object):
def __init__(self):
pass
@Mod.InitServer()
def TeleportPointModServerInit(self):
serverApi.RegisterSystem('TeleportPointMod', 'TeleportPointServerSystem', 'Script_TeleportPointMod.TeleportPointServerSystem.TeleportPointServerSystem')
@Mod.DestroyServer()
def TeleportPointModServerDestroy(self):
pass
@Mod.InitClient()
def TeleportPointModClientInit(self):
pass
@Mod.DestroyClient()
def TeleportPointModClientDestroy(self):
pass
```
`TeleportPointServerSystem.py`如下:
```python
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
from mod.server.system.serverSystem import ServerSystem
ServerSystem = serverApi.GetServerSystemCls()
class TeleportPointServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerChatEvent", self, self.on_chat)
# 用来暂时缓存玩家的传送点信息,在每次重新开启一次游戏后会被重置
self.player_tp_cache = {}
def on_chat(self, event):
# 获取玩家ID
player_id = event['playerId']
# 获取聊天消息
message = event['message']
if message.startswith('tppoint set'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
teleport_pos = pos_comp.GetFootPos()
# 保存玩家的当前坐标至字典缓存中
self.player_tp_cache[player_id] = teleport_pos
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "已设置传送点")
print '已设置玩家传送点'
if message.startswith('tppoint remove'):
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
# 弹出字典中的玩家数据
self.player_tp_cache.pop(player_id)
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "已移除传送点")
print '已移除玩家传送点'
else:
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "请先设置传送点")
print '该玩家没有设置传送点'
if message.startswith('tppoint apply'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
teleport_pos = self.player_tp_cache[player_id]
# 传送玩家
pos_comp.SetPos(teleport_pos)
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "已传送至传送点")
print '已应用玩家传送点'
else:
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "请先设置传送点")
print '该玩家没有设置传送点'
def Destroy(self):
pass
```