2.6
This commit is contained in:
BIN
docs/mcguide/20-玩法开发/18-性能优化/images/after_optimization.png
(Stored with Git LFS)
Normal file
BIN
docs/mcguide/20-玩法开发/18-性能优化/images/after_optimization.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/mcguide/20-玩法开发/18-性能优化/images/before_optimization.png
(Stored with Git LFS)
Normal file
BIN
docs/mcguide/20-玩法开发/18-性能优化/images/before_optimization.png
(Stored with Git LFS)
Normal file
Binary file not shown.
131
docs/mcguide/20-玩法开发/18-性能优化/代码进阶优化.md
Normal file
131
docs/mcguide/20-玩法开发/18-性能优化/代码进阶优化.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
front: https://nie.res.netease.com/r/pic/20210728/5507b669-4c6f-4958-b5d0-b8556ab4cfb5.png
|
||||
hard: 进阶
|
||||
time: 10分钟
|
||||
---
|
||||
|
||||
# ModAPI代码进阶优化
|
||||
|
||||
> 本文档介绍了一些高级的ModAPI代码优化技巧,帮助开发者们编写更高效的代码。
|
||||
> 我们将结合一些开发情景,逐一讲解优化流程
|
||||
|
||||
## 使用实体Attr实现Molang同步
|
||||
|
||||
### 背景说明
|
||||
|
||||
在开发过程中,假设我们需要给玩家添加创造飞行或鞘翅动画效果。但是翻找wiki却发现原版并没有提供相关的Molang,所以我们需要自定义以下两个Molang变量:
|
||||
|
||||
```
|
||||
query.mod.ysm_is_create_flying # 是否正在创造飞行
|
||||
query.mod.ysm_is_elytra_flying # 是否正在鞘翅飞行
|
||||
```
|
||||
|
||||
### 实现方案对比
|
||||
|
||||
**不推荐的方案:**
|
||||
|
||||
通过tick函数持续获取玩家状态并与客户端通信来设置Molang值
|
||||
这种方式会导致严重的性能开销,十分的不友好
|
||||
|
||||
**推荐的方案:**
|
||||
|
||||
结合以下两个关键功能实现:
|
||||
|
||||
1. [OnPlayerActionServerEvent](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/事件/玩家.html#onplayeractionserverevent) - 用于监听玩家动作状态
|
||||
2. [实体自定义属性](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/接口/实体/索引.html?catalog=1#自定义属性) - 实现Molang值的自动同步
|
||||
|
||||
### 工作原理
|
||||
|
||||
众所周知,当使用服务端的`SetAttr`接口设置属性值后,系统会自动将这些值同步到客户端
|
||||
在实体Attr文档中,我们可以找到一个关键接口`RegisterUpdateFunc`,它用于注册属性值变化时的回调函数
|
||||
这个机制就是实现实体Molang值自动同步的核心
|
||||
|
||||
### Attr同步优点
|
||||
|
||||
- 高性能、低功耗的实现方式
|
||||
- 使用游戏原生发包的Attr通信,比直接通信接口更快
|
||||
- 自动处理可见渲染范围内的同步,无需手动监听`AddPlayerCreatedClientEvent`事件
|
||||
- 适合在tick内进行多次操作,统一更新数据
|
||||
|
||||
### 代码实现
|
||||
|
||||
##### 服务端代码
|
||||
|
||||
```python
|
||||
# 服务端代码实现
|
||||
ServerComp = serverApi.GetEngineCompFactory()
|
||||
|
||||
class PlayerActionServerSystem(ServerSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
ServerSystem.__init__(self, namespace, systemName)
|
||||
|
||||
def OnPlayerActionServerEvent(self, args):
|
||||
# 玩家动作事件,当玩家开始/停止某些动作时触发该事件
|
||||
playerId = args["playerId"]
|
||||
actionType = args["actionType"]
|
||||
modAttr = ServerComp.CreateModAttr(playerId)
|
||||
|
||||
# 使用鞘翅飞行/创造飞行的动作枚举值为(15,16,34,35)
|
||||
if actionType == 34:
|
||||
# 开始创造飞行
|
||||
modAttr.SetAttr("playerIsCreateFlying", 1)
|
||||
elif actionType == 35:
|
||||
# 停止创造飞行
|
||||
modAttr.SetAttr("playerIsCreateFlying", 0)
|
||||
elif actionType == 15:
|
||||
# 开始鞘翅飞行
|
||||
modAttr.SetAttr("playerIsElytraFlying", 1)
|
||||
elif actionType == 16:
|
||||
# 停止鞘翅飞行
|
||||
modAttr.SetAttr("playerIsElytraFlying", 0)
|
||||
```
|
||||
|
||||
##### 客户端代码
|
||||
|
||||
```python
|
||||
# 客户端代码实现
|
||||
ClientComp = clientApi.GetEngineCompFactory()
|
||||
levelId = clientApi.GetLevelId()
|
||||
playerId = clientApi.GetLocalPlayerId()
|
||||
|
||||
# 注册自定义Molang变量
|
||||
ClientComp.CreateQueryVariable(levelId).Register("query.mod.ysm_is_create_flying", 0)
|
||||
ClientComp.CreateQueryVariable(levelId).Register("query.mod.ysm_is_elytra_flying", 0)
|
||||
|
||||
# key为需要监听的attr名称,value为需要设置的对应Molang变量名称
|
||||
queryDict = {
|
||||
"playerIsFlying": "query.mod.ysm_is_flying",
|
||||
"playerIsElytraFlying": "query.mod.ysm_is_elytra_flying"
|
||||
}
|
||||
|
||||
# 根据queryDict自动配置监听,简化代码
|
||||
def CreateAttrCallBack(bindQuery):
|
||||
def _eventFuckCallBack(args):
|
||||
ClientComp.CreateQueryVariable(args["entityId"]).Set(bindQuery, args["newValue"])
|
||||
return _eventFuckCallBack
|
||||
|
||||
|
||||
class PlayerActionClientSystem(ClientSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
ClientSystem.__init__(self, namespace, systemName)
|
||||
# 注册本地玩家的属性值变化回调函数
|
||||
modAttr = ClientComp.CreateModAttr(playerId)
|
||||
for attr, query in queryDict.items():
|
||||
ClientComp.CreateModAttr(playerId).RegisterUpdateFunc(attr, CreateAttrCallBack(query))
|
||||
|
||||
def OnAddPlayerAOIClient(self, args):
|
||||
"""玩家加入游戏或进入视野时触发,注册属性值变化回调"""
|
||||
pId = args["playerId"]
|
||||
modAttr = ClientComp.CreateModAttr(pId)
|
||||
for attr, query in queryDict.items():
|
||||
ClientComp.CreateModAttr(pId).RegisterUpdateFunc(attr, CreateAttrCallBack(query))
|
||||
```
|
||||
|
||||
### 工作流程说明
|
||||
|
||||
1. 客户端为本地玩家和其他玩家注册属性值变化的回调函数
|
||||
2. 服务端通过`OnPlayerActionServerEvent`事件监测玩家的飞行状态
|
||||
3. 使用`SetAttr`设置属性值,自动同步到客户端
|
||||
4. 客户端的回调函数被触发,更新相应的Molang值
|
||||
|
||||
#### 其他性能优化教程,敬请期待...
|
||||
177
docs/mcguide/20-玩法开发/18-性能优化/配置文件优化.md
Normal file
177
docs/mcguide/20-玩法开发/18-性能优化/配置文件优化.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
front: https://nie.res.netease.com/r/pic/20210728/5507b669-4c6f-4958-b5d0-b8556ab4cfb5.png
|
||||
hard: 进阶
|
||||
time: 20分钟
|
||||
---
|
||||
|
||||
# 配置文件优化指南
|
||||
|
||||
本文介绍了如何优化Python配置文件的内存使用。
|
||||
|
||||
## 1. 使用结构优化建筑生成
|
||||
|
||||
在建筑生成和放置方面,我们推荐使用结构文件而不是Python配置。
|
||||
|
||||
### 使用feature\_rules控制建筑生成
|
||||
|
||||
参考教程:[自定义特征](../15-自定义游戏内容/4-自定义维度/4-自定义特征.md)
|
||||
|
||||
**优点:**
|
||||
|
||||
* 无需代码处理,无卡顿效果
|
||||
* 采用微软原生机制,性能优异
|
||||
|
||||
**缺点:**
|
||||
|
||||
* feature\_rules文件中的molang表达式不宜过于复杂
|
||||
* 复杂的表达式可能导致内存占用增加,影响游戏加载速度
|
||||
|
||||
### 通过API放置structure
|
||||
|
||||
|
||||
参考API:<a href="../../../mcdocs/1-ModAPI/接口/世界/地图.html#placestructure" rel="noopenner">PlaceStructure</a>
|
||||
|
||||
**优点:**
|
||||
|
||||
* 提供灵活的代码控制能力
|
||||
* 可实现复杂的生成规则
|
||||
|
||||
**缺点:**
|
||||
|
||||
* 放置时可能出现短暂卡顿
|
||||
|
||||
## 2. 使用方块调色板节约内存
|
||||
|
||||
方块调色板可用于保存建筑数据。使用<a href="../../../mcdocs/1-ModAPI/接口/世界/方块组合.html#getblockpalettebetweenpos" rel="noopenner">GetBlockPaletteBetweenPos</a>等接口获取方块调色板后,使用<a href="../../../mcdocs/1-ModAPI/接口/方块/方块调色板.html#serializeblockpalette" rel="noopenner">SerializeBlockPalette</a>将调色板转化成字典后,手动存入配置文件中。
|
||||
|
||||
**优点:**
|
||||
|
||||
* 方块调色板是高度压缩的字典结构,可最大程度节约内存
|
||||
* 对应的放置接口<a href="../../../mcdocs/1-ModAPI/接口/世界/方块组合.html#setblockbyblockpalette" rel="noopenner">SetBlockByBlockPalette</a>是原生c++批量放置,性能较好
|
||||
* 可实现复杂的生成规则
|
||||
|
||||
**缺点:**
|
||||
|
||||
* 放置时可能出现短暂卡顿
|
||||
|
||||
## 3. 使用引用避免重复定义
|
||||
|
||||
在配置文件中,应避免重复定义相同的配置项,而是采用引用方式:
|
||||
|
||||
* 错误的写法
|
||||
|
||||
```python
|
||||
# 存在大量重复的方块数据定义
|
||||
building = {
|
||||
'build_A': {
|
||||
'size': [0, 1, 2],
|
||||
'blocks': [
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
* 正确的写法
|
||||
|
||||
```python
|
||||
# 定义基础方块数据
|
||||
AirBlock = {'aux': 0, 'name': 'minecraft:air'}
|
||||
GrassBlock = {'aux': 0, 'name': 'minecraft:grass'}
|
||||
StoneBlock = {'aux': 0, 'name': 'minecraft:stone'}
|
||||
|
||||
# 通过引用复用方块数据
|
||||
building = {
|
||||
'build_A': {
|
||||
'size': (0, 1, 2),
|
||||
'blocks': [
|
||||
AirBlock, AirBlock, StoneBlock, AirBlock, AirBlock,
|
||||
GrassBlock, AirBlock, GrassBlock, AirBlock, AirBlock, StoneBlock,
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 使用元组替代字典
|
||||
|
||||
对于只读的配置数据,建议使用元组代替字典,以提高内存使用效率:
|
||||
|
||||
* 错误的写法
|
||||
|
||||
```python
|
||||
# 定义基础方块数据
|
||||
AirBlock = {'aux': 0, 'name': 'minecraft:air'}
|
||||
GrassBlock = {'aux': 0, 'name': 'minecraft:grass'}
|
||||
StoneBlock = {'aux': 0, 'name': 'minecraft:stone'}
|
||||
```
|
||||
|
||||
* 正确的写法
|
||||
|
||||
```python
|
||||
# 使用元组存储只读数据,提高内存效率
|
||||
AirBlock = ('minecraft:air', 0)
|
||||
GrassBlock = ('minecraft:grass', 0)
|
||||
StoneBlock = ('minecraft:stone', 0)
|
||||
```
|
||||
|
||||
## 5. 分割配置文件并动态加载
|
||||
|
||||
如果你的配置文件已经有几十M的大小,建议将配置文件按功能模块分割,采用动态加载方式:
|
||||
|
||||
* 错误的写法
|
||||
|
||||
```python
|
||||
# 一次性导入所有配置
|
||||
from build import build1, build2, build3
|
||||
```
|
||||
|
||||
* 正确的写法
|
||||
|
||||
```python
|
||||
def load_build_config(data):
|
||||
building = data['build']
|
||||
|
||||
if build == '1':
|
||||
# 按需导入配置
|
||||
import build1
|
||||
place_build(data, build1)
|
||||
elif build == '2':
|
||||
# 按需导入配置
|
||||
import build2
|
||||
place_build(data, build)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
* 建议将一个模块切割得尽量细,并结合前面所述的方法减少单个模块的大小,否则动态加载模块也会带来卡顿。
|
||||
* 即便使用动态加载,如果玩家将各个类型的建筑都摆放一次,那么所有配置也会被加载进来,大量占用内存。
|
||||
* 如果想进一步优化,可以将配置存储为json文件,通过json方式使用和卸载,相关接口<a href="../../../mcdocs/1-ModAPI/接口/通用/工具.html#getmodconfigjson" rel="noopenner">GetModConfigJson</a>。
|
||||
* 可以使用`zlib`库压缩数据。
|
||||
|
||||
## 优化效果
|
||||
|
||||
**优化前内存占用715.32M、优化后占用下降到了224.46M**
|
||||
|
||||
优化前的内存占用(使用方块探针工具):
|
||||

|
||||
|
||||
优化后的内存占用(使用方块探针工具):
|
||||

|
||||
|
||||
## 总结
|
||||
|
||||
1. 优先使用结构文件/方块调色板存储大型数据
|
||||
2. 使用类组织相关配置
|
||||
3. 采用引用而不是重复定义
|
||||
4. 使用元组存储只读数据
|
||||
5. 按功能分割配置文件
|
||||
Reference in New Issue
Block a user