- 《赶山赶海开饭店》优化
- 文件服务器访问华为云Obs问题
- 将摄像机渲染到Sprite上
- 循环特效闪一下问题
- Unity Autoconnect Profiler不起作用
- 小程序云存档线上问题
1. 《赶山赶海开饭店》优化
我们的休闲游戏《赶山赶海开饭店》的小游戏上线,但得到的反馈整体玩起来是比较卡。所以一起参与到这个项目的优化中。
简单分析了之后发现两个很明显的问题:
- 批次太高,饭店场景最高会到400多批
- 很多功能开发方式不对,把所有功能用到的资源全部堆在了场景里面,然后通过代码控制节点的隐藏、显示来做功能,这会导致两个问题,一个是进入场景的时候内存就会到达峰值,另外也会影响场景加载的速度。
(a) 合批
Unity提供两种合批功能,静态合批和动态合批
静态合批
静态合批是以空间换时间,会增加一定的内存。同时静态合批的模型只有放在场景中才起作用,通过动态加载进来的模型是没用的。另外还需要在Player Settings中开启静态合批才能开启Unity静态合批。
动态合批
动态合批需要增加一定的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数据,而我们回退的时候正好是这个情况下,导致所有备份都没有了。
反思:
- 线上回退版本要慎重
- 跟存档相关的发布要再三考虑清楚