58. 2023-04-30周总结

  1. 《赶山赶海开饭店》优化
  2. 文件服务器访问华为云Obs问题
  3. 将摄像机渲染到Sprite上
  4. 循环特效闪一下问题
  5. Unity Autoconnect Profiler不起作用
  6. 小程序云存档线上问题

1. 《赶山赶海开饭店》优化

我们的休闲游戏《赶山赶海开饭店》的小游戏上线,但得到的反馈整体玩起来是比较卡。所以一起参与到这个项目的优化中。

简单分析了之后发现两个很明显的问题:

  1. 批次太高,饭店场景最高会到400多批
  2. 很多功能开发方式不对,把所有功能用到的资源全部堆在了场景里面,然后通过代码控制节点的隐藏、显示来做功能,这会导致两个问题,一个是进入场景的时候内存就会到达峰值,另外也会影响场景加载的速度。

(a) 合批

Unity提供两种合批功能,静态合批和动态合批

  1. 静态合批
    静态合批是以空间换时间,会增加一定的内存。同时静态合批的模型只有放在场景中才起作用,通过动态加载进来的模型是没用的。另外还需要在Player Settings中开启静态合批才能开启Unity静态合批。

  2. 动态合批
    动态合批需要增加一定的CPU负担,同时也会有一定的限制,只能适用于小模型。这个功能也需要再Player Settings中开启才能使用。

另外还有一个独立于Unity之外的合批方式,就是让美术合并模型。以这种方式可以极大的降低批次,例如我们的场景模型通过这种方式从100+批次降低到了1次。唯一的问题就是合并成大模型之后舍弃了小模型各种摆放的灵活性。同时如果相同的小模型存在不同的大模型中就会增加一定的内存。所以如何合并需要根据项目进行取舍。

我们合批的优化方向是能尽量美术合并的先美术合并,然后再利用Unity静态合批功能对场景中的静态物体进行合批,最后再考虑动态合批(能不开的话尽量不开)

(b) 功能资源使用方式修改

让对应同学把资源从场景里面删除,根据数据以动态方式加载进来显示,同时利用对象池对一些资源进行复用,减少内存同时增加效率。

这两个优化做完之后,接下去需要再根据Unity Profiler的反馈进行CPU或者GPU优化了,不过后续的卡点大概率在CPU上,需要具体问题再具体分析。

2. 文件服务器访问华为云Obs问题

华为云提供了Obs访问的.Net SDK,接入之后发现会有很久没有返回的情况,卡死所有线程,导致文件服务器所有请求都不能处理,同时随着请求上来,机器负载和内存会不断上涨,最终被操作系统OOM了。

翻了下SDK代码没找到对应的设置超时的地方,同时这个SDK写的也比较烂,不排除其他Bug的可能。同时我们的文件服务器也只用到了读文件,写文件的接口,于是参考华为云的验证方式,通过自己的底层封装的Http发送请求到Obs。

目前跑了几天下来,看到偶尔也会出现超时的请求,但是超时之后会被底层中断,抛出异常,不会卡住线程。所以整体还是可以稳定运行。

部分源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
async Task<bool> PutObjectAsync(string bucketName, string path, Stream contentStream)
{
var date = DateTime.UtcNow.ToString(new CultureInfo("en-US", false).DateTimeFormat.RFC1123Pattern, CultureInfo.GetCultureInfo("en-US"));
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, $"https://{bucketName}.{_endpoint}/{path}");
request.Headers.TryAddWithoutValidation("Date", date);
request.Headers.TryAddWithoutValidation("Authorization", GetAuthorization("PUT", date, "application/octet-stream", $"{bucketName}/{path}"));
request.Content = new StreamContent(contentStream);
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
IHttpResult result = await HttpUtils.Request(request);
return result.IsSuccess;
}

async Task<Stream> GetObjectAsync(string bucketName, string path)
{
var date = DateTime.UtcNow.ToString(new CultureInfo("en-US", false).DateTimeFormat.RFC1123Pattern, CultureInfo.GetCultureInfo("en-US"));
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"https://{bucketName}.{_endpoint}/{path}");
request.Headers.TryAddWithoutValidation("Date", date);
request.Headers.TryAddWithoutValidation("Authorization", GetAuthorization("GET", date, null, $"{bucketName}/{path}"));

IHttpResult result = await HttpUtils.Request(request);
return await result.ReadAsStreamAsync();
}

async Task<bool> HasObjectAsync(string bucketName, string path)
{
var date = DateTime.UtcNow.ToString(new CultureInfo("en-US", false).DateTimeFormat.RFC1123Pattern, CultureInfo.GetCultureInfo("en-US"));
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Head, $"https://{bucketName}.{_endpoint}/{path}");
request.Headers.TryAddWithoutValidation("Date", date);
request.Headers.TryAddWithoutValidation("Authorization", GetAuthorization("HEAD", date, null, $"{bucketName}/{path}"));

IHttpResult result = await HttpUtils.Request(request);
return result.StatusCode != 404;
}

private string GetAuthorization(string method, string date, string contentType, string resourcePath)
{
return $"OBS {_ak}:{GetSign(method, date, contentType, resourcePath)}";
}

private string GetSign(string method, string date, string contentType, string resourcePath)
{
string signString;
if (contentType == null)
{
signString = $"{method}\n\n\n{date}\n/{resourcePath}";
}
else
{
signString = $"{method}\n\n{contentType}\n{date}\n/{resourcePath}";
}

return Convert.ToBase64String(_hmacsha1.ComputeHash(StringUtils.StringToBytes(signString)));
}

其中_hmacsha1通过_hmacsha1 = new HMACSHA1(StringUtils.StringToBytes(_sk));进行创建。

发起Http请求则直接使用HttpClient.SendAsync进行发送即可,我们底层为了Http底层实现方式对上层透明,对Http进行了二次封装,方便在Unity版中使用。

3. 将摄像机渲染到Sprite上

3D项目中,场景是使用Unity Sprite搭建的。现在有个需求是要摆放一个3D人物在这个场景上走。为了更好控制这个3D人物的位置,想到的一种方式是有个摄像机来渲染这个3D人物到RenderTexture上。但是Sprite不像UGUI提供一个RawImage来直接渲染RenderTexture。需要自己通过RenderTexture去生成一个Texture2D,然后再利用Texture2D生成一个Sprite。网上找了两种方式:

1
2
3
4
5
6
7
8
9
Texture2D toTexture2D(RenderTexture rTex)
{
Texture2D tex = new Texture2D(512, 512, TextureFormat.RGB24, false);
// ReadPixels looks at the active RenderTexture.
RenderTexture.active = rTex;
tex.ReadPixels(new Rect(0, 0, rTex.width, rTex.height), 0, 0);
tex.Apply();
return tex;
}
1
2
3
4
5
6
7
Texture2D toTexture2D(RenderTexture rTex)
{
Texture2D tex = new Texture2D(512, 512, TextureFormat.RGB24, false);
// ReadPixels looks at the active RenderTexture.
Graphics.CopyTexture(rTex, tex);
return tex;
}

实测第二种方式性能会更好点。同时为了避免每次在Update里面创建Texture2D和Sprite,需要在Awake或者Start的时候创建好Texture2D和Sprite,Update只需要调用Graphics.CopyTexture(rTex, tex);进行复制即可。

另外Graphics.CopyTexture(rTex, tex);的时候我们遇到一个报错Graphics.CopyTexture called with mismatching mip counts (src 1 dst 10)。原因是创建的Texture2D开启了Mipmap,创建的时候mipmap参数传false即可。

4. 循环特效闪一下问题

特效做出来的循环特效,会出现突然闪一下的问题:

特效也不知道问题出现在哪,了解了下特效的实现方式,通过设置starttime,duration,looping这三个参数来实现特效循环

另外在粒子生命周期内,通过设置Color属性来设置透明度变动效果。

经过分析发现,亮一下的原因是因为一瞬间存在了两个粒子,颜色叠加导致变亮。没找到为啥会出现这种情况,调了很多参数都不能解决,猜测是Unity自己精度问题导致的,如果是这个问题的话就解决不了了。

想了另外一个补丁方案,另外挂一个脚本,利用dotween做动画,然后在Update里面通过下面的方式去设置粒子的颜色和大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void Update()
{
ChangePSColor();
}

void ChangePSColor()
{
_testPartical.GetParticles(pSparticles);
if (pSparticles.Length > 0)
{
for (int a = 0; a < pSparticles.Length; a++)
{
if (ShowColorDataList.Count != 0)
{
pSparticles[a].startColor = _showColor;
}
if (ShowScaleDataList.Count != 0)
{
pSparticles[a].startSize = _showScale;
}

}
_testPartical.SetParticles(pSparticles, pSparticles.Length);
}
}

5. Unity Autoconnect Profiler不起作用

打出来的webgl包死活连不上Unity Profiler,之前还试过可以,最后把整个Unity产生的文件夹(Library,Temp,Logs)删除,重新打开Unity才解决

6. 小程序云存档线上问题

发布了一个小程序版本,主要是做了一个云存档功能,但是上线之后出现卡loading进不去的情况,运营那边一看出现这个情况直接回退版本了,但是我们代码是不兼容回退版本的,导致线上玩家存档直接丢失了。之前考虑过存档丢失的情况,把本地存档备份了下,但是发现一个历史原因的大坑,某些情况下在启动的时候会去删除所有PlayerPrefs数据,而我们回退的时候正好是这个情况下,导致所有备份都没有了。

反思:

  • 线上回退版本要慎重
  • 跟存档相关的发布要再三考虑清楚