feat:上传mcguide-开发指南部份
This commit is contained in:
291
mcguide/20-玩法开发/18-性能优化/CPU优化.md
Normal file
291
mcguide/20-玩法开发/18-性能优化/CPU优化.md
Normal file
@@ -0,0 +1,291 @@
|
||||
---
|
||||
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接口将其设置为不存盘,那就不再需要处理该实体被卸载而无法删除的情况。
|
||||
3
mcguide/20-玩法开发/18-性能优化/README.md
Normal file
3
mcguide/20-玩法开发/18-性能优化/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 前言
|
||||
|
||||
本篇为网易员工多年开发心得,及在排查一些开发者容易卡顿的MOD时归纳得出的一些程序优化方案,供广大开发者参考,MC的性能与生态圈需要大家的努力,游戏平均帧率每提升一点,内存少用一点,都能让更多玩家玩得起我们的游戏,对玩家体验会好很多,另外也期待大家能给我们更多宝贵的意见和建议。
|
||||
222
mcguide/20-玩法开发/18-性能优化/Shader优化.md
Normal file
222
mcguide/20-玩法开发/18-性能优化/Shader优化.md
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
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为mediump,float根据设备不同无默认精度
|
||||
```
|
||||
声明方法例子(直接把精度关键字加在变量类型前面):
|
||||
```glsl
|
||||
lowp float color
|
||||
```
|
||||
|
||||
## 移除无用变量与逻辑
|
||||
|
||||
部分开发者编写Shader的时候可能会偷下懒,从自己以前写的代码里面复制粘贴过来,但这部分代码可能有很多逻辑或者变量都没有用上,导致大量的运算是无意义浪费性能的,需要去掉。
|
||||
|
||||
- 错误写法:
|
||||
(包含大量无用逻辑)
|
||||
```python
|
||||
void main()
|
||||
{
|
||||
//这里声明多个变量,又或许是从其它地方复制过来,具体值先省略
|
||||
A = ...;
|
||||
B = ...;
|
||||
C = ...;
|
||||
|
||||
//这里只用了变量A;B和C都没有用到
|
||||
DoSomeThingWithA(A);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
- 正确写法:
|
||||
(删除无用逻辑)
|
||||
```python
|
||||
void main()
|
||||
{
|
||||
//只留下A,把其它没用上的都删除掉
|
||||
A = ...;
|
||||
|
||||
//这里只用了变量A;B和C都没有用到
|
||||
DoSomeThingWithA(A);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
## 分级做多个MOD版本
|
||||
|
||||
开发者可根据Shader复杂度上架不同版本,举个例子,例如可以有“网易光影低配版”, “网易光影高配版”,玩家看名字就大概知道对性能有不同的要求了,让玩家下载时自行选择。
|
||||
BIN
mcguide/20-玩法开发/18-性能优化/images/textureFlickering.png
Normal file
BIN
mcguide/20-玩法开发/18-性能优化/images/textureFlickering.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
92
mcguide/20-玩法开发/18-性能优化/内存优化.md
Normal file
92
mcguide/20-玩法开发/18-性能优化/内存优化.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
front: https://nie.res.netease.com/r/pic/20210727/82dd4b1e-04e1-4a90-a4c5-1a4d5cec462a.png
|
||||
hard: 进阶
|
||||
time: 20分钟
|
||||
---
|
||||
|
||||
# 内存优化
|
||||
|
||||
## 前言
|
||||
|
||||
内存的优化对中低端机型极其敏感与有意义,随着MC内容的增长,和开发者上架MOD的越来越多。玩家在游戏中可能装载的MOD,能体验的内容也越来越多。中低端设备内存的瓶颈变得越来越明显。所以很有必要做内存上面的优化。
|
||||
|
||||
## 资源大小优化
|
||||
|
||||
### 贴图分辨率
|
||||
|
||||
#### 意义
|
||||
|
||||
贴图的分辨率控制非常重要。首先会直接决定贴图加载的速度和占用的内存。其次因为内部实现会把方块,物品的多张贴图打成一张图集,单张贴图分辨率大也会造成整体这个图集分辨率太大,在使用图集的时候性能也会有所下降,所以不要觉得我有一个物品,用了非常高清分辨率很高的贴图,但只要我不使用这个物品,对游戏性能就没有影响。
|
||||
|
||||
#### 两个原则
|
||||
|
||||
1. 贴图清晰的基础上尽量小:
|
||||
比如500 * 500的贴图如果缩小为100 * 100之后效果相差不大,则建议直接缩小到100 * 100
|
||||
|
||||
2. 贴图尽量为2次幂,尺寸为2次幂的贴图在加载和渲染时比较友好:
|
||||
例如:1200 * 1200的贴图建议缩小到1024 * 1024;
|
||||
20 * 20的贴图建议缩小到16 * 16。
|
||||
|
||||
#### PNG贴图压缩
|
||||
|
||||
使用到PNG贴图的时候,可以用pngquant工具先进行压缩,接近于无损压缩
|
||||
|
||||
### 音频
|
||||
|
||||
音效的资源格式尽可能使用ogg格式的文件,wav格式文件压缩率较低
|
||||
|
||||
|
||||
### 贴图预加载
|
||||
|
||||
贴图资源的加载并不是都在加载存档的时候加载的,很多时候是在需要显示这张贴图的时候才会进行异步加载,而异步加载是需要一定时间的,那就会导致这张图片一段时间内无法显示出来,当执行比较快的图片切换的时候,首次显示图片可能就会出现闪烁问题,比较典型的情况可以参考<a href="../../18-界面与交互/30-UI说明文档.html#SelectionWheel" rel="noopenner"> UI控件SelectionWheel的闪烁问题(注2中)</a>。
|
||||
|
||||
因此,我们需要在加载存档的时候一开始就指定加载这些贴图,保证后续使用的时候它们已经加载进内存,不用再进行异步加载。可以通过文件 "your_resourcepack/textures/preload_textures_list.json" 进行指定,文件若不存在可以手动创建,以上面的问题为例子写一个文件,当前需要预先加载的是如下图的这些图片
|
||||
|
||||

|
||||
|
||||
则 preload_textures_list.json 为
|
||||
```json
|
||||
[
|
||||
"textures/ui/SelectionWheel/1/custom_wheel_selection_0",
|
||||
"textures/ui/SelectionWheel/1/custom_wheel_selection_1",
|
||||
"textures/ui/SelectionWheel/1/custom_wheel_selection_2",
|
||||
"textures/ui/SelectionWheel/1/custom_wheel_selection_3",
|
||||
"textures/ui/SelectionWheel/1/custom_wheel_selection_4",
|
||||
"textures/ui/SelectionWheel/1/custom_wheel_selection_5",
|
||||
"textures/ui/SelectionWheel/1/custom_wheel_selection_default"
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## 动态加载
|
||||
|
||||
资源如果不是频繁使用的,尽量不要预加载,应该在使用到的时候再进行加载
|
||||
|
||||
骨骼模型系统(不常用的骨骼模型应该声明dy_load字段为true)
|
||||
netease_models.json:
|
||||
```json
|
||||
{
|
||||
"datiangou" : {
|
||||
"skeleton" : "skeleton/datiangou_skeleton.json",
|
||||
"mesh" : "mesh/datiangou_mesh.json",
|
||||
"animation":{
|
||||
"fengxi":"animation/datiangou_animation_fengxi.json",
|
||||
"run": "animation/datiangou_animation_run.json"
|
||||
},
|
||||
"dy_load": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
音频系统(不常用的音频应该声明在dy_load_list中)
|
||||
sound_definitions.json:
|
||||
```json
|
||||
{
|
||||
"dy_load_list":[
|
||||
"sounds/testaudio/test",
|
||||
"sounds/music/game/test/test1",
|
||||
]
|
||||
}
|
||||
```
|
||||
139
mcguide/20-玩法开发/18-性能优化/组件与接口使用优化.md
Normal file
139
mcguide/20-玩法开发/18-性能优化/组件与接口使用优化.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
front: https://nie.res.netease.com/r/pic/20210728/5507b669-4c6f-4958-b5d0-b8556ab4cfb5.png
|
||||
hard: 进阶
|
||||
time: 20分钟
|
||||
---
|
||||
|
||||
# 组件与接口使用优化
|
||||
|
||||
## 前言
|
||||
|
||||
MC的强大在于他的开放性,为开发者提供了大量系统功能和Api做支持,但很多功能如果使用不当,容易出现性能问题。这里会列出一些需要注意的系统功能的使用。
|
||||
|
||||
## 角色特效数量控制
|
||||
|
||||
### 意义
|
||||
|
||||
因为实际游戏中会存在多个角色同时显示在屏幕内的情况,所以如果是绑定在角色身上的特效需要更为注重性能问题,避免往角色身上挂接会发射大量粒子的粒子特效,或者挂接大量序列帧或者文本。
|
||||
|
||||
### 粒子特效使用太多
|
||||
|
||||
- 错误写法:
|
||||
(自定义的翅膀,定义了每秒发射200到300个粒子,太多了,如果同屏有6个玩家,就是1200到1800个粒子,粒子的更新与渲染都有一定性能消耗)
|
||||
```json
|
||||
# 某个翅膀的定义 testWind.json:
|
||||
{
|
||||
"particleeffect": {
|
||||
"emissionrate": {
|
||||
"max": "300.0",
|
||||
"min": "200.0"
|
||||
},
|
||||
...(省略无关代码)
|
||||
}
|
||||
```
|
||||
|
||||
- 正确写法:
|
||||
(以下几个方案可结合使用)
|
||||
```python
|
||||
1)方案1, 严格控制粒子数量在100左右
|
||||
# 某个翅膀的定义 testWind.json:
|
||||
{
|
||||
"particleeffect": {
|
||||
"emissionrate": {
|
||||
"max": "100.0",
|
||||
"min": "80.0"
|
||||
},
|
||||
...(省略无关代码)
|
||||
}
|
||||
|
||||
2)方案2,分档定义多个翅膀,游戏内让玩家根据自己设备配置自行选择
|
||||
# 高配翅膀
|
||||
{
|
||||
"particleeffect": {
|
||||
"emissionrate": {
|
||||
"max": "300.0",
|
||||
"min": "280.0"
|
||||
},
|
||||
...(省略无关代码)
|
||||
}
|
||||
# 中配翅膀
|
||||
{
|
||||
"particleeffect": {
|
||||
"emissionrate": {
|
||||
"max": "200.0",
|
||||
"min": "180.0"
|
||||
},
|
||||
...(省略无关代码)
|
||||
}
|
||||
# 低配翅膀
|
||||
{
|
||||
"particleeffect": {
|
||||
"emissionrate": {
|
||||
"max": "100.0",
|
||||
"min": "80.0"
|
||||
},
|
||||
...(省略无关代码)
|
||||
}
|
||||
3)方案3,提供显示隐藏特效切换按钮,提供显示隐藏其它玩家特效切换按钮,玩家觉得卡顿时可随时自行切换。
|
||||
```
|
||||
|
||||
## 点对点的消息同步
|
||||
|
||||
### 意义
|
||||
|
||||
服务端消息通知客户端能点对点就不要广播,游戏中服务端经常需要下发消息给客户端,如果采用广播的话,全部客户端都会收到,只对某个玩家发的消息不应该用广播。
|
||||
|
||||
例子:游戏中有A,B,C三位玩家,服务端给A发消息:
|
||||
|
||||
- 错误写法:
|
||||
(广播给所有人,其实其它人根本不需要接收这个消息,会浪费这多余的流量与性能)
|
||||
```python
|
||||
# 客户端:
|
||||
class MyClientSystem(ClientSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
ClientSystem.__init__(self, namespace, systemName)
|
||||
# 注册监听
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), 'OnServerSay', self, self.OnServerSay)
|
||||
|
||||
# 监听回调函数
|
||||
def OnServerSay(self, data):
|
||||
if data["playerId"] == clientApi.GetLocalPlayerId():
|
||||
print("server talk to me, content:", data["content"])
|
||||
|
||||
# 服务端
|
||||
class UIDemoServerSystem(ServerSystem):
|
||||
def talkToClient(self, playerId):
|
||||
# 广播方式
|
||||
self.BroadcastToAllClient("OnServerSay", {
|
||||
"playerId": playerId,
|
||||
"content": "hello!"
|
||||
})
|
||||
|
||||
def func(self):
|
||||
self.talkToClient(playerAId)
|
||||
```
|
||||
|
||||
- 正确写法:
|
||||
```python
|
||||
# 客户端:
|
||||
class MyClientSystem(ClientSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
ClientSystem.__init__(self, namespace, systemName)
|
||||
# 注册监听
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), 'OnServerSay', self, self.OnServerSay)
|
||||
|
||||
# 监听回调函数
|
||||
def OnServerSay(self, data):
|
||||
print("server talk to me, content:", data["content"])
|
||||
|
||||
# 服务端
|
||||
class UIDemoServerSystem(ServerSystem):
|
||||
def talkToClient(self, playerId):
|
||||
# 点对点发送的方式
|
||||
self.NotifyToClient(playerId, "OnServerSay", {
|
||||
"content": "hello!"
|
||||
})
|
||||
|
||||
def func(self):
|
||||
self.talkToClient(playerAId)
|
||||
```
|
||||
Reference in New Issue
Block a user