first commit

This commit is contained in:
boybook
2025-03-17 13:24:39 +08:00
commit 9a0334ee84
6410 changed files with 221907 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
---
front:
hard: 入门
time: 10分钟
---
# 开发环境的搭建
开发我的世界玩法组件需要使用Python2.7,同时推荐安装一个集成开发环境来辅助代码的开发。
那么在本节课程中将会带领大家下载并安装Python2.7和PyCharm。
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=632867066b13db499d094793" width="800" height="600" allow="fullscreen"/>
## Python2.7
Python2.7下载地址: [链接](https://www.python.org/downloads/release/python-2718/)
大家可以根据自己的操作系统来选择下载。一般Windows64位系统都下载`Windows x86-64 MSI installer`
下载完成后打开进行安装,自行选择安装路径,需要注意的是,在这个界面,翻到底部,将`Add python.exe to Path`勾选,然后再继续点击安装。
![](./images/1.png)
## PyCharm
PyCharm是一款由JetBrains开发的用于Python的集成开发环境下载地址: [链接](https://www.jetbrains.com/pycharm/download/)。
打开页面后,下载`Community`版本即可,然后点击安装,在这里勾选创建桌面快捷方式。
![](./images/2.png)
## 安装补全库
在完成安装后,我们还需要手动安装补全库,有了补全库之后,模组的开发将会更加便捷。
同时按下Win和R在弹出的“运行”窗口中输入cmd并回车。
![](./images/3.png)
在命令提示符窗口中,输入`pip install mc-netease-sdk`,然后回车
![](./images/4.png)
出现这样的提示就安装成功,如果出现错误,可以检查网络连接是否稳定,更换网络后再试。
![](./images/5.png)
## 使用PyCharm打开附加包项目
首先我们需要在Studio中找一个需要打开的附加包然后右键点击打开目录。
![](./images/6.png)
打开目录后记录下目录的路径。然后在PyCharm中点击Open在目录浏览的窗口中找到刚刚弹出的目录点击打开。
![](./images/7.png)
打开后目录中有`behavior_pack``resource_pack`,即为正确打开。
## 课后作业
在课后作业中我们需要验证Python2.7的安装是否成功。
具体操作步骤如下:
- 打开cmd
- 输入`python -V`
- 输入`pip -V`
- 检查是否有正确输出
打开cmd的方法在安装补全库的时候已经介绍过了正确的输出如下图所示可以自行输入并参考。
![](./images/8.png)
可以看到Python的版本是2.7.18pip的路径在python2.7的目录中。

View File

@@ -0,0 +1,135 @@
---
front:
hard: 进阶
time: 30分钟
---
# Python基础语法
想要使用Python进行玩法开发那么就必须学会Python的基本语法。
因为已经有大量的优秀的教程来带领大家学习Python这门语言所以在这里我们将不过多介绍Python的基础语法主要介绍Python与我的世界玩法开发相关的地方。Python的教程见[链接](https://www.runoob.com/python/python-chinese-encoding.html),在链接的这系列的教程中,我们已经完成了环境搭建,所以从链接到的那一章开始往后看,一直看到`Python 模块`,额外加上`Python 面向对象`,即可开始本节内容的学习。
![](./images/9.png)
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=63286726e6c041f2578ca816" width="800" height="600" allow="fullscreen"/>
## 面向对象技术
面向对象技术(Object-Oriented Technology),强调软件在开发过程中,采用人认知客观世界的过程中普遍运用的思维方法,直观、自然地描述客观世界中的有关事物。
在我的世界的玩法开发中,同样也运用到了这门技术。
在学习过面向对象技术之后想要开发我的世界玩法组件就首先需要了解我们本次介绍的Python零件开发中需要使用的类。
```python
# -*- coding: utf-8 -*-
from Preset.Model.PartBase import PartBase
import random
from Preset.Model.GameObject import registerGenericClass
@registerGenericClass("MyLogPart")
class MyLogPart(PartBase):
def __init__(self):
super(MyLogPart, self).__init__()
self.name = "日志零件"
self.description = "日志零件,用于测试调试日志"
self.interval = (30, 90)
self._tickCnt = 0
self._tickInterval = random.randint(self.interval[0], self.interval[1])
def TickClient(self):
self._tickCnt += 1
if self._tickCnt == self._tickInterval:
print("%s%s" % (self.GetDisplayPath(), self.GetWorldPosition()))
self._tickInterval = random.randint(self.interval[0], self.interval[1])
self._tickCnt = 0
```
```python
# -*- coding: utf-8 -*-
from Meta.ClassMetaManager import sunshine_class_meta
from Meta.EnumMeta import DefEnum
from Meta.TypeMeta import PBool, PStr, PInt, PCustom, PVector3, PVector3TF, PEnum, PDict, PFloat, PArray, PVector2
from Preset.Model import PartBaseMeta
@sunshine_class_meta
class MyLogPartMeta(PartBaseMeta):
CLASS_NAME = "MyLogPart"
PROPERTIES = {
"interval": PVector2(sort=1000, group="MyLogPart", text="打印间隔"),
}
```
这里是官方内置零件中一个日志零件的两个Python文件。
可以看到,第一个文件,是一个名为`MyLogPart`的class它继承了`PartBase`这个类,同时定义了一些成员变量,用来描述这个零件的名字(name)、作用(description)。
这个类主要有两个函数,一个\_\_init\_\_在初始化时会被执行一个TickClient函数会打印日志信息。
第二个文件,是一个名为`MyLogPartMeta`的类,继承了`PartBaseMeta`这个类,这个类同样也定义了成员变量,用来存储这个类的一些基本数据。这个类实际上是一个元数据类,用来和编辑器对接,方便我们将一部分变量通过编辑器设置到零件中。在本节中我们主要只做了解,不深入研究。
那么在玩法开发中其实并不止PartBase和PartBaseMeta这两个类可以继承所有与玩法开发相关的类都在文档中可以查阅[链接](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/3-PresetAPI/%E9%A2%84%E8%AE%BE%E5%AF%B9%E8%B1%A1/%E9%80%9A%E7%94%A8/%E6%B8%B8%E6%88%8F%E5%AF%B9%E8%B1%A1GameObject.html?catalog=1)。
感兴趣的同学可以点击链接,大致了解一下有哪些类可以用,它们的作用又是什么,继承关系又是怎么样的。
## 课后作业
在了解了面向对象技术之后,为了加深对这个技术的理解,课后作业希望使用面向对象技术,制作一个简单的程序。
### 要求
1. 编写Entity类代表游戏中的所有实体需要有一个成员变量pos类型为元组代表坐标初始化时接收一个3个int的元组存入pos。
2. 编写Player类继承Entity类需要有一个成员变量name类型为str代表玩家名定义一个hello函数打印"Hello 玩家名"并打印这个类的pos变量的值。
3. 实例化Player对象调用hello函数。
### 操作步骤
在PyCharm左上角的菜单栏中点击File选择New Project创建一个新项目。
路径自行选择,名称也自行命名。为了简单起见,我们不使用虚拟环境,`Python Interpreter`请选择`Previousl configured interprer`路径应为你Python的安装路径不包含venv等字样。
![](./images/10.png)
创建完成后打开main.py就可以看到已经有了一个简单的helloworld程序我们在main.py中进行编写即可。
首先编写Entity类代码如下包含了一个pos的成员变量和一个构造函数。
```python
class Entity:
pos = None
def __init__(self, pos):
self.pos = pos
```
接着继续编写Player类它继承了Entity类同时有一个name变量
```python
class Player(Entity):
name = None
def __init__(self, pos, name=""):
Entity.__init__(self, pos)
self.name = name
def hello(self):
print "Hello {}\n{}".format(self.name, self.pos)
```
随后编写主函数
```python
if __name__ == "__main__":
player = Player((0, 0, 0), "player1")
player.hello()
```
![](./images/11.png)
编写完成后运行,完整的代码和运行结果如图。

View File

@@ -0,0 +1,301 @@
---
front:
hard: 进阶
time: 20分钟
---
# 零件开发基础
本节将通过一些内置的零件模板,来介绍零件的生命周期,带领大家初步了解零件开发。
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=6328676ba240f794f8c5fbc1" width="800" height="600" allow="fullscreen"/>
## 例子
### MyLogPart
首先我们创建一个玩家预设,再在新建零件中,找到`MyLogPart`进行创建。并将MyLogPart挂接到玩家预设下。
接下来我们可以看到MyLog这个零件主要由两个python源代码文件构成分别是`MyLogPart.py``MyLogPartMeta`我们在PyCharm中打开这个项目。
![](./images/12.png)
对文件右键,打开文件所在位置,找到这个项目的根目录
![](./images/13.png)
将路径复制在PyCharm中打开并将资源包的文件夹设置为Sources Root否则补全功能将无法正常使用。
![](./images/14.png)
接下来按照如图路径打开`MyLogPart.py`,来观察它的代码。
![](./images/15.png)
可以发现,代码中有一个`MyLogPart`类,继承了`PartBase`类。在`__init__`这个初始化函数中,定义了自身的`name``description`。这两个均是从PartBase中继承下来的变量分别代表这个零件的名字和描述。
所有零件的保留变量名在下方列出了,开发者在对零件进行开发时,要避免使用这些变量名,以防出现不可预料的意外。
- id
- classType
- isClient
- filterKeys
- _parent
- entityId
- boxId
- name
- transform
- isRemoved
- loaded
- needUpdate
- <iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=63286726e6c041f2578ca816" width="800" height="600" allow="fullscreen"/>
- tickEnable
- data
- dataKeys
- eventMap
- replicated
而剩下的变量定义,均为这个类的成员变量。
`TickClient`函数是一个可重写的函数它会在客户端被每Tick调用。
并且每次都随机在30到90之间随机一个数字作为打印输出的间隔。
接下来看MyLogPartMeta.py它是继承了PartBaseMeta是用来存储在预设编辑器上的可编辑内容的
![](./images/16.png)
例如这里的interval就对应了MyLogPart.py的interval变量类型为PVector2并且可以设置提示文本具体编写格式会在后面一起介绍。
经过Meta的设置我们就可以在预设编辑器的零件的属性中看到对应的打印间隔的设置项。
![](./images/17.png)
### ReplicatePart
再次新建一个`ReplicatePart`零件,继续观察它的代码。
![](./images/18.png)
可以看到分裂零件的`__init__`函数和日志零件的大同小异,同样都是初始化数据。
这里重写了`CanAdd`函数和`InitServer`函数,其中`InitServer`代表服务器初始化,`CanAdd`会在挂载零件时触发,用来防止零件挂载到错误的预设上。
和日志相比,它多监听了一个实体受伤事件,用来在受伤时进行分裂。
它的PartMeta也是大同小异定义了两个参数分别是PBool和PInt类型。
```python
@sunshine_class_meta
class ReplicatePartMeta(PartBaseMeta):
CLASS_NAME = "ReplicatePart"
PROPERTIES = {
"loop": PBool(sort=1000, group="ReplicatePart", text="循环分裂"),
"health": PInt(sort=1001, group="ReplicatePart", text="血量"),
}
```
## 生命周期
零件的生命周期指零件在游戏从开始到结束的整个运行过程。
![](./images/19.png)
零件分服务端和客户端,在运行的各个过程,会触发不同的函数。
拿服务端来说,会在初始化阶段先`__init__`,然后`InitServer`。初始化完成便每Tick触发`TickServer`。在卸载阶段(游戏关闭/区块卸载),`UnloadServer`。在被击杀或者主动调用Destroy接口之后触发`DestroyServer`。客户端同理。
具体每个函数的定义,可以在<a href="../../../../mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.html">文档</a>中查看。同时文档中还有别的函数,可以自行查看用法,例如刚刚看到的<a href="../../../../mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.html#canadd">CanAdd函数</a>。
## 自定义属性
自定义属性就是由`PartBaseMeta`类来定义的。要创建一个自定义属性,我们主要需要有两个步骤。
1. 在继承了`PartBase`的类中定义成员变量
2. 在继承了`PartBaseMeta`的类中定义`PROPERTIES`字典其中的Key为变量名Value为这个数据对应的变量属性。
当前自定义的零件支持编辑python的所有基本类型整数int浮点数float布尔bool字符串str字典dict列表list除此之外针对一些特定需求也提供了相应的支持如下拉列表选择多维向量等。
![](./images/A1.png)
上表列出了目前所有的支持的属性变量同时列出了对应的Value定义。
每个属性的详细解释可以参考<a href="../../../../mcguide/20-玩法开发/14-预设玩法编程/2-深入理解零件/1-自定义属性面板.html?catalog=1#类型与属性">官方文档</a>。
```python
@sunshine_class_meta
class ReplicatePartMeta(PartBaseMeta):
CLASS_NAME = "ReplicatePart"
PROPERTIES = {
"loop": PBool(sort=1000, group="ReplicatePart", text="循环分裂"),
"health": PInt(sort=1001, group="ReplicatePart", text="血量"),
}
```
还是拿分裂零件的属性来说,首先要定义一个`CLASS_NAME`,这些变量是哪个类的成员变量(会自动生成,不需要手动编写)。再定义一个`PROPERTIES`,定义两个变量`loop``health`,设置顺序和所属分组以及描述文本。
## 课后作业
1. 新建空白附加包创建玩家预设并创建一个零件在零件的生命周期的每个阶段除tick都打印信息来观察执行顺序。
2. 给这个零件设置自定义属性提供2个参数分别代表玩家的最大血量和当前血量并且在玩家发送聊天消息"更新血量"的时候,应用到玩家身上。
### 操作步骤
新建附加包,创建玩家预设的操作在之前已经演示过很多遍,这里直接跳过。
接下来新建一个空零件,命名为`PlayerHealthPart`,并挂接到玩家预设上。
接下来使用PyCharm打开项目文件夹设置Sources Root打开对应的预设python文件。
文件中默认已经重写了一部分生命周期函数,我们可以在这基础上进行修改,每个都添加一个打印的函数,用来输出每个阶段。
并且额外重写`UnloadClient``UnloadServer`,加上对应输出,修改后的代码如下:
```python
# -*- coding: utf-8 -*-
from Preset.Model.GameObject import registerGenericClass
from Preset.Model.PartBase import PartBase
@registerGenericClass("PlayerHealthPartPart")
class PlayerHealthPartPart(PartBase):
def __init__(self):
PartBase.__init__(self)
# 零件名称
self.name = "空零件"
def InitClient(self):
print "InitClient"
def InitServer(self):
print "InitServer"
def TickClient(self):
pass
def TickServer(self):
pass
def DestroyClient(self):
print "DestroyClient"
def DestroyServer(self):
print "DestroyServer"
def UnloadClient(self):
print "UnloadClient"
def UnloadServer(self):
print "UnloadServer"
```
接下来新建两个变量分别是health和maxHealth代表玩家的血量和最大血量修改这个零件的name。
```python
@registerGenericClass("PlayerHealthPartPart")
class PlayerHealthPartPart(PartBase):
def __init__(self):
PartBase.__init__(self)
self.health = 20
self.maxHealth = 30
self.name = "自定义玩家血量零件"
```
并且在`PlayerHealthPartPartMeta.py`中修改`PROPERTIES`,修改后代码如下:
```python
# -*- coding: utf-8 -*-
from Meta.ClassMetaManager import sunshine_class_meta
from Meta.TypeMeta import PInt
from Preset.Model import PartBaseMeta
@sunshine_class_meta
class PlayerHealthPartPartMeta(PartBaseMeta):
CLASS_NAME = "PlayerHealthPartPart"
PROPERTIES = {
"health": PInt(sort=1, text="血量", default=20, group="自定义零件血量"),
"maxHealth": PInt(sort=2, text="最大血量", default=30, group="自定义零件血量"),
}
```
这时打开编辑器,已经可以看到零件的属性面板显示了自定义的属性。
![](./images/27.png)
然后再回到`PlayerHealthPartPart.py`中,编写设置血量的逻辑。新增一个函数,监听<a href="../../../../mcdocs/1-ModAPI/事件/世界.html?key=Join&docindex=2&type=0#serverchatevent">ServerChatEvent</a>那么我们直接判断聊天内容和发送的实体id是否是我们这个玩家然后设置给它更新血量。
```python
def ServerChatEvent(self, args):
if args["message"] != "更新血量":
return
parent = self.GetParent()
entityId = parent.GetEntityId()
if args["playerId"] != entityId:
return
self.SetEntityAttrValue(entityId, AttrType.HEALTH, self.health)
self.SetEntityAttrMaxValue(entityId, AttrType.HEALTH, self.maxHealth)
```
这样我们的血量就设置完成啦。最终代码:
```python
# -*- coding: utf-8 -*-
from Preset.Model.GameObject import registerGenericClass
from Preset.Model.PartBase import PartBase
from mod.common.minecraftEnum import AttrType
@registerGenericClass("PlayerHealthPartPart")
class PlayerHealthPartPart(PartBase):
def __init__(self):
PartBase.__init__(self)
self.health = 20
self.maxHealth = 30
self.name = "自定义玩家血量零件"
def InitClient(self):
print "InitClient"
def InitServer(self):
print "InitServer"
def TickClient(self):
pass
def TickServer(self):
pass
def ServerChatEvent(self, args):
if args["message"] != "更新血量":
return
parent = self.GetParent()
entityId = parent.GetEntityId()
if args["playerId"] != entityId:
return
self.SetEntityAttrValue(entityId, AttrType.HEALTH, self.health)
self.SetEntityAttrMaxValue(entityId, AttrType.HEALTH, self.maxHealth)
def DestroyClient(self):
print "DestroyClient"
def DestroyServer(self):
print "DestroyServer"
def UnloadClient(self):
print "UnloadClient"
def UnloadServer(self):
print "UnloadServer"
```
我们进入游戏测试,观察日志输出,可以看到有相关的生命周期输出。
![](./images/20.png)
并且发送 `更新血量`,可以看到我们的生命值被更改。
![](./images/21.png)

View File

@@ -0,0 +1,258 @@
---
front:
hard: 进阶
time: 20分钟
---
# 代码编写基础
本节将主要介绍接口和事件之间的区别、文档的查阅以及客户端和服务端之间的通信。
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=632867a9e6c041f2578ca820" width="800" height="600" allow="fullscreen"/>
## 接口与事件
这里的接口与事件和逻辑编辑器的接口和事件定义一致,如果有遗忘的同学可以去[回顾](../2-逻辑编辑器基础/3-逻辑编辑器的基础概念2.html)。
那么在逻辑编辑器中,所有事件,都体现为`监听:xxxxx`,而在零件开发中,监听事件一般都是定义一个函数。
如果需要查阅所有可以监听的事件,可以在<a href="../../../../mcdocs/1-ModAPI/事件/世界.html?catalog=1">这里</a>进行查询。
例如在`PartBase`中定义以下函数就视为监听了ServerChatEvent
```python
def ServerChatEvent(self, args):
pass
```
而在零件开发中调用接口,都是以调用函数的形式来调用的。
还是这个事件,`GetParent``SetEntityAttrValue``SetEntityAttrMaxValue`,都是调用了`PartBase`的接口。
所有可以使用的接口都可以在<a href="../../../../mcdocs/3-PresetAPI/预设管理/PresetApi.html?catalog=1">这里</a>进行查询。
```python
def ServerChatEvent(self, args):
if args["message"] != "更新血量":
return
parent = self.GetParent()
entityId = parent.GetEntityId()
if args["playerId"] != entityId:
return
self.SetEntityAttrValue(entityId, AttrType.HEALTH, self.health)
self.SetEntityAttrMaxValue(entityId, AttrType.HEALTH, self.maxHealth)
```
> 为什么<a href="../../../../mcdocs/3-PresetAPI/预设对象/预设/预设基类PresetBase.html?catalog=1#预设基类presetbase">PartBase</a>中没有找到上方代码使用的GetParent?
>
> 因为我们是基于面向对象技术进行预设和零件开发的。PartBase继承了SdkInterface和TransformObject对象自然就可以调用来自父类的函数。
>
> 实际上我们调用的GetParent接口是来自<a href="../../../../mcdocs/3-PresetAPI/预设对象/通用/变换对象TransformObject.html#getparent">TransformObject</a>的。这样GetParent因为零件挂载玩家预设上就能得到玩家。
## 服务器客户端通信
在之前的逻辑编辑器界面制作中,我们就已经稍微接触了一些服务器和客户端之间的通信。
当时的界面,将客户端中的内容,发送到了服务端。服务端监听,并执行命令。
这样就是一个客户端往服务端的通信。但是实际上,客户端和服务端之间,是可以双向通信的。下面将会详细介绍通信的使用方法。
### 客户端->服务端
客户端向服务端的通信主要需要在客户端调用`NotifyToServer`接口,而服务端需要在初始化的时候调用`ListenSelfEvent`接口,来监听这个事件。
#### NotifyToServer
文档说明:<a href="../../../../mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.html?key=NotifyToServer&docindex=2&type=0#notifytoserver">点我</a>
| 参数名 | 数据类型 | 说明 |
| :-------- | :------- | :------- |
| eventName | str | 事件名称 |
| eventData | object | 事件数据 |
这是一个客户端接口。`eventName`是事件名称。事件名称可以理解为这个事件的具体含义。客户端中使用这个事件名称发送到服务端,那么服务端相应的也需要使用这个事件名称来监听。`eventData`是事件的具体数据即一般事件中的args一般传入一个字典。
#### ListenSelfEvent
文档说明:<a href="../../../../mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.html?catalog=1#listenselfevent">点我</a>
| 参数名 | 数据类型 | 说明 |
| :-------- | :------- | :------- |
| eventName | str | 事件名称 |
| target | object | 目标 |
| func | object | 回调函数 |
这个接口,双端通用。
`eventName`就是我们NotifyToServer时所定义的事件名称。`target`为监听哪个对象的事件一般填写self。`func`为回调函数定义一个函数参数为args然后在这里传入function类型的值。
例子:
```python
class TestPartPart(PartBase):
def InitServer(self):
print "InitServer"
self.ListenSelfEvent("TestEvent", self, self.OnTestEvent)
def OnTestEvent(self, args):
print "收到来自 {} 的客户端事件".format(args["playerId"])
def SendToServer(self):
self.NotifyToServer("TestEvent", {"playerId": self.GetLocalPlayerId()})
```
这样如果在客户端调用SendToServer函数服务端就会相应的收到事件并打印消息。
### 服务端->客户端
服务端向客户端的通信主要需要在服务端调用`NotifyToClient`接口,而客户端需要在初始化的时候调用`ListenSelfEvent`接口,来监听这个事件。
#### NotifyToClient
文档说明:<a href="../../../../mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.html?catalog=1#notifytoclient">点我</a>
| 参数名 | 数据类型 | 说明 |
| :-------- | :------- | :------- |
| playerId | str | 玩家ID |
| eventName | str | 事件名称 |
| eventData | object | 事件数据 |
这是一个服务端接口。`playerId`为需要发送到的玩家id。其他的参数和`NotifyToServer`用法一致。
如果需要广播到所有玩家的客户端,可以使用<a href="../../../../mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.html?catalog=1#broadcasttoallclient">BroadcastToAllClient</a>。
#### ListenSelfEvent
ListenSelfEvent和客户端向服务端通信中的使用方法一致。
文档说明:<a href="../../../../mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.html?catalog=1#listenselfevent">点我</a>
| 参数名 | 数据类型 | 说明 |
| :-------- | :------- | :------- |
| eventName | str | 事件名称 |
| target | object | 目标 |
| func | object | 回调函数 |
这个接口,双端通用。
`eventName`就是我们NotifyToServer时所定义的事件名称。`target`为监听哪个对象的事件一般填写self。`func`为回调函数定义一个函数参数为args然后在这里传入function类型的值。
例子:
```python
class TestPartPart(PartBase):
def InitClient(self):
print "InitClient"
self.ListenSelfEvent("TestEvent", self, self.OnTestEvent)
def OnTestEvent(self, args):
print "收到来自服务端的事件 {}".format(args)
def SendToServer(self,playerId):
self.NotifyToClient(playerId, "TestEvent", {"msg": "test"})
```
这样如果在客户端调用SendToServer函数服务端就会相应的收到事件并打印消息。
## 课后作业
### 辨认接口和事件
辨别下方的代码是调用接口还是监听事件
1. `self.SetCommand()`
2. `def OnCommandOutputServerEvent(self, args):`
> 答案
>
> 1. 调用接口
> 2. 监听事件
### 零件开发实际操作
使用零件开发来编写一个爆炸箭的功能:所有射出的弓箭,在击中目标时都会产生爆炸。
并且使用自定义属性面板,设置爆炸范围。
#### 操作步骤
1. 创建一个玩家预设、空零件。零件命名为`ExplosionArrow`
2. 将零件挂接到玩家预设上。
3. 接下来使用PyCharm打开`ExplosionArrowPart.py`,编辑代码。
4.`__init__`下定义一个爆炸半径成员变量,方便后面制作自定义属性。
5. 监听<a href="../../../../mcdocs/1-ModAPI/事件/实体.html?key=ProjectileDoHitEffectEvent&docindex=2&type=0#projectiledohiteffectevent">ProjectileDoHitEffectEvent</a>,并获取对应位置,再调用<a href="../../../../mcdocs/3-PresetAPI/预设对象/通用/SDK接口封装SdkInterface.html?key=CreateExplo&docindex=4&type=0#createexplosion">CreateExplosion</a>,创建爆炸,其中爆炸半径使用成员变量,随后删除箭的实体。
代码参考:
```python
@registerGenericClass("ExplosionArrowPart")
class ExplosionArrowPart(PartBase):
def __init__(self):
PartBase.__init__(self)
self.explosionRadius = 5
self.name = "爆炸弓零件"
def ProjectileDoHitEffectEvent(self, args):
self.CreateExplosion((args["x"], args["y"], args["z"]), self.explosionRadius, True, True, args["srcId"], args["srcId"])
self.DestroyEntity(args["id"])
```
接下来设置元数据,打开`ExplosionArrowPartMeta.py`,修改`PROPERTIES`,添加一个爆炸半径变量。
代码参考:
```python
@sunshine_class_meta
class ExplosionArrowPartMeta(PartBaseMeta):
CLASS_NAME = "ExplosionArrowPart"
PROPERTIES = {
"explosionRadius": PInt(text="爆炸半径", sort=1000, default=5, group="爆炸箭头")
}
```
这样就修改完成。打开编辑器,选中爆炸弓零件,就可以看到相应的设置。
![](./images/22.png)
### 通信
利用客户端和服务端的通信系统,将玩家聊天的内容发送到客户端,并打印到日志窗口中。
#### 操作步骤
1. 新建一个空零件,命名为`NotifyTest`,并挂接在玩家预设上。
2. 编辑`NotifyTestPart.py`,监听<a href="../../../../mcdocs/1-ModAPI/事件/世界.html?key=Join&docindex=2&type=0#serverchatevent">ServerChatEvent</a>获取消息内容和玩家ID使用NotifyToClient将其发送给客户端事件名为`ChatToClient`数据是一个dict`{"msg": 消息内容}`
3. 在InitClient中调用ListenSelfEvent监听`ChatToClient`事件,再定义一个函数叫做`OnRecvChat`接收args作为参数打印`args["msg"]`,作为回调函数。
代码参考:
```python
@registerGenericClass("NotifyTestPart")
class NotifyTestPart(PartBase):
def __init__(self):
PartBase.__init__(self)
self.name = "通信测试"
def InitClient(self):
self.ListenSelfEvent("ChatToClient", self, self.OnRecvChat)
def OnRecvChat(self, args):
print "从服务端发来的聊天信息: {}".format(args["msg"])
def ServerChatEvent(self, args):
message = args["message"]
playerId = args["playerId"]
self.NotifyToClient(playerId, "ChatToClient", {"msg": message})
```
这样每次发消息,在日志窗口都会由客户端打印收到的消息内容。

View File

@@ -0,0 +1,259 @@
---
front:
hard: 进阶
time: 30分钟
---
# 使用Python编写界面逻辑
在上一章我们已经使用逻辑编辑器制作了一个简单的弹出界面用来发送title消息。
本节将会继续使用上次所制作的界面json文件使用Python零件开发来重新实现一次功能。
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=6328682ee6c041f2578ca826" width="800" height="600" allow="fullscreen"/>
在开始编写之前我们需要找到上一章使用逻辑编辑器制作界面逻辑的时候所编辑的项目然后选择导出资源找到资源包ui文件夹中当时所编辑的文件。
![](./images/23.png)
导出并保存到一个能找得到的位置,再新建一个空白附加包,将刚刚导入的界面文件导入。
导入完成之后,可以自行修改命名空间。继续新建一个界面预设,给它命名为`TitleScreen`。创建完成后勾选预加载切换到PushScreen方式打开界面。
![](./images/24.png)
到这里的操作和之前使用逻辑编辑器是完全一致的。
接下来我们就可以打开PyCharm打开这个项目的文件夹进行代码编辑第一次打开需要设置`Sources Root`,这里不再截图展示。
![](./images/25.png)
找到`脚本文件夹/uiScript/TitleScreen.py`就是我们的ui逻辑文件。
默认第二行的代码是
```python
import client.extraClientApi as clientApi
```
我们这里可以将其修改为
```python
import mod.client.extraClientApi as clientApi
```
这样就可以正常的使用补全功能,解决自动提示的报错。
观察这个类我们可以看到其实和逻辑编辑器中的蓝图零件可以重写的函数基本一致同样拥有4个函数。其中`OnActivate``OnDeactive`是只有PushScreen方式创建的界面才会被调用的函数。
使用Python代码创建的UI有两种逻辑编写的方式。一种是像逻辑编辑器一样在初始化时为按钮添加回调函数的绑定。还有一种是使用数据绑定在Json文件中提前定义好需要调用的Python文件和代码中的函数和变量进行绑定。
## 方法一
我们首先先介绍第一种方法
那么现在我们就可以在`__init__`函数中在初始化时将一些常量定义好。比如我们需要使用到的ui控件的路径。
```python
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
self.mMainPanel = "/main_panel"
self.mTitleText = self.mMainPanel + "/title_text"
self.mConfirmButton = self.mMainPanel + "/confirm_button"
```
这样就定义好了所有我们可能要用到的控件的路径,方便后面的调用。
接下来重写`Create`函数,为按钮添加回调函数。
```python
def Create(self):
"""
@description UI创建成功时调用
"""
buttonControl = self.GetBaseUIControl(self.mConfirmButton).asButton()
buttonControl.AddTouchEventParams({"isSwallow": True})
buttonControl.SetButtonTouchUpCallback(self.OnConfirmButtonClick)
def OnConfirmButtonClick(self, args):
pass
```
这样我们的在按钮点击后,就会触发`OnConfirmButtonClick`函数
接下来我们回到预设编辑器,新建一个空零件,命名为`UILogic`,并将其挂接到`TitleScreen`这个界面预设下。
我们在这个零件中监听客户端发送过来的发送Title事件。首先需要给我们的这个零件改个好记的名字这里叫做`界面服务端监听`,因为我们等会儿还需要在界面逻辑文件中,通过名字获取这个零件的示例,来给自己的服务端发送通知消息。同时在初始化服务器的时候,监听`TitleEvent`事件并发送Title指令。
代码参考如下:
```python
@registerGenericClass("UILogicPart")
class UILogicPart(PartBase):
def __init__(self):
PartBase.__init__(self)
self.name = "界面服务端监听"
def OnRecvTitle(self, args):
self.SetCommand("/title @a title {}".format(args["text"]))
def InitServer(self):
"""
@description 服务端的零件对象初始化入口
"""
self.ListenSelfEvent("TitleEvent", self, self.OnRecvTitle)
```
编写完`界面服务端监听`这个零件后,我们就可以回到界面逻辑的代码文件中,修改按钮回调函数。
在py文件开头处先引入预设API
```python
import Preset.Controller.PresetApi as presetApi
```
然后修改按钮回调函数,先获取`TitleScreen`这个预设,再获取它的零件`界面服务端监听`,随后调用`NotifyToServer`函数,发送我们的事件。
发送完成后关闭这个界面。
```python
def OnConfirmButtonClick(self, args):
text = self.GetBaseUIControl(self.mTitleText).asTextEditBox().GetEditText()
presetApi.GetPresetByName("TitleScreen").GetPartByName("界面服务端监听").NotifyToServer("TitleEvent", {"text": text})
clientApi.PopScreen()
```
完整代码如下:
```python
# -*- coding: utf-8 -*-
import Preset.Controller.PresetApi as presetApi
import mod.client.extraClientApi as clientApi
ViewBinder = clientApi.GetViewBinderCls()
ViewRequest = clientApi.GetViewViewRequestCls()
ScreenNode = clientApi.GetScreenNodeCls()
class TitleScreen(ScreenNode):
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
self.mMainPanel = "/main_panel"
self.mTitleText = self.mMainPanel + "/title_text"
self.mConfirmButton = self.mMainPanel + "/confirm_button"
def Create(self):
"""
@description UI创建成功时调用
"""
buttonControl = self.GetBaseUIControl(self.mConfirmButton).asButton()
buttonControl.AddTouchEventParams({"isSwallow": True})
buttonControl.SetButtonTouchUpCallback(self.OnConfirmButtonClick)
def OnConfirmButtonClick(self, args):
text = self.GetBaseUIControl(self.mTitleText).asTextEditBox().GetEditText()
presetApi.GetPresetByName("TitleScreen").GetPartByName("界面服务端监听").NotifyToServer("TitleEvent", {"text": text})
clientApi.PopScreen()
```
## 方法二
接下来介绍如何通过数据绑定的方式来获取文本框的数据,绑定按钮的回调函数。
在跟着方法一的步骤操作之后,方法二的区别就主要在`uiScript/TitleScreen.py`这个界面逻辑文件上。
数据绑定的官方说明文档 <a href="../../../../mcguide/18-界面与交互/70-UI数据绑定.html?catalog=1">点我</a>,可以配合本教程食用。
我们查阅UI说明文档找到我们所使用的界面控件查看他的详细Json参数。
例如我们这里使用了<a href="../../../../mcguide/18-界面与交互/30-UI说明文档.html?catalog=1#image-button">按钮</a>和<a href="../../../../mcguide/18-界面与交互/30-UI说明文档.html?catalog=1#texteditbox">文本编辑框</a>。
### 按钮
我们主要看文档中的参数解释的最后几个。
可以看到`$pressed_button_name`这个参数对应了python的类名和对应的函数。`button_mappings`代表了具体映射。
| $pressed_button_name | fpsBattle代表编写UI逻辑的python类名click代表按钮按下时会执行该python类的click函数。也可以使用API AddTouchEventHandler 动态注册按钮回调 |
| --------------------------- | ------------------------------------------------------------ |
| is_handle_button_move_event | 表示按钮是否可以响应按钮移动事件需置true配合API AddTouchEventHandler使用 |
| button_mappings | 表示按钮事件响应映射网,需置[]配合API AddTouchEventHandler使用 |
我们再打开`资源包/ui/soldier_title_screen.json`这个文件,搜索`common.button`,找到我们的按钮控件。
![](./images/26.png)
可以看到按钮的json配置中已经存在这两个值接下来我们按照要求修改删除`button_mappings`这个项,并修改`$pressed_button_name``%文件名.函数名`。具体的来说,就是修改为
````json
"$pressed_button_name" : "%TitleScreen.OnConfirmButtonClick",
````
**编辑完这个文件之后,建议不要再次用界面编辑器打开这个界面。否则有可能会被自动覆盖,覆盖后需要再次将`button_mappings`删除**
接下来回到`TitleScreen.py`,为`OnConfirmButtonClick`添加绑定,在函数的上一行,添加`@ViewBinder.binding(ViewBinder.BF_ButtonClickUp)`。
同时删除按钮监听的相关函数。
```python
def Create(self):
"""
@description UI创建成功时调用
"""
pass
@ViewBinder.binding(ViewBinder.BF_ButtonClickUp)
def OnConfirmButtonClick(self, args):
text = self.GetBaseUIControl(self.mTitleText).asTextEditBox().GetEditText()
presetApi.GetPresetByName("TitleScreen").GetPartByName("界面服务端监听").NotifyToServer("TitleEvent", {"text": text})
clientApi.PopScreen()
```
### 文本编辑框
接下来继续看文本编辑框的绑定。
绑定的参数主要是`$text_edit_box_content_binding_name`和`$text_box_name`
我们参考文档中的注1定义一个变量存储文本框中的实时内容。
然后定义2个函数一个赋值一个返回值并且修改按钮的回调函数让按钮从我们定义的这个变量获取文本内容。
```python
class TitleScreen(ScreenNode):
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
self.mText = ""
@ViewBinder.binding(ViewBinder.BF_EditChanged | ViewBinder.BF_EditFinished)
def TextBox(self, args):
self.mText = args["Text"]
return ViewRequest.Refresh
@ViewBinder.binding(ViewBinder.BF_BindString)
def ReturnTextString(self):
return self.mText
@ViewBinder.binding(ViewBinder.BF_ButtonClickUp)
def OnConfirmButtonClick(self, args):
presetApi.GetPresetByName("TitleScreen").GetPartByName("界面服务端监听").NotifyToServer("TitleEvent", {"text": self.mText})
clientApi.PopScreen()
```
那么不难发现其实数据绑定只是将一个函数绑定一个对应的类型并将其体现到json中。
例如`ReturnTextString`就返回了一个str类型的变量并且binding中的类型也是`BF_BindString`,一个文本变量。
同样的如果json中需要填写的是int类型的变量我们也可以定义一个int变量然后使用binding绑定`BF_BindInt`然后返回这个值并在json文件中修改对应的函数。
接下来我们再修改Json将对应的配置和我们的类与函数匹配。
```json
"$text_box_name" : "%TitleScreen.TextBox",
"$text_edit_box_content_binding_name" : "#TitleScreen.ReturnTextString",
```
上方截取了部分重要的json配置修改完成后就应该是这样的。
截至目前就修改完成。
界面的完整代码可以在这里[下载](https://g79.gdl.netease.com/Cpp_AddOn_PartUI.zip),其中`行为包/uiScript/TitleScreen_V1.py`为方法一的代码,供大家参考。

View File

@@ -0,0 +1,129 @@
---
front:
hard: 进阶
time: 20分钟
---
# 代码编写进阶
在已经学习了使用Python来编写逻辑玩法之后我们还需要学习如何来提高自己的代码的质量。
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=6328686fc6dfd1bb76f1d13c" width="800" height="600" allow="fullscreen"/>
## 规范
首先我们应该注意自己的代码规范。
零件开发拥有自己的<a href="../../../../mcguide/20-玩法开发/14-预设玩法编程/2-深入理解零件/6-零件开发规范.html?catalog=1">规范</a>,规范中的保留字段已经在之前介绍过了,其他部分的内容较为简单,大家可以自己查阅。
同时还有<a href="../../../../mcguide/20-玩法开发/13-模组SDK编程/20-制作规范.html?catalog=1">模组SDK</a>我们虽然是使用预设玩法和零件编程进行玩法开发但是实际上我们还是使用了部分模组SDK中的函数来进行玩法开发所以在这篇规范中我们可以参考**除了命名**以外的所有规范。命名由于涉及到一些已经保存了的预设的内部文件路径数据,所以不推荐进行修改。
同时,我们在编写代码的过程中,也需要注意编码规范。这样写出来的代码,不仅不容易出错,自己很容易看懂,也可以让团队里的其他成员更快的理解你的代码。这里我们抽取了<a href="../../../../mcguide/27-网络游戏/课程6插件教学/第1节官网插件规范.html?catalog=1">官方插件规范</a>中的一部分与代码编写相关的进行说明。
- 所有类名都是用驼峰法命名,首字母大写,比如类 GameObjectType。
- 常量都使用驼峰法命名,首字母大写,比如 ModVersion = "0.0.1"
- 类非静态成员变量使用驼峰法命名以“m”开头例如 mLevel。
- 类非静态成员函数使用驼峰法命名,首字母大写,例如 Init()。
- event使用驼峰法命名首字母大写例如“PlayerTransactionFromClientEvent”
- 统一用tab而不是四个空格缩进。
- 例子:
```python
class TitleScreen(ScreenNode):
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
self.mMainPanel = "/main_panel"
self.mTitleText = self.mMainPanel + "/title_text"
self.mConfirmButton = self.mMainPanel + "/confirm_button"
def Create(self):
"""
@description UI创建成功时调用
"""
buttonControl = self.GetBaseUIControl(self.mConfirmButton).asButton()
buttonControl.AddTouchEventParams({"isSwallow": True})
buttonControl.SetButtonTouchUpCallback(self.OnConfirmButtonClick)
def OnConfirmButtonClick(self, args):
text = self.GetBaseUIControl(self.mTitleText).asTextEditBox().GetEditText()
presetApi.GetPresetByName("TitleScreen").GetPartByName("界面服务端监听").NotifyToServer("TitleEvent", {"text": text})
clientApi.PopScreen()
```
## 技巧
### 函数的封装
例如需要求一组数据的平均值,我们可以自己封装一个函数,并在需要使用的时候进行调用,这样会大大提高代码的可读性。
```python
def avg(*args)
return sum(args) / len(args)
```
平均值只是一个比较简单的运用,如果不是像平均值这样的简单计算,大量的重复会显得代码很臃肿,并且如果在以后需要修改,就要在每个使用的地方进行修改。相对的,使用函数,我们就可以只在函数的定义处进行修改,从而减少工作量。
### 避免重复运算
假设我们需要使用模组SDK的接口制作一个连锁挖矿的功能需要调用`PlayerDestoryBlock`这个接口来挖掘方块,并且已经通过搜索方法找到了要破坏的方块列表为`blocks`
那么实际上,循环调用`PlayerDestoryBlock`这个接口时我们的blockInfoComp的对象是一模一样的。所以我们就可以将获取blockInfoComp放在循环外然后在循环内只调用`blockInfoComp.PlayerDestoryBlock`,来减少运算量。
错误的写法:
```python
for pos in blocks:
blockInfoComp = serverApi.GetEngineCompFactory().CreateBlockInfo(playerId)
blockInfoComp.PlayerDestoryBlock(pos,1,False)
```
正确的写法:
```python
blockInfoComp = serverApi.GetEngineCompFactory().CreateBlockInfo(playerId)
for pos in blocks:
blockInfoComp.PlayerDestoryBlock(pos,1,False)
```
使用正确的写法,在需要破坏的方块数量大时,极大地提高运行效率。
这种错误是很多新手都会犯的,所以我们查阅接口时,不能不假思索地直接将文档中示例代码直接复制到我们的项目中进行使用。我们应该思考代码中的每一行都是用来干什么的。同时应该清楚,每多执行一行代码,就会有更大的性能开销,如何使用速度更快的代码来实现同样的效果,是值得我们关注的。
### 使用xrange来代替range
xrange是python2独有的一个内置函数他的用法与 range 完全相同,所不同的是生成的不是一个数组,而是一个生成器。
相比range它拥有更高的执行效率。
```python
for i in xrange(5):
print i
```
### 使用dict来代替多分支if
修改前:
```python
def getXXX(a):
if a == "a":
return 1
if a == "b":
return 2
if a == "c":
return 3
return 4
```
修改后:
```python
exampleDict = {"a":1,"b":2,"c":3}
def getXXX(a):
result = exampleDict.get(a)
if not result
return result
return 4
```
相比多个if直接使用字典来存储数据并通过get来获取并返回在数据量大的时候会有更高的效率。