feat:上传mcguide-开发指南部份
256
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/0-零件开发.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# 零件开发
|
||||
|
||||
## 零件构成
|
||||
|
||||
当使用编辑器成功创建一个零件时,以创建MyLogPart为例,编辑器会自动创建以下文件:
|
||||
|
||||

|
||||
|
||||
- MyLog.part
|
||||
|
||||
MyLog.part是零件文件,它记录了该零件的有关信息。开发者无需关心该文件内容,但不建议开发者对该文件作任何形式的更改。
|
||||
|
||||
- MyLogPart.py
|
||||
|
||||
MyLogPart.py是逻辑文件,它继承自PartBase基类,拥有零件给定的生命周期函数和API接口,开发者在该文件中实现零件逻辑,如下例:
|
||||
|
||||
```python
|
||||
class MyLogPart(PartBase):
|
||||
def __init__(self):
|
||||
super(MyLogPart, self).__init__()
|
||||
self.name = "打印位置"
|
||||
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
|
||||
```
|
||||
|
||||
- MyLogPartMeta.py
|
||||
|
||||
MyLogPartMeta.py是元数据文件,当开发者需要在编辑器中对该零件的某个数据成员进行可视化编辑时,可以将该数据成员以一定的规则编写在该Meta文件中,如下例:
|
||||
|
||||
```python
|
||||
class MyLogPartMeta(PartBaseMeta):
|
||||
CLASS_NAME = "MyLogPart"
|
||||
PROPERTIES = {
|
||||
"interval": PVector2(sort=1000, group="MyLogPart", text="打印间隔"),
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 零件编辑
|
||||
|
||||
- 本体
|
||||
|
||||
当创建一个零件后,零件将作为代码资源存放在资源管理器中。单纯的零件是不会有作用的,只有依赖于预设,即将零件挂接在预设下并且该预设被实例化,零件的代码逻辑才会正常运行。
|
||||
|
||||
- 实例
|
||||
|
||||
零件被挂接到预设下,将该零件称为零件实例,实例可以看作对零件的引用。对零件实例的修改会被保存在预设文件中,而不会对零件代码进行修改。相同的,当零件代码被修改时,所有该零件的实例都会同步这一修改,但预设文件中对零件实例的修改仍保留且优先于零件代码生效。
|
||||
|
||||
- 变量与自定义属性
|
||||
|
||||
零件中的变量被可视化后才可以在编辑器中动态的、可视化的编辑,才能在被挂接到预设下后被覆写。详情见[自定义属性面板](1-自定义属性面板.md)
|
||||
|
||||
|
||||
|
||||
## 内置零件
|
||||
|
||||
我们的游戏引擎内置了一些内置零件,开发者无法对内置零件的逻辑进行修改,也无法像模板零件一样创建出零件副本。开发者可以为预设创建内置零件,并修改可以修改的数据成员。
|
||||
|
||||

|
||||
|
||||
目前一共内置了7个零件。
|
||||
|
||||
- PlayerBasicPart
|
||||
|
||||
玩家基础数据零件,可以设置玩家基础数据,该零件只能挂在玩家预设下。
|
||||
|
||||
- WorldPart
|
||||
|
||||
世界属性零件,可以设置世界属性,该零件建议挂接在设置了预加载的预设下。
|
||||
|
||||
- TriggerPart
|
||||
|
||||
触发器零件,可以设置触发条件等。
|
||||
|
||||
- PortalPart
|
||||
|
||||
传送门零件,可以设置传送门及其形状。
|
||||
|
||||
- EntityBasePart
|
||||
|
||||
实体零件,可以在指定位置生成实体生物
|
||||
|
||||
- NavPointsPart
|
||||
|
||||
路径点零件,可以在编辑器中设置相对于零件位置选择一系列路径点,并在运行时通过接口获得路径点的绝对坐标列表。
|
||||
|
||||
- CameraTrackPart
|
||||
|
||||
相机轨迹零件,可以在编辑器中设置相机移动轨迹,并在运行时通过接口播放。
|
||||
|
||||
## 模板零件
|
||||
|
||||
我们的编辑器内置了一些模板零件,开发者可以通过创建模板零件创建出该零件的副本,从而对该零件进行自定义的修改。
|
||||
|
||||

|
||||
|
||||
目前一共内置了13个模板零件
|
||||
|
||||
- AddPartTestPart
|
||||
|
||||
添加零件测试零件
|
||||
|
||||
- ApiTestPart
|
||||
|
||||
零件接口测试零件
|
||||
|
||||
- MyLogPart
|
||||
|
||||
日志零件,用于测试调试日志。
|
||||
|
||||
- NetworkReplicatedPart
|
||||
|
||||
网络同步零件
|
||||
|
||||
- ReplicatePart
|
||||
|
||||
分裂零件
|
||||
|
||||
- TransformAnimationPart
|
||||
|
||||
变化动画零件
|
||||
|
||||
- TriggerTestPart
|
||||
|
||||
测试触发器零件,可以和TriggerPart配合使用。
|
||||
|
||||
- BlueprintPart
|
||||
|
||||
蓝图零件
|
||||
|
||||
- BlueprintReplicatePart
|
||||
|
||||
蓝图分裂零件
|
||||
|
||||
- BlueprintUIPart
|
||||
|
||||
蓝图UI零件
|
||||
|
||||
- EtsTestPart
|
||||
|
||||
Ets测试零件
|
||||
|
||||
- InputBindPart
|
||||
|
||||
输入绑定零件
|
||||
|
||||
- PresetDebugPart
|
||||
|
||||
预设测试零件
|
||||
|
||||
## 零件热更
|
||||
|
||||
**返回编辑器时**,零件代码会自动热更并更新当前舞台内的实例,属性刷新
|
||||
|
||||
## 零件事件
|
||||
|
||||
### 监听SDK事件
|
||||
|
||||
零件提供了两种可以监听SDK事件的方式。
|
||||
|
||||
#### 拥有各种ID的的SDK事件可直接重载监听
|
||||
|
||||
SDK事件中有相当数量的参数中包含实例ID的事件,当此类事件发生时,SDK会在抛出事件的同时通过参数告知我们是哪个ID的实例是这个事件的当事人,对于这些事件的监听我们可以直接重载与事件同名的接口,并将零件挂接到具有实体的预设(如玩家预设、实体预设)上来实现。当事件被提起时所包含的实例ID与该预设的实例ID相匹配时该接口将会被调用。SDK事件中包含以下参数的事件都可以用此方法进行监听。
|
||||
|
||||
"id", "entityId", "playerId", "rideId", "actorId", "victimId", "targetId", "srcId", "projectileId", "spawnerId",
|
||||
"sourceId", "attacker", "victim", "src", "actor", "secondaryActor", "dieEntityId","victims", "playerList"
|
||||
|
||||
以ReplicatePart为例
|
||||
|
||||
```python
|
||||
class ReplicatePart(PartBase):
|
||||
def __init__(self):
|
||||
super(ReplicatePart, self).__init__()
|
||||
......
|
||||
|
||||
def ActuallyHurtServerEvent(self, args):
|
||||
# 生物Id
|
||||
entityId = data["entityId"]
|
||||
# 伤害来源,详见Minecraft枚举值文档的ActorDamageCause
|
||||
cause = data["cause"]
|
||||
# 伤害值
|
||||
damage = data["damage"]
|
||||
# 吸收的伤害值(原始伤害减去damage)
|
||||
absorbedDamage = data["absorbedDamage"]
|
||||
......
|
||||
```
|
||||
|
||||
该零件重载了ActuallyHurtServerEvent事件同名函数并实现了分裂该实体预设的功能。
|
||||
|
||||
#### 使用监听引擎事件接口监听事件
|
||||
|
||||
零件基类PartBase提供了ListenForEvent接口和UnListenForEvent接口用于注册和反注册引擎事件监听。详情见<a href="../../../../mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.html" rel="noopenner"> 零件PartBase </a>
|
||||
|
||||
### 监听特定零件事件
|
||||
|
||||
零件基类PartBase提供了ListenPartEvent,BroadcastClientEvent等接口用于零件之间的事件通讯,相关接口详情见<a href="../../../../mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.html" rel="noopenner"> 零件PartBase </a>
|
||||
|
||||
以TriggerPart和TriggerTestPart为例
|
||||
|
||||
TriggerPart在Tick函数中,满足一定条件时广播给客户端OnTriggerEntityEnter事件
|
||||
|
||||
```python
|
||||
class TriggerPart(PartBase):
|
||||
def __init__(self):
|
||||
super(TriggerPart, self).__init__()
|
||||
......
|
||||
|
||||
def TickClient(self, args):
|
||||
......
|
||||
if self.isTriggerEnter and len(enter_entities) > 0:
|
||||
data = {
|
||||
'TriggerPart': self,
|
||||
'EnterEntityIds': enter_entities
|
||||
}
|
||||
self.BroadcastClientEvent("OnTriggerEntityEnter", data)
|
||||
......
|
||||
```
|
||||
|
||||
TriggerTestPart对该事件注册监听事件
|
||||
|
||||
```python
|
||||
class TriggerTestPart(PartBase):
|
||||
def __init__(self):
|
||||
super(TriggerTestPart, self).__init__()
|
||||
......
|
||||
|
||||
def InitClient(self):
|
||||
......
|
||||
id = part.id
|
||||
self.ListenPartClientEvent(id, "OnTriggerEntityEnter", self, self.OnTriggerEntityEnter)
|
||||
|
||||
def OnTriggerEntityEnter(self, arg):
|
||||
print("TriggerTestPart OnTriggerEntityEnter", arg)
|
||||
```
|
||||
|
||||
### 监听预设系统事件
|
||||
|
||||
参考<a href="./4-零件通讯教程.html?catalog=1" rel="noopenner">零件通讯</a>
|
||||
|
||||
## 开发调试
|
||||
|
||||
零件的逻辑调试写法和先前的脚本开发调试写法一致,开发者可以在适当的零件代码位置打印日志,在进行开发中,这些日志将会被打印在调试日志窗口中,供开发者进行开发调试。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
603
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/1-自定义属性面板.md
Normal file
@@ -0,0 +1,603 @@
|
||||
# 自定义属性面板
|
||||
|
||||
## 前言
|
||||
|
||||
一个完整的零件一般主要有3个文件,以自定义零件中的PortalPart为例,包含Portal.part,PortalMeta.py,Portal.py。
|
||||
|
||||

|
||||
|
||||
其中Portal.py中定义了一个继承自PartBase的类PortalPart,该类的实例属性诸如"\__init__"方法中的一些"self."开头的变量可通过编辑器的属性面板来进行配置。
|
||||
|
||||

|
||||
|
||||
PortalMeta.py文件中则定义了各个变量希望在编辑器中呈现的编辑方式。
|
||||
|
||||

|
||||
|
||||
在编辑器中点击对应的".part"文件即可在属性面板中预览并编辑对应的属性。
|
||||
|
||||

|
||||
|
||||
## 总览
|
||||
|
||||
当前自定义的零件支持编辑python的所有基本类型,即:整数int,浮点数float,布尔bool,字符串str,字典dict,列表list,除此之外,针对一些特定需求,也提供了相应的支持,如下拉列表选择,多维向量等。
|
||||
|
||||
下图给出了所有的基本控件样式与写法。代码可在附录中找到。
|
||||
|
||||

|
||||
|
||||
## 类型与属性
|
||||
|
||||
### 通用属性
|
||||
|
||||
| 属性 | 默认值 | 说明 |
|
||||
| :------- | :----- | :----------------------------------------------------------- |
|
||||
| default | None | 属性默认值,当实际值与默认值不相等时会提供一个按钮设为默认值 |
|
||||
| text | "" | 属性在编辑器中的显示名称 |
|
||||
| sort | 0 | 属性显示顺序,数字越小越靠前 |
|
||||
| editable | True | 属性在编辑器中是否可编辑(反过来就是是否只读) |
|
||||
| tip | "" | 鼠标移上去显示的Tips |
|
||||
| visible | True | 属性是否可见 |
|
||||
| group | "" | 所属分组,可用于简单布局。(只适用于第一级结点) |
|
||||
|
||||
### 整数:PInt
|
||||
|
||||
除通用属性外的额外属性:
|
||||
|
||||
| 属性 | 默认值 | 说明 |
|
||||
| :--- | :----- | :---------------------------------------------- |
|
||||
| min | None | 最小值 |
|
||||
| max | None | 最大值 |
|
||||
| step | 1 | 步进量,即每次点击控件中增加/减少按钮修改的差值 |
|
||||
|
||||
### 浮点数:PFloat
|
||||
|
||||
除通用属性外的额外属性:
|
||||
|
||||
| 属性 | 默认值 | 说明 |
|
||||
| :-------- | :----- | :---------------------------------------------- |
|
||||
| min | None | 最小值 |
|
||||
| max | None | 最大值 |
|
||||
| step | 1.0 | 步进量,即每次点击控件中增加/减少按钮修改的差值 |
|
||||
| precision | 2 | 小数点后位数 |
|
||||
|
||||
### 布尔值:PBool
|
||||
|
||||
无额外属性
|
||||
|
||||
### 字符串:PStr
|
||||
|
||||
除通用属性外的额外属性:
|
||||
|
||||
| 属性 | 默认值 | 说明 |
|
||||
| :---- | :----- | :----------------------- |
|
||||
| regex | None | 内容需要符合的正则表达式 |
|
||||
|
||||
### 列表:PArray
|
||||
|
||||
除通用属性外的额外属性:
|
||||
|
||||
| 属性 | 默认值 | 说明 |
|
||||
| :------------- | :----- | :------------------------------------------ |
|
||||
| childAttribute | None | 子元素的Meta定义,如:childAttribute=PStr() |
|
||||
| maxSize | 9999 | 最大长度 |
|
||||
| moveChild | False | 子元素添加可以上下移动按钮 |
|
||||
| insertChild | False | 子元素添加插入新元素到当前位置按钮 |
|
||||
|
||||
### 字典:PDict
|
||||
|
||||
除通用属性外的额外属性:
|
||||
|
||||
| 属性 | 默认值 | 说明 |
|
||||
| :-------- | :----- | :-------------------------------------------------------- |
|
||||
| addable | False | 允许动态增加子元素,该子元素的key需要存在于此Meta定义中。 |
|
||||
| removable | False | 允许动态删除已存在的子元素 |
|
||||
| fixList | [] | 不受动态增删影响的固定属性,如["key1"] |
|
||||
|
||||
### 枚举类型:PEnum
|
||||
|
||||
除通用属性外的额外属性:
|
||||
|
||||
| 属性 | 默认值 | 说明 |
|
||||
| :------- | :----- | :----------------------------------------------------------- |
|
||||
| enumType | "" | 使用DefEnum定义的枚举类型数据名称,如DefEnum("MyEnum", {1: "a", 2: "b"}),这里就填"MyEnum" |
|
||||
|
||||
### 多维向量:PVector2,PVector3,PVector4
|
||||
|
||||
除通用属性外的额外属性:
|
||||
|
||||
| 属性 | 默认值 | 说明 |
|
||||
| :-------- | :----- | :---------------------------------------------- |
|
||||
| min | None | 最小值 |
|
||||
| max | None | 最大值 |
|
||||
| step | 1.0 | 步进量,即每次点击控件中增加/减少按钮修改的差值 |
|
||||
| precision | 2 | 小数点后位数 |
|
||||
|
||||
### 颜色:PColor
|
||||
|
||||
除通用属性外的额外属性:
|
||||
|
||||
| 属性 | 默认值 | 说明 |
|
||||
| :----- | :----- | :----------------------------------------------------------- |
|
||||
| format | (0,0,0)或"#000000" | 颜色格式:当format="#RRGGBB"(现在只有这一种),此时,实际值形如:"#A6B7C8",当不传format参数时或传的值不支持时,会使用默认格式(r, g, b)。如:(244, 37, 18)|
|
||||
|
||||
### 对象数组:PObjectArray
|
||||
|
||||
除通用属性外的额外属性:
|
||||
|
||||
| 属性 | 默认值 | 说明 |
|
||||
| :---------- | :----- | :--------------------------------- |
|
||||
| itemCreator | None | 继承于PropertyListObject的自定义类 |
|
||||
| moveChild | False | 子元素添加可以上下移动按钮 |
|
||||
| insertChild | False | 子元素添加插入新元素到当前位置按钮 |
|
||||
|
||||
对于较为复杂的编辑数据,我们除了可以使用列表PArray、字典PDict进行多层嵌套以外,还可以使用对象数组。
|
||||
|
||||
首先我们需要创建一个新的编辑对象类,它需要继承PropertyListObject
|
||||
|
||||
```python
|
||||
from Meta.PropertyObject import sunshine_property_object
|
||||
from Preset.Model.PresetDataMeta import PropertyListObject
|
||||
|
||||
@sunshine_property_object
|
||||
class EventDataObject(PropertyListObject):
|
||||
PROPERTIES = {}
|
||||
```
|
||||
|
||||
将EventDataObject用到对象数组PObjectArray中:
|
||||
|
||||
```python
|
||||
@sunshine_class_meta
|
||||
class LevelStagePartMeta(StageBasePartMeta):
|
||||
CLASS_NAME = "LevelStagePart"
|
||||
ObjectArrayDef = {
|
||||
"events": EventDataObject,
|
||||
}
|
||||
PROPERTIES = {
|
||||
"events": PObjectArray(
|
||||
text="刷怪事件", sort=1004, group="阶段",
|
||||
itemCreator=EventDataObject
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
ObjectArrayDef和PROPERTIES的key必须保持一致,EventDataObject作为itemCreator参数传入。
|
||||
|
||||
这种方式除了让层级更为清晰之外,更重要的是可以隔离自定义函数func传入的obj。
|
||||
|
||||
上面这个例子里,EventDataObject里属性func所传入的是EventDataObject,而不是根节点LevelStagePart。
|
||||
|
||||
当你需要控制列表内单个对象的不同属性的互相影响时,必须使用对象数组,比如我们通过condition属性来控制dependencyEvents和startTime属性的显隐。
|
||||
|
||||
```python
|
||||
class EventDataObject(PropertyListObject):
|
||||
PROPERTIES = {
|
||||
"classType": PStr(sort=0, text="类名", default="SpawnMobEvent", visible=False),
|
||||
"condition": PEnum(sort=2, text="事件触发条件", enumType='ConditionType'),
|
||||
"dependencyEvents": PArray(
|
||||
sort=3, text="条件事件", childAttribute=PCustom(text='事件ID', sort=0, editAttribute='MCEnum', extend=getEvents),
|
||||
func=lambda obj: {'visible': obj.condition == "dependency"}
|
||||
),
|
||||
"startTime": PInt(
|
||||
text="间隔时间", sort=4, default=10,
|
||||
func=lambda obj: {'visible': obj.condition == "timer"}
|
||||
),
|
||||
}
|
||||
```
|
||||
|
||||
这里的PROPERTIES每个编辑子项的写法与前面介绍的一致。
|
||||
|
||||
实际效果:
|
||||
|
||||

|
||||
|
||||
### 其他
|
||||
- PCoordinate
|
||||
继承自 PVector3,具有与PVector3相同的属性和功能,在原有功能基础上添加了两个辅助按钮,分别是选择地图位置,定位位置。
|
||||
|
||||
- PGameObjectArea
|
||||
区域选定组件, 可视化当前设置的区域大小。
|
||||
继承自PDict, children字段必须为:
|
||||
```python
|
||||
{
|
||||
'min': PVector3(sort=0, text="顶点1"),
|
||||
'max': PVector3(sort=1, text="顶点2"),
|
||||
'dimensionId': PInt(sort=2, text="维度")
|
||||
}
|
||||
```
|
||||
|
||||
- PCustom
|
||||
用于扩展的自定义控件类型,editAttribute属性用于设置扩展的控件的类型
|
||||
注意:
|
||||
|
||||
- PCustom 很多属性是随着editAttribute的不同设置而有所不同。
|
||||
|
||||
- customFunc 和func 属性没有关系,func是用于动态设置属性的,即可以通过Func属性动态修改控件的text , visible , sort 等属性,customFunc 是某些拓展组件用于设置数据更新函数。
|
||||
|
||||
实例代码:
|
||||
```python
|
||||
PROPERTIES = {
|
||||
"MCEnum": PCustom(sort=0, text="MCEnum", group="custom", editAttribute="MCEnum", customFunc=updateMCEnum),
|
||||
"MCBlock": PCustom(editAttribute="MCBlock", default=('dirt', 0), group="custom", sort=1),
|
||||
'MCBlockName': PCustom(sort=2, editAttribute="MCBlockName", group="custom"),
|
||||
"SelectItemWidget": PCustom(sort=3, editAttribute="SelectItemWidget", group="custom"),
|
||||
"MCEnumEx": PCustom(sort=4, editAttribute="MCEnumEx", customFunc=updateMCEnum, group="custom"),
|
||||
"MCCustomCreature": PCustom(sort=5, editAttribute="MCCustomCreature", group="custom"),
|
||||
'MCItems': PCustom(
|
||||
sort=6,
|
||||
editAttribute='MCItems',
|
||||
default=('minecraft:apple', 0),
|
||||
withNamespace=False,
|
||||
withAuxValue=True,
|
||||
isBlock=None,
|
||||
itemCategory=8,
|
||||
categoryList=[8, 31],
|
||||
group="custom"
|
||||
),
|
||||
'MCResource': PCustom(
|
||||
sort=7, tip='背包格子中该物品显示的贴图。',
|
||||
editAttribute="MCResource",
|
||||
baseFolder=["textures"],
|
||||
relativePath=['items'],
|
||||
extension='.png',
|
||||
default='textures/items/apple',
|
||||
description='image',
|
||||
withExtension=True,
|
||||
group="custom"
|
||||
),
|
||||
'MCTipWidget': PCustom(
|
||||
group="custom",
|
||||
sort=8, editAttribute="MCTipWidget", text="MCTipWidget",
|
||||
content="说明内容:",
|
||||
linkText="链接文本>>",
|
||||
linkUrl="https://www.baidu.com")
|
||||
}
|
||||
```
|
||||
效果:
|
||||

|
||||
|
||||
下面是各种扩展控件用法的说明:
|
||||
|
||||
- editAttribute="MCEnum":
|
||||
|
||||
这是一个自定义的枚举控件,相比PEnum这个会自动添加 "":"空" 的默认枚举子项
|
||||
|
||||
跟随属性:
|
||||
|
||||
1. customFunc=updateMCEnum //必要
|
||||
updateMCEnum是一个参数为当前零件逻辑对象放回值为一个key值和value都为str类型的字典的函数,该返回值将作为枚举控件的选项数据显示。
|
||||
1. searchable=False
|
||||
是否以搜索样式显示
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
PROPERTIES = {
|
||||
"MCEnum": PCustom(sort=0, text="MCEnum", editAttribute="MCEnum", customFunc=updateMCEnum),
|
||||
}
|
||||
...
|
||||
def updateMCEnum(obj):
|
||||
return {
|
||||
"A" : "1",
|
||||
"B" : "2",
|
||||
"C" : "dd",
|
||||
}
|
||||
```
|
||||
|
||||
- editAttribute="MCTipWidget":
|
||||
|
||||
这是一个自定义的说明控件,用来显示一些必要的提示,注意链接最后不要有/
|
||||
|
||||
example:
|
||||
```python
|
||||
'MCTipWidget': PCustom(
|
||||
group="custom",
|
||||
sort=8, editAttribute="MCTipWidget", text="MCTipWidget",
|
||||
content="说明内容:",
|
||||
linkText="链接文本>>",
|
||||
linkUrl="https://www.baidu.com")
|
||||
```
|
||||
|
||||
- editAttribute="MCBlock":
|
||||
|
||||
选取原版方块的控件,返回值是一个第一个元素是去命名空间的identifier的tuple,如:('birch_button', 0)
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
"MCBlock": PCustom(
|
||||
text="原生方块", editAttribute="MCBlock", default=('dirt', 0)
|
||||
)
|
||||
```
|
||||
|
||||
- editAttribute="MCBlockName":
|
||||
|
||||
选取原版方块的控件,返回值是方块的identifier , 如 "minecraft:stone"
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
'MCBlockName': PCustom(sort=4, editAttribute="MCBlockName"),
|
||||
```
|
||||
|
||||
- editAttribute="SelectItemWidget":
|
||||
|
||||
选取mc物品的控件,只显示图标,返回值是形如{'itemName': 'minecraft:stripped_crimson_hyphae', 'auxValue': 0}的dict,
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
"SelectItemWidget": PCustom(sort=11, text="物品",
|
||||
editAttribute="SelectItemWidget"),
|
||||
```
|
||||
|
||||
|
||||
- editAttribute="MCEnumEx"
|
||||
|
||||
效果同editAttribute="MCEnum",可以支持当前值不在枚举列表的情况。
|
||||
|
||||
- editAttribute="MCCustomCreature":
|
||||
|
||||
自定义生物选取控件,返回值是生物的identifier
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
"MCCustomCreature":PCustom(sort=3, editAttribute="MCCustomCreature"),
|
||||
```
|
||||
|
||||
- editAttribute='MCItems':
|
||||
|
||||
选取mc物品的控件,显示小图标和名字
|
||||
跟随属性:
|
||||
1. withNamespace:bool(False)
|
||||
返回值是否带命名空间 默认值False
|
||||
|
||||
2. withAuxValue:bool(True)
|
||||
是否返回Aux值 ,该值用于指定子物品分类id 默认值True
|
||||
|
||||
3. isBlock:bool(None)
|
||||
|
||||
选择列表是否需要方块,三种情况:None 所有物品,True 只有方块,False 只没有方块,默认值为None
|
||||
|
||||
4. itemCategory:int(31)
|
||||
默认选中分类,与categoryList配合使用
|
||||
|
||||
建筑 = 1
|
||||
自然 = 8
|
||||
物品 = 4
|
||||
装备 = 2
|
||||
全部 = 31
|
||||
|
||||
5. categoryList;[int]
|
||||
|
||||
这是一个数组,用于设置显示物品分类列表页的内容,物品分类参考 itemCategory,默认值是None,会显示所有分类页
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
'MCItems': PCustom(
|
||||
sort=4,
|
||||
editAttribute='MCItems',
|
||||
default=('minecraft:apple', 0),
|
||||
withNamespace=False,
|
||||
withAuxValue=True,
|
||||
isBlock=None,
|
||||
itemCategory=16,
|
||||
categoryList=[16,31]
|
||||
),
|
||||
...
|
||||
self.MCItems = ('end_bricks', 0)
|
||||
```
|
||||
返回值:如果有多个默认值则使用tuple类型,withAuxValue与的设置相关
|
||||
|
||||
- editAttribute="MCResource":
|
||||
|
||||
文件资源选取控件,返回相对于当前存档路径的相对路径
|
||||
|
||||
跟随属性:
|
||||
1. extension: [str]
|
||||
|
||||
过滤文件后缀名
|
||||
|
||||
2. pack:str(None)
|
||||
|
||||
如果是“b”字符串则以行为包里的文件夹为查找目录,否则是资源包的文件夹,默认值None
|
||||
|
||||
3. baseFolder:[str]
|
||||
|
||||
字符串数组,文件夹路径数组,用于设置搜索路径,返回结果不包含此路径 默认值[]
|
||||
|
||||
4. relativePath:[str]
|
||||
|
||||
字符串数组,文件夹路径数组,在baseFolder的基础上继续设置搜索路径,返回结果包含此路径 默认值[]
|
||||
|
||||
5. recursive: bool(False)
|
||||
|
||||
是否递归遍历文件搜索文件,如果False只会搜索当前路径(由pack,baseFolder,relativePath决定)下的文件,默认值False
|
||||
|
||||
6. withExtension:bool(False)
|
||||
|
||||
返回值是否带文件后缀
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
'MCResource': PCustom(
|
||||
sort=4, tip='背包格子中该物品显示的贴图。',
|
||||
editAttribute="MCResource",
|
||||
baseFolder = ["textures"],
|
||||
relativePath=['items'],
|
||||
extension='.png',
|
||||
default='textures/items/apple',
|
||||
description='image',
|
||||
withExtension= True,
|
||||
)
|
||||
...
|
||||
self.MCResource = "items/apple.png"
|
||||
```
|
||||
|
||||
- 原生/自定义生物下拉列表
|
||||
|
||||
在编辑器内,我们提供了EntityManager单例,它提供了如下接口:
|
||||
|
||||
- getCreatureEnum:原生生物和自定义生物
|
||||
- getExtraCreatureEnum:自定义生物
|
||||
|
||||
利用这些接口,结合PCustom,可以定制编辑器的生物下拉列表,以下为内置的实体零件样例:
|
||||
|
||||
```python
|
||||
from Meta.ClassMetaManager import sunshine_class_meta
|
||||
from Meta.TypeMeta import PBool, PStr, PInt, PCustom, PVector3, PVector3TF, PEnum, PDict, PFloat, PArray, PVector2
|
||||
from MC.World.EntityManager import EntityManager
|
||||
from Preset.Model import PartBaseMeta
|
||||
|
||||
@sunshine_class_meta
|
||||
class EntityBasePartMeta(PartBaseMeta):
|
||||
CLASS_NAME = "EntityBasePart"
|
||||
PROPERTIES = {
|
||||
"engineType": PCustom(sort=101, group="实体", text="实体类型", editAttribute="MCEnum", customFunc=lambda _: EntityManager.getCreatureEnum()),
|
||||
}
|
||||
```
|
||||
|
||||
- 原生/自定义物品选择
|
||||
|
||||
```python
|
||||
"item": PCustom(sort=102, text="类型", editAttribute='MCItems', withAuxValue=True, withNamespace=True),
|
||||
```
|
||||
|
||||
- func 属性说明实例
|
||||
|
||||
```python
|
||||
def updateMCEnum(obj):
|
||||
return {
|
||||
"d": "2",
|
||||
"b": "1",
|
||||
}
|
||||
def updateMCEnum1(obj):
|
||||
return {
|
||||
"d": "2d",
|
||||
"b": "1d",
|
||||
}
|
||||
|
||||
def fun1():
|
||||
return {"visible": True, "text": "hello", "customFunc": updateMCEnum1}
|
||||
|
||||
@sunshine_class_meta
|
||||
class EmptyPartMeta(PartBaseMeta):
|
||||
CLASS_NAME = "EmptyPart"
|
||||
PROPERTIES = {
|
||||
"test": PCustom(sort=1, text="test", func=fun1, editAttribute="MCEnum", customFunc=updateMCEnum)
|
||||
}
|
||||
```
|
||||

|
||||
如上所示组件通过func属性动态修改了text属性和customFunc属性,func所指向的函数会在当前编辑数据有变化时被调用。
|
||||
|
||||
|
||||
## 附录:
|
||||
|
||||
### 示例
|
||||
|
||||
#### MyPart.py
|
||||
|
||||
```python
|
||||
from Preset.Model.PartBase import PartBase
|
||||
from Preset.Model.GameObject import registerGenericClass
|
||||
|
||||
@registerGenericClass("MyPart")
|
||||
class MyPart(PartBase):
|
||||
def __init__(self, uuid):
|
||||
super(SafeAreaData, self).__init__(uuid)
|
||||
self.int1 = 1
|
||||
self.int2 = 2
|
||||
self.float1 = 1.1
|
||||
self.float2 = 2.2
|
||||
self.bool1 = True
|
||||
self.bool2 = False
|
||||
self.str1 = "str1"
|
||||
self.str2 = "str2"
|
||||
self.enum1 = 1
|
||||
self.enum2 = "zombie"
|
||||
self.vector2 = [1.0, 2.0]
|
||||
self.vector3 = [1.0, 2.0, 3.0]
|
||||
self.vector4 = [1.0, 2.0, 3.0, 4.0]
|
||||
self.color1 = [10, 20, 30]
|
||||
self.color2 = "#A6B7C8"
|
||||
self.array1 = ["item1", "item2", "item3"]
|
||||
self.array2 = [1, 2, 3]
|
||||
self.dict1 = {"key1": 1, "key2": "str_2"}
|
||||
self.dict2 = {"key1": 1, "key2": "str_2"}
|
||||
|
||||
self.groupedInt = 1
|
||||
self.groupedFloat = 1.1
|
||||
self.groupedBool = True
|
||||
self.groupedStr = "grouped str"
|
||||
self.groupedEnum = "pig"
|
||||
self.groupedVector3 = [1.0, 2.0, 3.0]
|
||||
self.groupedColor = [40, 50, 60]
|
||||
self.groupedArray = ["item1", "item2", "item3"]
|
||||
self.groupedDict = {"key1": 1, "key2": "str_2"}
|
||||
```
|
||||
|
||||
#### MyPartMeta.py
|
||||
|
||||
```python
|
||||
from Meta.ClassMetaManager import sunshine_class_meta
|
||||
from Meta.EnumMeta import DefEnum
|
||||
from Meta.TypeMeta import PInt, PEnum
|
||||
from Preset.Model import PartBaseMeta
|
||||
|
||||
|
||||
DefEnum("IntOption", {1: "选项1", 2: "选项2"})
|
||||
DefEnum("StrOption", {"zombie": "僵尸", "pig": "猪"})
|
||||
|
||||
|
||||
@sunshine_class_meta
|
||||
class SafeAreaDataMeta(PartBaseMeta):
|
||||
CLASS_NAME = "SafeAreaData"
|
||||
PROPERTIES = {
|
||||
"int1": PInt(text="整数1", sort=1, default=1, tip="这是个整数"),
|
||||
"int2": PInt(text="整数2", sort=2, default=1),
|
||||
"float1": PFloat(text="浮点数1", sort=3, default=1.1),
|
||||
"float2": PFloat(text="浮点数2", sort=4, default=2.0),
|
||||
"bool1": PBool(text="Bool1", sort=5, default=False),
|
||||
"bool2": PBool(text="Bool2", sort=6),
|
||||
"str1": PStr(text="字符串1", sort=7, default=""),
|
||||
"str2": PStr(text="字符串2", sort=8, default="default"),
|
||||
"enum1": PEnum(text="枚举1", sort=9, enumType="IntOption"),
|
||||
"enum2": PEnum(text="枚举2", sort=10, enumType="StrOption"),
|
||||
"vector2": PVector2(text="二维向量", sort=11),
|
||||
"vector3": PVector3(text="三维向量", sort=13),
|
||||
"vector4": PVector4(text="四维向量", sort=15),
|
||||
"color1": PColor(text="颜色1", sort=16),
|
||||
"color2": PColor(text="颜色2", sort=17, format="#RRGGBB"),
|
||||
"array1": PArray(text="字符串数组", sort=18, childAttribute=PStr()),
|
||||
"array2": PArray(text="整数数组", sort=19, childAttribute=PInt()),
|
||||
"dict1": PDict(text="字典1", sort=20, children={
|
||||
"key1": PInt(),
|
||||
"key2": PStr(),
|
||||
}),
|
||||
"dict2": PDict(text="字典2", sort=21, removable=True, addable=True, children={
|
||||
"key1": PInt(),
|
||||
"key2": PStr(),
|
||||
}),
|
||||
"groupedInt": PInt(text="组内整数", sort=22, group="分组1:数字"),
|
||||
"groupedFloat": PFloat(text="组内浮点数", sort=23, group="分组1:数字"),
|
||||
"groupedBool": PBool(text="组内Bool", sort=24, group="分组2:其他"),
|
||||
"groupedStr": PStr(text="组内字符串", sort=25, group="分组2:其他"),
|
||||
"groupedEnum": PEnum(text="组内枚举", sort=26, enumType="StrOption", group="分组2:其他"),
|
||||
"groupedVector3": PVector3(text="组内Vector3", sort=27, group="分组2:其他"),
|
||||
"groupedColor": PColor(text="组内颜色", sort=28, group="分组2:其他"),
|
||||
"groupedArray": PArray(text="组内数组", sort=29, childAttribute=PStr(), group="分组2:其他"),
|
||||
"groupedDict": PDict(text="组内字典", sort=30, group="分组2:其他", children={
|
||||
"key1": PInt(),
|
||||
"key2": PStr(),
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
#### 实际效果
|
||||
|
||||

|
||||
|
||||

|
||||
168
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/2-自定义属性零件模板.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# 自定义属性零件示例模板
|
||||
|
||||
- 就目前的零件而言,大多数功能需求仍然要开发者手动编辑零件代码来实现,将功能需求抽象成特定属性,并将此类属性以可视化形式暴露在编辑器中,
|
||||
开发者可以使用这些零件来实现玩法,可视化的形式提高了开发效率,此类零件也可复用。属性零件的灵感来源于此。
|
||||
|
||||
- 同时,在预设编辑器内提供了一个属性模板零件作为编码参考,接下来的内容会围绕此零件的实现过程阐述。
|
||||
|
||||
- 强调一点,所有的功能均在新版编辑器内进行,并且相关作品均为新版作品。
|
||||
|
||||
## 创建一个属性模板零件
|
||||
|
||||
- 属性模板零件的创建入口和其他模板零件一致,在预设编辑器中点击创建零件,弹出的界面中就可以选择该零件,见下图。
|
||||
|
||||

|
||||
|
||||
- 勾选并二次确认后,属性模板零件成功创建,同时下方文件栏也会打开其目录结构,见下图。
|
||||
|
||||

|
||||
|
||||
- 在文件系统打开看的更清晰,下图中红圈标注的文件是会涉及修改的内容,AttrTemplate.part为零件主体,包含零件文件路径等;
|
||||
AttrTemplatePart.py为零件数据属性逻辑主体;AttrTemplatePartMeta.py是零件界面显示逻辑主体,后面会详细介绍。
|
||||
|
||||

|
||||
|
||||
- 零件功能逻辑的编写可以直接在创建出来的属性模板零件文件内容的基础上进行修改。
|
||||
|
||||
## 属性设置框架
|
||||
|
||||
- 属性模板零件的实现依赖底层的属性设置框架,框架旨在减少重复逻辑的编写,包含属性管理、属性与默认值不等才修改属性等特性,将属性设置功能
|
||||
简化为属性配置形式。
|
||||
|
||||
- 零件包含数据逻辑和界面逻辑两部分,接下来按照顺序介绍对应内容。
|
||||
|
||||
- 零件开发基础教程在官网有详细内容 [零件开发 | 我的世界开发者官网 (163.com)](./0-零件开发.md)
|
||||
|
||||
## 数据逻辑
|
||||
|
||||
- 数据逻辑包含零件数据,也是逻辑操作的主体,前面文件结构中AttrTemplatePart.py为数据逻辑主体。
|
||||
|
||||
- 为减少零件类中代码量,框架增加了属性类的概念,必须继承属性基类BasicPartSdkAttr,属性类中编写属性相关配置,零件类中仅仅调用属性类方法即可。
|
||||
数据逻辑的py文件中包含零件属性类PartAttr和零件类AttrTemplatePart,见下图。
|
||||
|
||||

|
||||
|
||||
### 属性组
|
||||
|
||||
- 框架定义了属性和属性组的概念,同一类属性归属一个属性组,既是数据上的集合单元,在界面表现上也属于同一属性分栏;
|
||||
属性实现依赖底层SDK属性,在零件类中需封装对应接口,举例属性为“是否开色彩校正效果”,在零件类中封装了接口SetEnableColorAdjustment,见下图,在属性类中以此接口名作为某一属性,名称需完全一致。
|
||||
|
||||

|
||||
|
||||
- 上面属性概念捋清晰之后,开始介绍属性组的概念,由上述逻辑知,属性组包含了同一类型的属性,而且每个属性在零件类中均可找到同名
|
||||
的接口;属性配置逻辑在属性类当中,模板定义了一个colorAdjustAttrs属性组,其包含两个属性,一是上述的SetEnableColorAdjustment属性、
|
||||
另一是“调整屏幕色彩的色调”属性SetColorAdjustmentTint,见下图。
|
||||
|
||||
- 图中可以看到属性组在属性中的存储形式是类变量,数据结构为字典(键为属性名、值为参数默认值),SetEnableColorAdjustment属性参数简单;SetColorAdjustmentTint属性参数为多参数,用一个字典存储其参数;目前受限于底层逻辑,属性组只能写为一行代码结构,后续会思考优化对策。
|
||||
|
||||

|
||||
|
||||
### 额外的配置信息
|
||||
|
||||
- 目前,有三个问题还没解决,同一属性组里属性有前置依赖关系、框架如何区分不同属性接口参数类型、零件代码同时跑在客户端和服务端如何区分。
|
||||
|
||||
- 框架原本设计中包含配置数据,用于将属性类中属性映射到对应的零件类当中,见下图,干脆将配置数据增加前置属性依赖顺序,配置数据格式为字典,
|
||||
元素为 '分组类变量名': ['依赖的前置属性']
|
||||
|
||||

|
||||
|
||||
- 判定接口参数类型、区分客户端服务端这两个问题,均采用配置数据处理,也许会造成配置数据冗余,但减少了运行时判断的逻辑消耗时间,当零件数目大的情形相对有效
|
||||
|
||||
- 判定接口参数类型问题,定义了多参数列表multiParamAttrs和复合参数列表multiPlexAttrs,见下图第一个标注框,分别存储相应参数属性,单参数属性无需处理,复合参数指可多次施加的接口,模板中有“增加禁用物品”属性AddBannedItem,可创建模板参考。
|
||||
|
||||
- 区分客户端服务端问题,定义了 服务端属性列表serverSdkAttrs、客户端属性列表clientSdkAttrs、双端列表serverAndClientSdkAttrs,属性基类采用服务端属性列表serverSdkAttrs作为判断依据,将服务端属性填入其中即可,元素格式为'属性名称' ;serverAndClientSdkAttrs是双端属性;clientSdkAttrs列表一般不需要填写,但也可重写CheckAttrServerClient方法来使用客户端属性列表。
|
||||
|
||||

|
||||
|
||||
### 属性接口调用
|
||||
|
||||
- 属性类实例化,在零件类中调用属性类相关方法,属性设置才会生效,见下图第一个标注框。
|
||||
|
||||
- 属性的添加通过在零件类的__init__方法中调用SdkAttrsInit方法,参数为零件类本身,见下图第二个标注框。
|
||||
|
||||
- 属性的设置,在零件类的InitClient方法和InitServer方法中调用即可,见下图第三个标注框。实际情况中,如零件属性仅包含客户端属性,只需在InitClient方法调用属性设置方法,同理,零件属性只包含服务端属性,只在InitServer方法中调用。
|
||||
|
||||

|
||||
|
||||
- 零件设置的属性效果,部分跟其挂接的预设相关,而像后处理零件,其设置的属性是画面显示,当零件卸载或销毁时需要将其效果复原,框架提供
|
||||
重置某一属性方法ResetSingleAttr和重置所有属性方法ResetAllAttr,开发者可以有选择的使用,如下图两次ResetSingleAttr调用和一次ResetAllAttr调用,
|
||||
实际效果一致,都消除了零件对后处理效果的设置。ResetSingleAttr方法调用顺序也需要遵循前置属性的规则,属性组前置属性在最后才被销毁
|
||||
|
||||

|
||||
|
||||
## 界面Meta逻辑
|
||||
|
||||
- 界面逻辑通过编写Meta类的形式实现,Meta类中将数据逻辑中的数据部分暴露在界面上,详细的Meta类编写逻辑在
|
||||
官网有相关详细教程[自定义属性面板 | 我的世界开发者官网 (163.com)](./1-自定义属性面板.md)
|
||||
|
||||
- 文件结构中的AttrTemplatePartMeta.py文件是属性模板零件的Meta相关逻辑。
|
||||
|
||||
- 与数据逻辑相关,界面Meta类同时包含属性Meta类和零件Meta类,属性Meta类中包含数据配置,零件Meta类中调用相关接口,见下图示例,属性Meta类需继承属性Meta基类BasicPartSdkAttrMeta。
|
||||
|
||||
- 与数据类设置思路相同,Meta属性类也包含属性配置内容,需实例化后在零件Meta类中使用,见下图。
|
||||
|
||||

|
||||
|
||||
- 属性配置方面,结构定义和数据逻辑类似,colorAdjustAttrs属性组存储属性的界面控件定义,所有属性名与零件类中的属性一一对应,详见下图第一个标注框。
|
||||
|
||||
- 类变量attrgroupMetaCfg存储需要添加到零件Meta中的属性组配置,元素格式为键值对 “ '分组类变量名': ['分组名称', 是否默认展开, 'URL地址'] ”,其中'URL地址'为非必填项,参考见下图中第二个标注框。
|
||||
|
||||

|
||||
|
||||
### 特殊的URL地址
|
||||
|
||||
- 对于上述的URL地址,指同一属性组内都会设置该URL地址,若有特殊需求,如某一属性组有URL地址,但其中一属性
|
||||
URL为空或独有URL,参照见下图,hrefTip为'Empty'则取消单一属性网址链接,hrefTip为'特定网址',
|
||||
则单一属性链接为特定链接,见下图SetEnableColorAdjustment属性。注意,tip为必须内容,其为提示文本内容。
|
||||
|
||||

|
||||
|
||||
- 属性Meta类实例在零件Meta类中使用相对简单,如下图,在零件Meta类中调用GetSdkAttrMeta方法,将返回值添加到PROPERTIES属性中去。
|
||||
|
||||

|
||||
|
||||
### 零件配置
|
||||
|
||||
- 在前面创建出的属性模板零件目前结构中,有一个名为AttrTemplate.part的文件,其包含零件的基本参数配置,同时也是零件本体,见下图
|
||||
- 需要手动修改的内容包含文件夹名称directoryPath、零件类名称AttrTemplatePart、零件Meta类名称metaType,一般情况下文件夹名称无需修改,但零件类名称、零件Meta类名称开发者可能会修改,需要重新配置。
|
||||
|
||||

|
||||
|
||||
|
||||
## 零件功能验收
|
||||
|
||||
### 属性设置
|
||||
|
||||
- 上述为零件功能的编码逻辑,零件功能的验收仍然需要在编辑器内进行,验收演示就直接使用属性模板零件,下图中文件结构,零件本体AttrTemplate.part是编辑器内操作主要单位。
|
||||
|
||||

|
||||
|
||||
- 在编辑器内点击零件本体,右侧界面会出现给该零件添加的属性,也是按属性组分类,见下图
|
||||
|
||||
- 修改零件本体属性,零件代码会同步修改,属性默认值也会修改,导致后续属性设置不生效
|
||||
- 如果右侧属性栏有部分属性未显示,大概率是零件类属性和零件Meta类中属性名称或数据类型不匹配,去检查代码
|
||||
|
||||

|
||||
|
||||
- 零件挂接到预设上,其中逻辑才会执行,同时,为避免修改零件本体导致代码里默认值被修改,将零件挂接到预设上调试效果,演示里创建了空预设并勾选零件本体直接挂接到空预设上,见下图
|
||||
|
||||

|
||||
|
||||
- 挂接成功后,点击空预设下的零件,右侧属性栏已经有对应属性,将开启色彩校正属性勾选,并将色调值改为绿色,点击右上角保存
|
||||
|
||||

|
||||
|
||||
### 运行验收
|
||||
|
||||
- 效果的验收需要切到关卡编辑器进行,将挂接属性模板零件的空预设Empty1拖拽到场景中,见下图
|
||||
- 注意!附加包作品不保存地图,对应预设需要在预设编辑器界面勾选预加载选项,勾选之后不用走上面的拖拽步骤;对于地图作品则不需要,演示内容为地图作品
|
||||
|
||||

|
||||
|
||||
- 零件效果的调试,点击右上角的运行,同时勾选2.1版本,即开始运行
|
||||
|
||||

|
||||
|
||||
- 点击运行,在游戏界面,后处理属性成功生效 ,参见下图
|
||||
|
||||

|
||||
124
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/3-通讯与网络同步.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 网络同步
|
||||
|
||||
|
||||
|
||||
接下来,我们为您来介绍零件的网络同步的功能以及如果在零件中进行使用
|
||||
|
||||
|
||||
|
||||
## 功能简介
|
||||
|
||||
网络同步功能用于简化零件开发中服务端和客户端数据同步,使用网络同步功能后,当服务端零件中指定的字段进行修改(包括字典和列表中子元素的增加、删除和修改)后,客户端零件中的相关字段也会同步的进行修改。
|
||||
|
||||
|
||||
|
||||
## 支持的数据结构
|
||||
|
||||
网络同步功能支持的数据结构为 Number(数字), String(字符串), List(列表), Tuple(元组), Dictionary(字典)。以及上述数据之间的组合嵌套。
|
||||
|
||||
|
||||
|
||||
## 使用方法
|
||||
|
||||
我们以模板零件NetworkReplicatedPart为例,来介绍如何使用网络同步功能。
|
||||
|
||||
使用网络同步功能只需要您在初始化时把需要使用进行同步的字段名字加入零件的replicated字段中,如下列代码所示
|
||||
|
||||
```python
|
||||
def __init__(self):
|
||||
super(NetworkReplicatedPart, self).__init__()
|
||||
self.name = "网络复制测试零件"
|
||||
self.intProperty = 0
|
||||
self.intProperty2 = 0
|
||||
self.strProperty = "0"
|
||||
self.strProperty2 = "0"
|
||||
self.listProperty = ["0", "0", "0"]
|
||||
self.listProperty2 = ["0", "0", "0"]
|
||||
self.dictProperty = {"key": 0}
|
||||
self.dictProperty2 = {"key": 0}
|
||||
|
||||
self.tupleProperty = ("0", "0", "0")
|
||||
self.tupleProperty2 = ("0", "0", "0")
|
||||
|
||||
self.flexProperty = ("0", "0", {"key": [0]})
|
||||
self.flexProperty2 = ("0", "0", {"key": [0]})
|
||||
|
||||
# 同步intProperty, strProperty, listProperty, dictProperty, tupleProperty, flexProperty
|
||||
self.replicated = ["intProperty", "strProperty", "listProperty", "dictProperty", "tupleProperty", "flexProperty"]
|
||||
self.tickIndex = 0
|
||||
```
|
||||
|
||||
在上图的代码中,我们为intProperty、strProperty、listProperty、dictProperty、tupleProperty、flexProperty这六种不同类型的字段使用了网络同步功能。
|
||||
|
||||
|
||||
|
||||
接下来,我们在服务端的tick中修改相关字段的值,并在客户端的tick中打印出来
|
||||
|
||||
```python
|
||||
def TickClient(self):
|
||||
self.tickIndex += 1
|
||||
if self.tickIndex % 150 == 30:
|
||||
print("TickClient", self)
|
||||
|
||||
def TickServer(self):
|
||||
self.tickIndex += 1
|
||||
if self.tickIndex % 150 == 0:
|
||||
self.intProperty += 1
|
||||
self.intProperty2 += 1
|
||||
self.strProperty = str(self.intProperty)
|
||||
self.strProperty2 = str(self.intProperty)
|
||||
self.listProperty[0] = self.strProperty
|
||||
self.listProperty2[0] = self.strProperty
|
||||
self.dictProperty["key"] = self.intProperty
|
||||
self.dictProperty2["key"] = self.intProperty
|
||||
self.tupleProperty = tuple(self.listProperty)
|
||||
self.tupleProperty2 = tuple(self.listProperty)
|
||||
self.flexProperty[2]["key"].append(self.intProperty)
|
||||
self.flexProperty2[2]["key"].append(self.intProperty)
|
||||
print("TickServer", self)
|
||||
```
|
||||
|
||||
|
||||
|
||||
使用开发测试进行测试后,脚本调试界面日志如下(为了方便查看,对无用日志进行了省略,调整了日志缩进)
|
||||
|
||||
`[17:11:17] ('TickClient', {`
|
||||
`......`
|
||||
`"dictProperty": {"key": 2},`
|
||||
`"dictProperty2": {"key": 0},`
|
||||
`"flexProperty": ["0","0", {"key": [0, 1, 2]}],`
|
||||
`"flexProperty2": ["0", "0", {"key": [0]}],`
|
||||
`"intProperty": 2,`
|
||||
`"intProperty2": 0,`
|
||||
`"listProperty": [ "2", "0", "0"],`
|
||||
`"listProperty2": ["0", "0", "0" ],`
|
||||
`"name": "NetworkReplicated",`
|
||||
`"partType": "NetworkReplicatedPart",`
|
||||
`"replicated": [ "intProperty", "strProperty", "listProperty", "dictProperty", "tupleProperty", "flexProperty"],`
|
||||
`"strProperty": "2",`
|
||||
`"strProperty2": "0",`
|
||||
`"tickEnable": true,`
|
||||
`"tickIndex": 300,`
|
||||
`......`
|
||||
`"tupleProperty": [ "2", "0", "0" ],`
|
||||
`"tupleProperty2": [ "0", "0", "0"`
|
||||
`]`
|
||||
|
||||
`})`
|
||||
|
||||
*注:tuple类型在客户端依然是tuple类型,打印为tuple,原因为打印值为零件结构的json字符串
|
||||
|
||||
可见,客户端零件中intProperty、strProperty、listProperty、dictProperty、tupleProperty、flexProperty这六个字段的值被服务端零件同步到了客户端中,未写入replicated的字段没有进行同步。
|
||||
|
||||
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 1、需要同步的字段需要在replicated字段之前初始化
|
||||
|
||||
- 2、字段必须为支持的数据结构,否则会无法同步而且报错
|
||||
|
||||
- 3、使用网络同步功能会产生额外的性能损耗,请根据实际需要进行使用
|
||||
|
||||
- 4、客户端同步的数据会有延迟,一般为1帧 + 网络传输的时间。
|
||||
|
||||
350
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/4-零件通讯教程.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# 零件通信
|
||||
|
||||
这个教程将为您来介绍零件的服务端与客户端通信的功能以及如何在编辑器中新建并使用
|
||||
在此之前, 先推荐先按流程看完之前的样例[教程][预设与零件]并且按步骤实际操作过,也需要了解服务端与客户端的区别,也就是([Client-Server架构][服务端与客户端])
|
||||
|
||||
## 前置指引
|
||||
|
||||
首先,让我们来先打开一个新版的编辑器,新建一个空白模板,地图或者addon都可以
|
||||
1. 点击新建
|
||||
2. 选择空白地图模板
|
||||
3. 点击升级作品
|
||||
4. 确认弹出对话框中的内容后点击升级
|
||||
|
||||

|
||||
|
||||
这样就会进入新版的编辑器界面
|
||||
1. 点击预设编辑器(方便后续实际使用)
|
||||
2. 点击资源管理器中零件分类的新建按钮新建一个空零件
|
||||
3. 样例就命名为A(实际不推荐无意义单词命名)
|
||||
|
||||

|
||||
|
||||
然后就成功创建了一个零件
|
||||
1. 在这里我们将零件的运行名称同步改成A,方便后续的结果展示
|
||||
2. 打开APart.py这个脚本本件就可以开始编写脚本测试了
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 样例脚本编写
|
||||
零件的通讯有多种不同的接口,它们都有对应的使用方式
|
||||
|
||||
### 单零件的服务端与客户端的通讯
|
||||
|
||||
+ 逻辑说明:每隔20秒(20 * 30 = 600帧),客户端通知服务端自己需要调整坐标,服务器收到通知后调整客户端玩家坐标向上50格,并在该玩家游戏界面显示"起飞"
|
||||
|
||||
+ api说明:
|
||||
> [BroadcastEvent][BroadcastEventUrl] 服务端/客户端 广播事件到 服务端/客户端,不支持跨网络
|
||||
>
|
||||
> [ListenSelfEvent][ListenSelfEventUrl] 监听来自本零件的事件
|
||||
>
|
||||
> [NotifyToServer][NotifyToServerUrl] 发送事件给服务端,支持跨网络通讯
|
||||
>
|
||||
> [NotifyToClient][NotifyToClientUrl] 发送事件给指定客户端,支持跨网络通讯
|
||||
>
|
||||
> [BroadcastToAllClient][BroadcastToAllClientUrl] 通知指所有客户端触发事件,支持跨网络通讯
|
||||
|
||||
+ 代码样例: [在此][Code_Url]
|
||||
```python
|
||||
class APart(PartBase):
|
||||
def __init__(self):
|
||||
super(APart, self).__init__()
|
||||
# 零件名称(之前步骤中将名称改为了A)
|
||||
self.name = "A"
|
||||
# 帧数变量用于之后逻辑判断
|
||||
self.tickIndex = 0
|
||||
|
||||
def printSelfMsg(self, msg):
|
||||
# 区分时客户端零件打印还是服务端的零件打印
|
||||
print("client:" if self.isClient else "server:", msg)
|
||||
|
||||
# 逻辑处理函数
|
||||
def OnHandleClientEvent(self, msg):
|
||||
if "msg" in msg:
|
||||
# 打印接收到的消息
|
||||
self.printSelfMsg(msg["msg"])
|
||||
if "id" in msg:
|
||||
# 处理逻辑
|
||||
player = msg["id"]
|
||||
# 获取发送信息玩家的当前位置
|
||||
pos = list(self.GetEntityFootPos(player))
|
||||
pos[1] += 50
|
||||
# 设置该玩家的位置
|
||||
self.SetEntityFootPos(player, tuple(pos))
|
||||
# 在发送ClientEvent事件的玩家客户端上显示红色的文字
|
||||
self.NotifyOneMessage(player, "起飞!", "§c")
|
||||
# 接口使用效果展示,在客户端发送LogicEvent事件给指定的客户端
|
||||
self.NotifyToClient(player, "LogicEvent", "from part {}_OnHandleClientEvent_NotifyToClient_{}".format(self.name, self.tickIndex))
|
||||
|
||||
def InitClient(self):
|
||||
# 监听自身内部的MsgEvent与LogicEvent事件
|
||||
self.ListenSelfEvent("MsgEvent", self, self.printSelfMsg)
|
||||
self.ListenSelfEvent("LogicEvent", self, self.printSelfMsg)
|
||||
|
||||
def InitServer(self):
|
||||
# 监听自身内部的MsgEvent与LogicEvent事件
|
||||
self.ListenSelfEvent("MsgEvent", self, self.printSelfMsg)
|
||||
self.ListenSelfEvent("LogicEvent", self, self.OnHandleClientEvent)
|
||||
|
||||
def TickClient(self):
|
||||
self.tickIndex += 1
|
||||
if self.tickIndex % 600 == 0:
|
||||
# 广播MsgEvent事件,用于验证其余另外两个Notify接口不一样的地方
|
||||
self.BroadcastEvent("MsgEvent", "tickClient_BroadcastEvent_Msg")
|
||||
# 在客户端发送LogicEvent事件给服务端,并且发送本地玩家的id与对应消息
|
||||
self.NotifyToServer("LogicEvent", {
|
||||
"id": self.GetLocalPlayerId(),
|
||||
"msg": "from player {} part {}_TickClient_NotifyToServer_{}".format(self.GetLocalPlayerId(), self.name, self.tickIndex)
|
||||
})
|
||||
|
||||
def TickServer(self):
|
||||
self.tickIndex += 1
|
||||
if self.tickIndex % 600 == 60:
|
||||
# 取消到第60帧那一次,保持和客户端的频率同步
|
||||
if self.tickIndex > 60:
|
||||
# 广播MsgEvent事件,用于验证其余另外两个接口不一样的地方
|
||||
self.BroadcastEvent("MsgEvent", "TickServer_BroadcastEvent_Msg")
|
||||
# BroadcastToAllClient接口验证测试
|
||||
self.BroadcastToAllClient("MsgEvent", "from player {} part {}_TickServer_BroadcastToAllClient_Msg".format(self.GetLocalPlayerId(), self.name))
|
||||
|
||||
def DestroyClient(self):
|
||||
# 反监听事件,确保安全退出
|
||||
self.UnListenSelfEvent("LogicEvent", self, self.printSelfMsg)
|
||||
self.UnListenSelfEvent("MsgEvent", self, self.printSelfMsg)
|
||||
|
||||
def DestroyServer(self):
|
||||
# 反监听事件,确保安全退出
|
||||
self.UnListenSelfEvent("LogicEvent", self, self.OnHandleClientEvent)
|
||||
self.UnListenSelfEvent("MsgEvent", self, self.printSelfMsg)
|
||||
```
|
||||
|
||||
+ 使用过程
|
||||
1. 新建一个空预设,命名随意
|
||||

|
||||
2. 将零件A挂接在预设下(左键点击A.part,按住拖动到左侧层级界面的预设下方即可)
|
||||
3. 预设右侧的预加载属性勾选上
|
||||

|
||||
4. 切换至关卡编辑器界面,可以看到左侧舞台界面上有预设显示
|
||||

|
||||
|
||||
|
||||
+ 运行结果
|
||||
1. 本地单机运行
|
||||

|
||||
1. 现象:
|
||||
1. 第一行和第四行日志可以看出,零件的客户端发出的BroadCastEvent事件被客户端接收到,服务端发出的BroadCastEvent事件被服务端接收到(即便他们接收的事件名称一样)。
|
||||
2. 第一行和第四行日志则不同,Notify系列的接口,则是在服务端发送的事件被客户端接收到了,客户端发送的被服务端接收到了
|
||||
2. 结论:
|
||||
1. BroadCastEvent 接口广播的事件是在本客户端/服务端(线程)内部的
|
||||
2. Notify系列与BroadcastToAllClient接口通知的事件是可以跨客户端/服务端(线程)的
|
||||
|
||||
2. 多人游戏运行
|
||||

|
||||

|
||||
1. 现象:(空白Addon为服务器玩家,Mod Pc开发包为加入的玩家)[多人联机如何测试?][MutiPlayerUrl]
|
||||
1. 服务器玩家比单机游玩时多接收到了一条来自加入玩家的零件客户端的消息(使用的是Notify系列的接口)
|
||||
2. 客户端玩家的日志只有零件客户端的消息成功打印出来,零件服务端没有反应
|
||||
2. 结论:
|
||||
1. 零件本体在运行时有服务端和客户端两种实例,单机运行时,两种实例程序都会启动并运行,但是多人游戏时,所有的游戏客户端都会运行零件客户端的代码逻辑,而仅有服务器玩家(多人游戏房主)会运行零件的服务端逻辑
|
||||
2. 成功验证了BroadCastEvent接口是不能够跨网络的,而Notify系列的接口则是可以跨网络通讯的
|
||||
|
||||
---
|
||||
|
||||
### 多零件之间的通讯
|
||||
|
||||
+ 逻辑说明: 重新新建B,C,D三个零件修改对应的零件名称,展示同预设下的零件之间的通讯,与跨预设的零件之间的通讯.
|
||||
|
||||
+ api说明:
|
||||
> [ListenPartEvent][ListenPartEventUrl] 监听来自本零件的事件
|
||||
>
|
||||
> [ListenPresetSystemEvent][ListenPresetSystemEventUrl] 监听来自预设系统的事件
|
||||
>
|
||||
> [BroadcastPresetSystemEvent][BroadcastPresetSystemEventUrl] 广播事件给预设系统
|
||||
>
|
||||
>> 获取预设下指定(类型)/(名称)的(零件列表)/(第一个零件)
|
||||
>> [GetPartsByType][GetPartsByTypeUrl], [GetPartByType][GetPartByTypeUrl], [GetPartsByName][GetPartsByNameUrl], [GetPartByName][GetPartByNameUrl].
|
||||
>
|
||||
>> 预设系统下根据零件类型与零件名称获取(零件列表)/(第一个零件)
|
||||
>> [GetGameObjectsByTypeName][GetGameObjectsByTypeNameUrl], [GetGameObjectByTypeName][GetGameObjectByTypeNameUrl]
|
||||
|
||||
+ 代码样例: 可导入资源包路径[在此][Code_Url]
|
||||
|
||||
B零件代码,放置在与D不同的预设下,发送事件给D零件
|
||||
```python
|
||||
def __init__(self):
|
||||
super(BPart, self).__init__()
|
||||
# 零件名称
|
||||
self.name = "B"
|
||||
# 帧数变量用于之后逻辑判断
|
||||
self.tickIndex = 0
|
||||
|
||||
def TickClient(self):
|
||||
self.tickIndex += 1
|
||||
if self.tickIndex == 660:
|
||||
self.NotifyToServer("PartEventB", "from player {} part {}_TickClient_NotifyToServer_{}".format(self.GetLocalPlayerId(), self.name, self.tickIndex))
|
||||
# 用作对比
|
||||
self.BroadcastEvent("PartEventB", "from player {} part {}_TickClient_BroadcastEvent_{}".format(self.GetLocalPlayerId(), self.name, self.tickIndex))
|
||||
self.BroadcastPresetSystemEvent("PresetSystemEvent", "from player {} part {}_TickClient_BroadcastPresetSystemEvent_{}".format(self.GetLocalPlayerId(), self.name, self.tickIndex))
|
||||
|
||||
def TickServer(self):
|
||||
self.tickIndex += 1
|
||||
if self.tickIndex % 600 == 60:
|
||||
self.BroadcastEvent("PartEventB", "from player {} part {}_TickServer_BroadcastEvent_{}".format(self.GetLocalPlayerId(), self.name, self.tickIndex))
|
||||
self.BroadcastPresetSystemEvent("PresetSystemEvent", "from player {} part {}_TickServer_BroadcastPresetSystemEvent_{}".format(self.GetLocalPlayerId(), self.name, self.tickIndex))
|
||||
|
||||
```
|
||||
|
||||
C零件代码,放置在与D相同的预设下,发送事件给D零件
|
||||
```python
|
||||
def __init__(self):
|
||||
super(CPart, self).__init__()
|
||||
# 零件名称
|
||||
self.name = "C"
|
||||
# 帧数变量用于之后逻辑判断
|
||||
self.tickIndex = 0
|
||||
|
||||
def TickServer(self):
|
||||
self.tickIndex += 1
|
||||
if self.tickIndex % 600 == 60:
|
||||
self.BroadcastEvent("PartEventC", "from player {} part {}_TickServer_BroadcastEvent_{}".format(self.GetLocalPlayerId(), self.name, self.tickIndex))
|
||||
self.BroadcastPresetSystemEvent("PresetSystemEvent", "from player {} part {}_TickServer_BroadcastPresetSystemEvent_{}".format(self.GetLocalPlayerId(), self.name, self.tickIndex))
|
||||
|
||||
```
|
||||
|
||||
D零件代码,接收BC发送的事件
|
||||
```python
|
||||
def __init__(self):
|
||||
super(DPart, self).__init__()
|
||||
# 零件名称
|
||||
self.name = "D"
|
||||
# 帧数变量用于之后逻辑判断
|
||||
self.tickIndex = 0
|
||||
|
||||
def printSelfMsg(self, msg):
|
||||
# 区分时客户端零件打印还是服务端的零件打印
|
||||
print("client:" if self.isClient else "server:", msg)
|
||||
|
||||
def InitServer(self):
|
||||
import Preset.Controller.PresetApi as presetApi
|
||||
# 在预设系统下获取指定类型(BPart)与指定名称(B)的零件(不用在同一个预设下)
|
||||
partB = presetApi.GetGameObjectByTypeName("BPart", "B")
|
||||
|
||||
if partB:
|
||||
# 监听来自于的partB事件
|
||||
print ("partD listen PartEventB")
|
||||
self.ListenPartEvent(partB.id, "PartEventB", self, self.printSelfMsg)
|
||||
|
||||
# 获取父预设的一个名称为(C)的子零件
|
||||
partC = self.GetParent().GetPartByName("C")
|
||||
|
||||
if partC:
|
||||
# 监听来自于的partC事件
|
||||
print ("partD listen PartEventC")
|
||||
self.ListenPartEvent(partC.id, "PartEventC", self, self.printSelfMsg)
|
||||
|
||||
# 监听预设系统的事件
|
||||
self.ListenPresetSystemEvent("PresetSystemEvent", self, self.printSelfMsg)
|
||||
|
||||
def TickServer(self):
|
||||
self.tickIndex += 1
|
||||
if self.tickIndex % 600 == 60:
|
||||
# 用作对比
|
||||
self.BroadcastEvent("PartEventB", "from player {} part {}_TickServer_{}".format(self.GetLocalPlayerId(), self.name, self.tickIndex))
|
||||
self.BroadcastEvent("PartEventC", "from player {} part {}_TickServer_{}".format(self.GetLocalPlayerId(), self.name, self.tickIndex))
|
||||
self.BroadcastEvent("PresetSystemEvent", "from player {} part {}_TickServer_{}".format(self.GetLocalPlayerId(), self.name, self.tickIndex))
|
||||
|
||||
def DestroyServer(self):
|
||||
import Preset.Controller.PresetApi as presetApi
|
||||
partB = presetApi.GetGameObjectByTypeName("BPart", "B")
|
||||
if partB:
|
||||
self.UnListenPartEvent(partB.id, "PartEventB", self, self.printSelfMsg)
|
||||
partC = self.GetParent().GetPartByName("C")
|
||||
if partC:
|
||||
self.UnListenPartEvent(partC.id, "PartEventC", self, self.printSelfMsg)
|
||||
self.UnListenPresetSystemEvent("PresetSystemEvent", self, self.printSelfMsg)
|
||||
```
|
||||
|
||||
+ 使用过程 (预设命名需要一致,原因在之后的注意事项)
|
||||
1. 新建一个空预设,命名为B,将B零件放置在B预设下,并勾选预加载
|
||||
2. 新建一个空预设,命名为CD,将C零件和D零件放置在CD预设下,并勾选预加载
|
||||
3. 切换至关卡编辑器界面,可以看到左侧舞台界面上有预设显示
|
||||

|
||||
|
||||
|
||||
|
||||
+ 运行结果日志
|
||||
因为需要日志前后跨度太大,就复制出后,只展示关键信息,加上序号
|
||||

|
||||
```
|
||||
。。。
|
||||
1. partD listen PartEventB
|
||||
2. partD listen PartEventC
|
||||
。。。
|
||||
3. ('server:', 'from player -1 part B_TickServer_BroadcastEvent_60')
|
||||
4. ('server:', 'from player -1 part B_TickServer_BroadcastPresetSystemEvent_60')
|
||||
5. ('server:', 'from player -1 part C_TickServer_BroadcastEvent_60')
|
||||
6. ('server:', 'from player -1 part C_TickServer_BroadcastPresetSystemEvent_60')
|
||||
。。。
|
||||
7. ('server:', 'from player -1 part B_TickServer_BroadcastEvent_660')
|
||||
8. ('server:', 'from player -1 part B_TickServer_BroadcastPresetSystemEvent_660')
|
||||
9. ('server:', 'from player -1 part C_TickServer_BroadcastEvent_660')
|
||||
10.('server:', 'from player -1 part C_TickServer_BroadcastPresetSystemEvent_660')
|
||||
11.('server:', 'from player -4294967295 part B_TickClient_NotifyToServer_660')
|
||||
```
|
||||
分析结论:
|
||||
1. D零件一直没能收到自己发送的事件,这就是ListenPartEvent中指定id的作用,以及BroadcastEvent与ListenPresetSystemEvent并不能配套使用
|
||||
2. 这里的所有日志除了11行都是TickServer发送的,说明BroadcastPresetSystemEvent与BroadcastEvent一样,都不能跨越服务端和客户端(线程)发送消息
|
||||
3. 3,5,6,9行表明的消息BroadcastEvent与ListenPartEvent配合可以做到,跨零件或者跨预设通信,但是不能跨越服务端和客户端(线程)发送消息
|
||||
4. 11行同时也说明,Notify系列的接口不仅仅可以跨越服务端和客户端(线程)发送消息,同时也可以配合ListenPartEvent跨越零件和预设发送消息
|
||||
5. 4,6,8,10行收到的消息BroadcastPresetSystemEvent与ListenPresetSystemEvent配合可以做到,跨零件或者跨预设通信
|
||||
|
||||
|
||||
|
||||
## 通讯接口使用总结
|
||||
|
||||
1. 本零件内部,服务端或者客户端的内部(单线程)通讯,使用ListenSelfEvent与BroadcastEvent配合
|
||||
2. 本零件内部,服务端与客户端的通讯(跨线程),都可以使用NotifyToServer,NotifyToClient,BroadcastToAllClient 与 ListenSelfEvent配合,同时只这一套接口也可以跨网络通讯。
|
||||
3. 零件与零件通讯(无论在不在同一个预设下),不跨网络,都可以使用ListenPartEvent与BroadcastEvent配合,或者BroadcastPresetSystemEvent与ListenPresetSystemEvent配合
|
||||
4. 零件与零件通讯(无论在不在同一个预设下),跨网络,使用NotifyToServer,NotifyToClient,BroadcastToAllClient与ListenPartEvent做到客户端与服务端的通讯
|
||||
|
||||
|
||||
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. ***注意addon模板只会加载预加载的预设,地图模板则会在加载预加载预设的同时,加载所有在玩家所在区块的预设【关卡编辑器中预设可以拖动至地图的某个坐标上】,详情见[预加载][loadUrl]***
|
||||
2. ***顺便补充一个设定,关卡编辑器中拖动到地图上的预设,会在玩家靠近时加载,远离时卸载,若勾选了常加载,则在靠近加载后远离也不会卸载,除非重启游戏***
|
||||
3. ***预设的加载是有顺序的,勾选了预加载的预设,是按照预加载的字母序(关卡编辑器的前后顺序)加载预设的;没有勾选则按照舞台上预设的顺序加载***
|
||||
4. ***加载过了的预设,其下的所有零件都已初始化,可以通过GetPartsByType或者GetGameObjectByTypeName获取到兄弟零件id,但是无法拿到别的未被加载的预设下的零件id***
|
||||
|
||||
## 补充
|
||||
|
||||
代码下载后是一个压缩包,解压后,将ABCD4个文件夹拖动到资源管理器Parts分类下右侧的展示界面即可
|
||||

|
||||
|
||||
[BroadcastEventUrl]: /mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.md##broadcastevent
|
||||
[ListenSelfEventUrl]: /mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.md##listenselfevent
|
||||
[NotifyToServerUrl]: /mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.md##notifytoserver
|
||||
[NotifyToClientUrl]: /mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.md##notifytoclient
|
||||
[BroadcastToAllClientUrl]: /mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.md##broadcasttoallclient
|
||||
[MutiPlayerUrl]: /mcguide/12-入门教程/20-MC%20Studio使用说明.md#基岩版组件
|
||||
[ListenPartEventUrl]: /mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.md##listenpartevent
|
||||
[ListenPresetSystemEventUrl]: /mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.md##listenpresetsystemevent
|
||||
[BroadcastPresetSystemEventUrl]: /mcdocs/3-PresetAPI/预设对象/零件/零件PartBase.md##broadcastpresetsystemevent
|
||||
[GetPartsByTypeUrl]: /mcdocs/3-PresetAPI/预设对象/预设/预设基类PresetBase.md##getpartsbytype
|
||||
[GetPartByTypeUrl]: /mcdocs/3-PresetAPI/预设对象/预设/预设基类PresetBase.md##getpartbytype
|
||||
[GetPartsByNameUrl]: /mcdocs/3-PresetAPI/预设对象/预设/预设基类PresetBase.md##getpartsbyname
|
||||
[GetPartByNameUrl]: /mcdocs/3-PresetAPI/预设对象/预设/预设基类PresetBase.md##getpartbyname
|
||||
[GetGameObjectsByTypeNameUrl]: /mcdocs/3-PresetAPI/预设管理/PresetApi.md##getgameobjectsbytypename
|
||||
[GetGameObjectByTypeNameUrl]: /mcdocs/3-PresetAPI/预设管理/PresetApi.md##getgameobjectbytypename
|
||||
|
||||
[loadUrl]: /mcguide/20-玩法开发/14-预设玩法编程/11-深入理解预设/2-实例与属性.md#预加载
|
||||
[Code_Url]: https://g79.gdl.netease.com/Parts.7z
|
||||
|
||||
[服务端与客户端]: /mcguide/20-玩法开发/13-模组SDK编程/1-Mod开发简介/3-Mod是如何工作的.md
|
||||
|
||||
[预设与零件]: /mcguide/20-玩法开发/14-预设玩法编程/9-第一个预设Mod/6-使用零件编程.md
|
||||
131
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/5-零件继承.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 零件继承
|
||||
|
||||
用户在设计游戏玩法时,偶尔会有需求是可以在编辑器提供已有的内置零件或者资源包零件上进行一些扩展即可做到的,这时就可以用零件继承来更为方便的完成玩法的设计实现,而不用全部重新再来一次
|
||||
|
||||
## 样例
|
||||
|
||||
建议: 需要理解python的类的继承与方法重写的意义
|
||||
|
||||
需求:判定一个区域为特殊的瘴气区域,玩家进入其中时会受伤,僵尸类生物在其中后移速大幅增加
|
||||
|
||||
实现:编辑器内置已有触发器零件,能够监听实体进入/停留/离开某个区域,只需要在这之上补充部分逻辑即可
|
||||
|
||||
1. 新建一个空地图模板,因为需要指定区域,不要选择addon模板
|
||||
|
||||
1. 需要新建一个空零件(样例的零件名为SpecialArea)
|
||||

|
||||
|
||||
2. 将零件的脚本与零件对应的属性脚本都继承对应的触发器脚本部分
|
||||

|
||||
|
||||
3. 编写逻辑代码, 链接[在此][CodeUrl],接口可在官方的api文档页面中找到[api][APIUrl]
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
from Preset.Model.GameObject import registerGenericClass
|
||||
from Preset.Parts.TriggerPart import TriggerPart
|
||||
from mod.common.minecraftEnum import EntityType
|
||||
from mod.common.minecraftEnum import ActorDamageCause
|
||||
|
||||
|
||||
@registerGenericClass("SpecialAreaPart")
|
||||
class SpecialAreaPart(TriggerPart):
|
||||
def __init__(self):
|
||||
super(SpecialAreaPart, self).__init__()
|
||||
# 零件名称
|
||||
self.name = "空零件"
|
||||
self.support = 2
|
||||
|
||||
def InitServer(self):
|
||||
# 因为继承了别的零件,若需要重写方法,推荐调用一下基类的方法,保证基类的逻辑不会破坏。
|
||||
super(SpecialAreaPart, self).InitServer()
|
||||
# 监听基类发送的3个事件,
|
||||
self.ListenSelfEvent("OnTriggerEntityEnter", self, self.handleEntityEnter)
|
||||
self.ListenSelfEvent("OnTriggerEntityStay", self, self.handleEntityStay)
|
||||
self.ListenSelfEvent("OnTriggerEntityExit", self, self.handleEntityExit)
|
||||
|
||||
def handleEntityEnter(self, data):
|
||||
# 触发器发送事件自带的参数
|
||||
part = data["TriggerPart"]
|
||||
entitySet = data["EnterEntityIds"]
|
||||
for entity in entitySet:
|
||||
# 获取进入实体的实体类型
|
||||
entityType = self.GetEntityEngineType(entity)
|
||||
if EntityType.Player == EntityType.Player & entityType:
|
||||
# 玩家实体则发送通知消息
|
||||
self.NotifyOneMessage(entity, "你进入了瘴气之地", "§c")
|
||||
|
||||
def handleEntityStay(self, data):
|
||||
part = data["TriggerPart"]
|
||||
entitySet = data["StayEntityIds"]
|
||||
for entity in entitySet:
|
||||
entityType = self.GetEntityEngineType(entity)
|
||||
if EntityType.Player == EntityType.Player & entityType:
|
||||
# 给玩家一个缓慢的DeBuff效果
|
||||
self.AddEffectToEntity(entity, "slowness", 30, 2, True)
|
||||
# 每次检测都给区域内的玩家减掉1生命值
|
||||
print("SetHurtByEntityNew to Player", self.SetHurtByEntityNew(entity, 1, ActorDamageCause.Override, None, False))
|
||||
|
||||
if EntityType.ZombieMonster == EntityType.ZombieMonster & entityType:
|
||||
# 给僵尸类型的生物一个等级3的加速效果(可在官网的api页面搜索对应的接口的参数含义)
|
||||
print("AddEffectToEntity to ZombieMonster", self.AddEffectToEntity(entity, "speed", 30, 2, True))
|
||||
|
||||
def handleEntityExit(self, data):
|
||||
part = data["TriggerPart"]
|
||||
entitySet = data["ExitEntityIds"]
|
||||
for entity in entitySet:
|
||||
entityType = self.GetEntityEngineType(entity)
|
||||
if EntityType.Player == EntityType.Player & entityType:
|
||||
self.NotifyOneMessage(entity, "你离开了瘴气之地", "§f")
|
||||
# 移除玩家的减速
|
||||
self.RemoveEffectFromEntity(entity, "slowness")
|
||||
|
||||
if EntityType.ZombieMonster == EntityType.ZombieMonster & entityType:
|
||||
# 移除僵尸的加速
|
||||
self.RemoveEffectFromEntity(entity, "speed")
|
||||
|
||||
def DestroyServer(self):
|
||||
super(SpecialAreaPart, self).DestroyServer()
|
||||
self.UnListenSelfEvent("OnTriggerEntityEnter", self, self.handleEntityEnter)
|
||||
self.UnListenSelfEvent("OnTriggerEntityStay", self, self.handleEntityStay)
|
||||
self.UnListenSelfEvent("OnTriggerEntityExit", self, self.handleEntityExit)
|
||||
```
|
||||
4. 配置属性列表(只用原有的触发器的属性即可满足需求)
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
from Meta.ClassMetaManager import sunshine_class_meta
|
||||
from Preset.Parts.TriggerPart import TriggerPartMeta
|
||||
|
||||
@sunshine_class_meta
|
||||
class SpecialAreaPartMeta(TriggerPartMeta):
|
||||
CLASS_NAME = "SpecialAreaPart"
|
||||
PROPERTIES = {
|
||||
}
|
||||
```
|
||||
5. 修改部分属性参数确保效果正确
|
||||
1. 将零件放置在预设下,并拖动放置在关卡编辑器的地图上
|
||||
2. 设置好起效的区域(设置区域的上方可以点击按钮定位显示选择的区域)
|
||||
3. 勾选监听3种状态(我们在逻辑代码监听了并处理了这3种事件)
|
||||
4. 因为我们只需要服务端的监听,就修改属性只监听服务端
|
||||

|
||||
|
||||
6. 运行测试
|
||||
|
||||
可一看到提示,僵尸怪物都有buff,女巫没有,人物右侧有debuff,可以自行运行感受
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 注意事项
|
||||
1. 继承零件不仅可以继承内置零件,也可以继承自己写的零件,不过由于某些原因,继承自己写的零件时路径需要注意
|
||||

|
||||
2. 继承多个零件与继承单个零件类似,只需要确保脚本代码与属性代码都继承了即可
|
||||
|
||||
3. 下载代码的使用方式与零件通讯中的一致
|
||||
|
||||
|
||||
|
||||
|
||||
[CodeUrl]: https://g79.gdl.netease.com/PartsInheritSample_SpecialArea.7z
|
||||
[APIUrl]: /mcdocs/3-PresetAPI/预设对象/通用/SDK接口封装SdkInterface.md
|
||||
58
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/6-零件开发规范.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 零件开发规范
|
||||
|
||||
## 目录与命名
|
||||
|
||||
当使用编辑器创建零件时,会在行为包目录下的Parts文件夹(若无会自动创建。下简称行为包Parts目录)下创建一个与.part文件同名的文件夹,与零件相关的三个文件都在该文件夹中,见[0-零件开发](0-零件开发.md)。该文件夹一旦确定不建议重命名,否则该零件将会失效。
|
||||
|
||||
由于mod的脚本文件正常运行需要依赖modmain所在的脚本目录,因此零件文件夹会以快捷方式的形式链接进目前编辑mod中的第一个脚本目录下的Parts文件夹中(下简称脚本Parts目录),以保证零件代码的正常运行。当该mod在studio被导出时,该快捷方式将被切断,行为包Parts目录下的零件文件夹将被完整的复制进脚本Parts目录中,同时行为包Parts目录下的零件文件夹仅保留.part文件,脚本Parts目录中下的零件文件夹保留两个python文件。
|
||||
|
||||
## 保留字段
|
||||
|
||||
以下字段为零件基类所使用的成员变量,开发者覆写该保留字段会导致不可知的后果,请避免使用。
|
||||
|
||||
- id
|
||||
|
||||
- classType
|
||||
|
||||
- isClient
|
||||
|
||||
- filterKeys
|
||||
|
||||
- _parent
|
||||
|
||||
- entityId
|
||||
|
||||
- boxId
|
||||
|
||||
- name
|
||||
|
||||
- transform
|
||||
|
||||
- isRemoved
|
||||
|
||||
- loaded
|
||||
|
||||
- needUpdate
|
||||
|
||||
- directoryPath
|
||||
|
||||
- tickEnable
|
||||
|
||||
- data
|
||||
|
||||
- dataKeys
|
||||
|
||||
- eventMap
|
||||
|
||||
- replicated
|
||||
|
||||
## import与开发规范
|
||||
|
||||
- 只支持零件主类的函数内修改,不能在主类的头部import
|
||||
|
||||
- 由于零件类在编辑器内也会执行初始化逻辑,但编辑器和引擎运行环境不同,为了避免报错影响开发者,尽量避免在零件的初始化函数内编写逻辑。
|
||||
|
||||
- 零件代码虽然只有一份,在游戏运行时是分为客户端和服务端两个实例,因此需要开发者时刻注意客户端零件代码和服务端零件代码的分离,避免出现客户端零件代码调用服务点sdk接口的问题。
|
||||
|
||||
- 由于零件需要运行在游戏代码和编辑器代码中,因此零件代码需要兼容python2和python3环境。
|
||||
|
||||
124
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/8-内置零件与模板零件.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 内置零件与模板零件
|
||||
## 什么是零件
|
||||
1. 零件是预设架构下,与代码绑定的一种玩法逻辑对象,它可以直接挂接到预设下,随着预设的加载而被加载实现零件代码里的逻辑。
|
||||
2. 空零件和空预设就是没有任何玩法逻辑的空白模板,开发者可以在此进行代码的编写,并在合适的时机加载预设以达成自己的目标。
|
||||
3. 为了帮助开发者快速上手"预设—零件"的新版开发方式,我们提供内置零件和模板零件两种具有实际玩法功能的零件供开发者使用和参考,希望开发者能使用中了解零件的生效逻辑和编写规范,最后能自己在空零件上自行编写代码,实现想要的功能。
|
||||
## 什么是内置零件
|
||||
内置零件就是在游戏引擎中内置好的一些玩法零件,开发者无法对内置零件的代码逻辑进行修改,也无法新建一个内置零件的副本进行编辑,开发者可以为预设添加内置零件,调整该零件的属性,以达到该零件所定义的功能。
|
||||
|
||||
目前已有的内置零件可在官网开发指南→玩法开发→预设玩法编程→理解预设系统→[内置零件](../0-理解预设系统/11-内置零件/00-触发器零件.md)找到相关介绍和说明。
|
||||
|
||||
## 什么是(模板)零件
|
||||
(模板)零件是内置在编辑器内内置的一些具有特定功能的零件,开发者可以通过创建模板零件的副本,对该零件进行修改,以达到自己的目标。通俗来讲,模板零件就是为了实现某种功能,在空零件的基础上进行代码的编写,提供给开发者自己在副本上修改该零件的代码逻辑。
|
||||
|
||||
目前已有的模板零件有
|
||||
|
||||
### 1、添加零件测试零件(AddPartTestPart)
|
||||
#### 简介:
|
||||
添加零件测试演示了如何通过代码进行添加和删除零件
|
||||
|
||||
#### 逻辑简述:
|
||||
该零件在TickClient 每隔 self.interval 的时间会执行一次 添加零件或删除零件函数(随机行为),添加的零件类型和执行的时间间隔可通过属性面板设置
|
||||
|
||||
#### 使用方法:
|
||||
1. 确保要添加的零件存在于存档中
|
||||
2. 设置好执行间隔
|
||||
3. 启动运行,查看log打印信息
|
||||
|
||||

|
||||
|
||||
运行游戏后可在测试日志窗口看到对应日志
|
||||
|
||||

|
||||
|
||||
### 2、零件接口测试零件(ApiTestPart)
|
||||
#### 零件简介:
|
||||
零件接口测试零件是提供几个简单的例子说明如何对零件接口进行测试。
|
||||
#### 主要逻辑描述:
|
||||
监听服务器ServerChatEvent接口,可以获取聊天窗口的输入,并根据预先实在好的消息分发调用不同的函数。
|
||||
这里预先设置好调用有:
|
||||
spawn -> 复制当前所有的实体预设实例
|
||||
add_part -> 为所有的实体预设实例添加零件
|
||||
destory_entity_preset -> 销毁所有的预设实例
|
||||
show_preset_info->控制台打印当前预设实例的信息
|
||||
#### 使用方法:
|
||||
1. 把本零件绑定在一个预加载的空预设上,
|
||||
2. 确保关卡中至少有一个实体预设实例
|
||||
3. 确保本零件的“零件类型”属性所有指定的零件存在
|
||||
4. 点击“运行”,在对话框中输入:spawn、add_part、destory_entity_preset、show_preset_info并查看效果
|
||||
|
||||
|
||||
### 3、日志零件(MyLogPart)
|
||||
#### 零件简介:
|
||||
日志零件演示如何打印log输出
|
||||
#### 逻辑简述:
|
||||
TickClient函数每个时间间隔会打印日志,时间间隔通过self.interval变量随机控制,日志内容是该零件的路径名和世界坐标
|
||||
#### 使用方法:
|
||||
1.通过属性面板设置好打印间隔区间
|
||||
2.启动运行,查看log打印信息
|
||||
### 4、网络同步零件(NetworkReplicatedPart)
|
||||
#### 零件简介:
|
||||
演示如何在零件中快捷实现网络同步的功能
|
||||
#### 逻辑简述:
|
||||
在零件类初始化函数中设置需要网络同步的字段在replicated中,运行时会从服务器到客户端同步同步这些字段的值
|
||||
#### 使用方法:
|
||||
把该零件挂接在预加载预设,然后启动游戏,查看log
|
||||
### 5、分裂零件(ReplicatePart)
|
||||
#### 零件简介:
|
||||
分裂零件主要是配合实体预设达到受伤分裂的玩法功能。
|
||||
#### 逻辑简述:
|
||||
1. 初始化时指定实体血量(可配置)
|
||||
2. 当实体受到实际伤害时,若此次攻击未击杀实体,则在实体附加生成一个新的实体预设
|
||||
3. 若未勾选循环生成(可配置),则移除新生成实体预设下的分裂零件,否则当新生成的实体受到伤害时,同样会分裂新的实体。
|
||||
|
||||
#### 使用方式:
|
||||
挂载在实体预设下,视需要配置零件属性即可。
|
||||
|
||||
### 6、变化动画零件(TransformAnimationPart)
|
||||
#### 零件简介:
|
||||
变换动画零件展示了如何通过零件来操作游戏对象的变换
|
||||
|
||||
#### 逻辑简述:
|
||||
1. 此零件在运行时,会动态设置父亲节点的变换(相对位移、旋转与缩放)。
|
||||
2. 在给定的持续时间和给定的关键帧参数下,此零件会进行线性插值,得出每一帧需要设置的变换,在tick的过程中进行变换设置。
|
||||
3. 这里选取了特效是为了方便展示效果,由于特效是纯客户端的,因此只需要实现客户端对应的接口即可。
|
||||
4. 实际上你可以把它挂接到其他类型的预设上,如实体预设,实现移动,放大的一些效果,但需要实现在零件的服务端接口。
|
||||
5. 你也可以把所有客户端接口,全部改为服务端接口(如InitClient => InitServer等),也能达到类似的效果,但性能会很差,因为每次变换都需要服务端通知客户端进行变换。
|
||||
|
||||
#### 使用方法:
|
||||
1. 将此零件挂接在任意特效底下。
|
||||
2. 编辑该零件的关键帧属性,控制在动画期间,每个关键帧父亲特效的目标变换(相对位移、旋转与缩放)。
|
||||
3. 如果希望特效动画只播放一次,勾选自动销毁,如果希望特效循环播放,勾选循环。
|
||||
### 7、测试触发器零件(TriggerTestPart)
|
||||
#### 零件简介:
|
||||
测试触发器零件,需要和触发器零件配合使用
|
||||
#### 逻辑简述:
|
||||
当实体进入、离开、停留在对应触发器零件的区域内时,打印相关日志
|
||||
#### 使用方法:
|
||||
1.将该零件和对应的触发器零件挂接到同一预设下
|
||||
2.通过属性面板设置好对应的触发器零件的名称
|
||||
3.启动运行,查看log打印信息
|
||||
|
||||
### 8、输入绑定零件(InputBindPart)
|
||||
#### 零件简介:
|
||||
输入绑定零件提供了通用的系统按键监听广播流程
|
||||
#### 逻辑简述:
|
||||
开发者可以在"按键绑定"属性中定义自定义事件以及唤起该事件所需要按下的系统按键。
|
||||
当某个按键被按下或抬起时,对应的自定义事件就会被广播。
|
||||
开发者可以在零件代码中调用零件的事件监听函数监听,或在mod代码中调用系统监听函数监听。
|
||||
#### 使用方法:
|
||||
1.通过属性面板设置自定义事件以及绑定的系统按键
|
||||
2.在需要的脚本中编写监听函数监听对应的自定义事件
|
||||
3.启动运行,查看回调运行情况
|
||||
### 9、预设测试零件(PresetDebugPart)
|
||||
#### 零件简介:
|
||||
预设测试零件用于输出当前指定端的预设信息
|
||||
#### 逻辑简述:
|
||||
服务器通过监听ServerChatEvent事件获取玩家聊天窗口的输入,根据输入内容,判断需要打印哪个端的预设信息
|
||||
#### 使用方法:
|
||||
聊天窗口输入下列字符以调用对应的功能:
|
||||
preset client --> 输出当前客户端的所有预设实例信息
|
||||
preset server --> 输出服务器所有预设实例信息
|
||||
preset client <id> --> 输出当前客户端的指定id的变换对象实例信息
|
||||
preset server <id> --> 输出服务器的指定id的变换对象实例信息
|
||||
输出信息格式为蓝色字体:节点显示路径 [当前节点及递归节点id列表]
|
||||
0
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/README.md
Normal file
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/1.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/2.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/3.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate1.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate10.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate11.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate12.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate13.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate14.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate15.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate16.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate17.png
Normal file
|
After Width: | Height: | Size: 583 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate18.png
Normal file
|
After Width: | Height: | Size: 647 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate19.png
Normal file
|
After Width: | Height: | Size: 887 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate2.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate20.png
Normal file
|
After Width: | Height: | Size: 756 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate21.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate3.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate4.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate5.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate6.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate7.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate8.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/AttrTemplate9.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 101 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/customProperty.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/func_explain.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 52 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/lj0.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/lj1.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/visible.gif
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z1.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z10.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z11.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z12.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z13.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z2.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z3.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z4.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z5.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z6.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z7.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z8.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z9.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z_1.png
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z_2.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z_3.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z_4.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z_5.png
Normal file
|
After Width: | Height: | Size: 430 KiB |
BIN
mcguide/20-玩法开发/14-预设玩法编程/12-深入理解零件/images/z_6.png
Normal file
|
After Width: | Height: | Size: 57 KiB |