同步官网文档8m_25d

This commit is contained in:
kwiilh
2025-08-25 18:36:29 +08:00
parent 4dc0ecf18d
commit 9e8855eeb4
5089 changed files with 8798 additions and 4799 deletions

View File

@@ -1,291 +0,0 @@
---
front: https://nie.res.netease.com/r/pic/20210728/2dc2a94f-71f6-4cc5-8700-3c3696f79a0c.jpg
hard: 进阶
time: 30分钟
---
# CPU优化
## 前言
CPU是设备计算力水平的代表CPU性能越好代表相同时间内能支持的运算数量越多。无论是内存的分配与释放渲染环境的准备与指令发送等等都需要CPU的参与CPU的优化能对设备整体性能产生比较大的影响。
## 缓存的使用(内存换CPU)
对象的重复创建与销毁会有一定性能消耗,对于需要频繁使用的数据,建议保存起来,下次从内存取出来直接使用,是一种常用的空间换时间(内存换CPU)的优化手段,对于减少游戏卡顿有较好效果。
### 避免在tick函数内使用import
import模块的消耗并没有小到可以忽略的地步建议挪到文件的顶部进行import。如果这样会导致循环引用则可以将模块缓存为类的成员变量
- 错误写法:
```python
class DemoClientSystem(ClientSystem):
def Update(self):
# 在每帧执行的逻辑内import模块
import mod.client.extraClientApi as clientApi
clientApi.xxx
```
- 正确写法:
```python
# 在文件顶部import模块
import mod.client.extraClientApi as clientApi
class DemoClientSystem(ClientSystem):
def Update(self):
clientApi.xxx
```
如果两个模块需要相互引用那么同时在文件顶部import对方会导致循环引用报错则可以用下面的方法处理
```python
class DemoClientSystem(ClientSystem):
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
# 假设当前模块与另一个otherModule模块需要相互引用
import demoScripts.client.otherModule as otherModule
self.otherModule = otherModule
def Update(self):
self.otherModule.xxx
```
### 避免多次初始化常量
- 错误写法:
在频繁调用的函数中进行声明例如每次Update的时候
```python
class DemoClientSystem(ClientSystem):
def Update(self):
# 常量,每帧创建,实际中可能这里会是比较多的数据
bigDict = {
(-1, -1): 1,
(-1, 0): 2,
(-1, 1): 3,
(0, -1): 4,
(0, 0): 5,
(0, 1): 6,
(1, -1): 7,
(1, 0): 8,
(1 1): 9,
}
# 读取常量做一些逻辑
do_something(bigDict)
```
- 正确写法:
包含数据比较多的一些常量特别是List或者Dict类型的可以放到类的__init__函数当中
```python
class DemoClientSystem(ClientSystem):
# 构造函数
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
# 在初始化时创建
self.bigDict = {
(-1, -1): 1,
(-1, 0): 2,
(-1, 1): 3,
(0, -1): 4,
(0, 0): 5,
(0, 1): 6,
(1, -1): 7,
(1, 0): 8,
(1 1): 9,
}
def Update(self):
do_something(self.bigDict)
```
### 缓存多次用到的中间数据
一些方法多次调用的返回值是一样,可以使用临时变量缓存,不需要重复调用
- 错误写法:
```python
class DemoServerSystem(ServerSystem):
# 监听的ServerItemUseOnEvent事件回调
def ServerItemUseOnEvent(self, args):
# 设置多个方块
self.SetBlock(args['dimensionId'], (args['x']-1, args['y'], args['z']), 'minecraft:air')
self.SetBlock(args['dimensionId'], (args['x']-1, args['y'], args['z']), 'minecraft:air')
self.SetBlock(args['dimensionId'], (args['x'], args['y'], args['z']), 'minecraft:air')
self.SetBlock(args['dimensionId'], (args['x'], args['y'], args['z']-1), 'minecraft:air')
self.SetBlock(args['dimensionId'], (args['x'], args['y'], args['z']+1), 'minecraft:air')
def SetBlock(self, dimensionId, pos, blockName):
serverApi.GetEngineCompFactory().CreateBlockInfo(levelId).SetBlockNew(pos, {'name': blockName}, 0, dimensionId)
```
- 正确写法:
```python
# compFactory使用缓存
serverCompFactory = serverApi.GetEngineCompFactory()
class DemoServerSystem(ServerSystem):
# 监听的ServerItemUseOnEvent事件回调
def ServerItemUseOnEvent(self, args):
# 对字典内的值做缓存
dimensionId = args['dimensionId']
x = args['x']
y = args['y']
z = args['z']
self.SetBlock(dimensionId, (x-1, y, z), 'minecraft:air')
self.SetBlock(dimensionId, (x-1, y, z), 'minecraft:air')
self.SetBlock(dimensionId, (x, y, z), 'minecraft:air')
self.SetBlock(dimensionId, (x, y, z-1), 'minecraft:air')
self.SetBlock(dimensionId, (x, y, z+1), 'minecraft:air')
def SetBlock(self, dimensionId, pos, blockName):
serverCompFactory.CreateBlockInfo(levelId).SetBlockNew(pos, {'name': blockName}, 0, dimensionId)
```
### 使用dict代替多个else if
当条件判断的分支很多时dict的性能会比一连串的else高很多
- 错误写法:
```python
serverCompFactory = serverApi.GetEngineCompFactory()
class DemoServerSystem(ServerSystem):
def HandleBlocks(self, pos, dimensionId):
# 获取方块信息
blockIdentifier = serverCompFactory.CreateBlockInfo(levelId).GetBlockNew(pos, dimensionId)[0]
# 根据方块类型做出不同的处理
if blockIdentifier == "minecraft:iron_ore":
self.handleIronBlock()
elif blockIdentifier == "minecraft:gold_ore":
self.handleGoldBlock()
elif blockIdentifier == "minecraft:diamond_ore":
self.handleDiamondBlock()
```
- 正确写法:
```python
serverCompFactory = serverApi.GetEngineCompFactory()
class DemoServerSystem(ServerSystem):
def __init__(self):
# 注册处理函数
self.blockHandlers = {
"minecraft:iron_ore": self.handleIronBlock,
"minecraft:gold_ore": self.handleGoldBlock,
"minecraft:diamond_ore": self.handleDiamondBlock,
}
def HandleBlocks(self, data):
blockIdentifier = serverCompFactory.CreateBlockInfo(levelId).GetBlockNew(pos, dimensionId)[0]
# 从dict中选取处理函数
handler = self.blockHandlers.get(blockIdentifier)
if handler:
handler()
```
## 分帧实时性换CPU
同一时刻内处理大量的逻辑,容易造成卡顿。这时候需要把逻辑执行的时间错开到多帧去执行,让每一帧的任务量不要太重。
### 大批量修改数据分多帧处理
这里以方块为例:
- 错误写法:
(同一时刻全部处理,需要处理 100* 100 * 100 即一百万个方块,必然会卡)
```python
# 修改某个区域 100 * 100 * 100范围内的方块为空气
def SetBlocksToAir(self, fromPos):
blockcomp = serverApi.CreateComponent(id, "Minecraft", "blockInfo")
for x in range(1, 100):
for y in range(1, 100):
for z in range(1, 100):
blockcomp.SetBlockNew((fromPos[0] + x, fromPos[1] + y, fromPos[2] + z), {'name':'minecraft:air'})
```
- 正确写法:
(分开每帧只处理5个)
```python
# 修改某个区域 100 * 100 * 100范围内的方块为空气
def SetBlocksToAir(self, fromPos):
# 命令队列
self.posList = []
self.posIndex = 0
for x in range(1, 100):
for y in range(1, 100):
for z in range(1, 100):
self.posList.append((fromPos[0] + x, fromPos[1] + y, fromPos[2] + z))
# 被引擎直接执行的父类的重写函数引擎会执行该Update回调1秒钟30帧
def Update(self):
if self.posList:
posListLen = len(self.posList)
blockcomp = serverApi.CreateComponent(id, "Minecraft", "blockInfo")
#每帧处理5个
handleNum = 5
while(handleNum > 0 and self.posIndex < posListLen):
blockcomp.SetBlockNew(self.posList[self.posIndex], {'name':'minecraft:air'})
self.posIndex = self.posIndex + 1
handleNum = handleNum - 1
# 全部处理完成
if self.posIndex >= posListLen:
self.posList = None
```
### 非重要逻辑降帧处理
不要每帧执行所有逻辑更新,不同的逻辑实际中根据实时性要求进行间隔更新
- 错误写法:
(每帧执行所有更新逻辑)
```python
def Update(self):
self.do_something1()
self.do_something2()
self.do_something3()
```
- 正确写法:
(分开每帧只处理5个)
```python
class DemoClientSystem(ClientSystem):
# 构造函数
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.tick = 0
def Update(self):
self.tick = self.tick + 1
# 重要逻辑每帧执行
self.do_something1()
if self.tick % 5 == 0:
# 次要逻辑降帧执行
self.do_something2()
if self.tick % 10 == 0:
# 更次要的逻辑,使用更低的帧率执行
self.do_something3()
```
### 少用轮询逻辑
使用事件或一些适用的接口来代替每帧尝试的操作。
假想有一个需求:我想删除一个实体,但是当前这个实体没有被加载
- 错误写法:
每帧尝试删除该实体,直到成功为止
- 推荐写法:
1. 监听AddEntityServerEvent在该实体的回调中删除。
2. 如果该实体是手动创建的可以使用SetPersistence接口将其设置为不存盘那就不再需要处理该实体被卸载而无法删除的情况。

View File

@@ -1,222 +0,0 @@
---
front: https://nie.res.netease.com/r/pic/20210728/5a263f49-e1f3-4a9a-96b3-307b11848590.png
hard: 进阶
time: 30分钟
---
# Shader优化
## 前言
为了做出更加炫酷的效果我们往往会自定义材质然后定义自己的Shader或者直接重写替换原版Shader特别是目前的很多光影MOD基本都需要对Shader进行修改。Shader与GPU性能密切相关Shader写得好能让配置不是很高的玩家也能流畅体验炫酷的效果。写得不好则可能导致高端机也很卡。
## 尽量少用 if else 条件语句
GPU是并行处理逻辑的采用SIMD单指令多数据结构, 同一段代码同一时刻会被多个GPU的处理单元同时处理这段代码的执行耗时取决于会被执行到的时间最长的代码。为了充分发挥GPU的并行性我们尽量少用条件分支逻辑让所有GPU处理单元都执行相同的代码。
if else的写法如果底下逻辑不多大部分可以合并则常常可以改用step()函数进行优化
step(a,b)的功能为:
```glsl
b >= a则返回1,否则返回0
```
所以如果有这么一个写法:
```glsl
if(r >= 0.5)
{
r = 0.6;
}else{
r = 0.4;
}
```
则应该写成:
```glsl
r = 0.4 + step(0.5, r) * (0.6 - 0.4);
```
逻辑上相当于若r >= 0.5 ,则:
```glsl
r = 0.4 + 1 * 0.2 = 0.6;
```
否则r < 0.5则:
```glsl
r = 0.4 + 0 * 0.2 = 0.4;
```
由此即可消去if else语句
- 错误写法:
(if else大量使用)
```python
// 简单的卡通着色例子把连续的颜色值映射到几个特殊的离散的值上面
//根据传入的颜色值取得一个新的颜色值, 这里为了展示简单我们用仅用一个通道进行举例
void main()
{
color.r = getNewRedColor(color.r);
...(省略无关代码)
}
float getNewRedColor(float r)
{
float newR;
if(r >= 0.6)
{
newR = 0.8;
}else if(r >= 0.3)
{
newR = 0.5;
}else{
newR = 0.1;
}
return newR;
}
```
- 正确写法:
(分开每帧只处理5个)
```python
// 简单的卡通着色例子把连续的颜色值映射到几个特殊的离散的值上面
//根据传入的颜色值取得一个新的颜色值, 这里为了展示简单我们用仅用一个通道进行举例
void main()
{
color.r = getNewRedColor(color.r);
...(省略无关代码)
}
float getNewRedColor(float r)
{
float newR = 0.0;
newR = newR + step(0.6, r) * 0.8;
newR = newR + step(0.3, r) * step(r, 0.6) * 0.5;
newR = newR + step(r, 0.3) * 0.1;
return newR;
}
```
## 循环语句
for, while这类的循环语句内部实现其实也会有条件判断if else语句并行性比较低所以如果可以不用尽量不用但这并不是让大家去复制粘贴多少次代码这没有意义则是尽量从逻辑上避免循环逻辑的出现如果实在需要使用则建议循环体内不要做太多耗性能的操作
除此之外循环变量一定要记得初始化变量的初始值在不同设备上有时候是不一样的, 比如int的初始值并不一定在所有设备上都是0
- 错误写法:
(循环变量i没有初始化)
```python
for(int i; i < 5; i ++)
{
func();
}
```
在一些设备上i的值会被初始化为0循环5次没有问题但在一些设备上i的默认值可能是一个没有意义的数甚至是负数例如是2147483648上面循环则会循环超级多次玩家会直接卡到动不了
- 正确写法:
(分开每帧只处理5个)
```python
//这里i一定要给一个默认值
for(int i = 0; i < 5; i ++)
{
func();
}
```
## 精美贴图开关
开关在游戏中的位置设置->视频->精美贴图
开发者可根据玩家是否开启精美贴图执行不一样的shader逻辑。这里需要声明两个文件materials/sad.json 和 materials/fancy.json。我们先看下原版中两个文件的内容
```python
sad.json:
[
{"path":"materials/sad.material"},
{"path":"materials/entity.material"},
{"path":"materials/terrain.material"},
{"path":"materials/portal.material"},
{"path":"materials/barrier.material"},
{"path":"materials/wireframe.material"}
]
fancy.json:
[
{"path":"materials/fancy.material", "+defines":["FANCY"]},
{"path":"materials/entity.material", "+defines":["FANCY"]},
{"path":"materials/terrain.material", "+defines":["FANCY"]},
{"path":"materials/hologram.material"},
{"path":"materials/portal.material", "+defines":["FANCY"]},
{"path":"materials/barrier.material"},
{"path":"materials/wireframe.material"}
]
```
开启精美贴图的时候会加载下面的材质不开启的话加载上面的材质。我们用一个材质进行举例比如sad和fancy中都有的terrain.material材质fancy中不同在于额外定义了FANCY字段则在Shader中可以这样写
```python
void main()
{
#ifdef FANCY
//这里可以做更多的逻辑渲染更好的效果
renderBeautiful();
#else
// 这里是关闭了精美贴图这里不应该执行过多逻辑只需要提供简单的显示效果就可以了
renderSimple();
#endif
}
```
## 降低精度
通常来说我们写的shader不需要过于关注精度因为大部分情况下性能瓶颈不在这里但一些过于复杂大部分玩家都说卡的MOD建议可以考虑在精度方面做一些优化。
shader中变量精度越低GPU运算越快精度分为3档关键字分别为
```glsl
低:lowp
中:mediump
高:highp
```
int整数建议256内的整数使用lowp, 1024内的整数使用mediump其它情况则使用highp
float(浮点)建议256内的浮点数使用lowp, 16384内的浮点数使用mediump其它情况则使用highp
默认精度:
```glsl
顶点着色器中float, int均为highp
像素着色器中int为mediumpfloat根据设备不同无默认精度
```
声明方法例子(直接把精度关键字加在变量类型前面)
```glsl
lowp float color
```
## 移除无用变量与逻辑
部分开发者编写Shader的时候可能会偷下懒从自己以前写的代码里面复制粘贴过来但这部分代码可能有很多逻辑或者变量都没有用上导致大量的运算是无意义浪费性能的需要去掉。
- 错误写法:
(包含大量无用逻辑)
```python
void main()
{
//这里声明多个变量又或许是从其它地方复制过来具体值先省略
A = ...;
B = ...;
C = ...;
//这里只用了变量AB和C都没有用到
DoSomeThingWithA(A);
return;
}
```
- 正确写法:
(删除无用逻辑)
```python
void main()
{
//只留下A把其它没用上的都删除掉
A = ...;
//这里只用了变量AB和C都没有用到
DoSomeThingWithA(A);
return;
}
```
## 分级做多个MOD版本
开发者可根据Shader复杂度上架不同版本举个例子例如可以有“网易光影低配版” “网易光影高配版”,玩家看名字就大概知道对性能有不同的要求了,让玩家下载时自行选择。

View File

@@ -127,4 +127,4 @@ class PlayerActionClientSystem(ClientSystem):
3. 使用`SetAttr`设置属性值,自动同步到客户端
4. 客户端的回调函数被触发更新相应的Molang值
#### 其他性能优化教程,敬请期待...
#### 其他性能优化教程,敬请期待...

View File

@@ -21,7 +21,7 @@ MC的强大在于他的开放性为开发者提供了大量系统功能和Api
- 错误写法:
(自定义的翅膀定义了每秒发射200到300个粒子太多了如果同屏有6个玩家就是1200到1800个粒子,粒子的更新与渲染都有一定性能消耗)
```json
# 某个翅膀的定义 testWind.json:
# 某个翅膀的定义 testWind.json:
{
"particleeffect": {
"emissionrate": {
@@ -29,14 +29,14 @@ MC的强大在于他的开放性为开发者提供了大量系统功能和Api
"min": "200.0"
},
...(省略无关代码)
}
}
```
- 正确写法:
(以下几个方案可结合使用)
```python
1方案1, 严格控制粒子数量在100左右
# 某个翅膀的定义 testWind.json:
# 某个翅膀的定义 testWind.json:
{
"particleeffect": {
"emissionrate": {
@@ -101,7 +101,7 @@ MC的强大在于他的开放性为开发者提供了大量系统功能和Api
print("server talk to me, content:", data["content"])
# 服务端
class UIDemoServerSystem(ServerSystem):
class UIDemoServerSystem(ServerSystem):
def talkToClient(self, playerId):
# 广播方式
self.BroadcastToAllClient("OnServerSay", {
@@ -127,10 +127,10 @@ MC的强大在于他的开放性为开发者提供了大量系统功能和Api
print("server talk to me, content:", data["content"])
# 服务端
class UIDemoServerSystem(ServerSystem):
class UIDemoServerSystem(ServerSystem):
def talkToClient(self, playerId):
# 点对点发送的方式
self.NotifyToClient(playerId, "OnServerSay", {
self.NotifyToClient(playerId, "OnServerSay", {
"content": "hello!"
})