找回密码
 立即注册
查看: 1191|回复: 20

工作上Unity遇到最大的坑是?

[复制链接]
发表于 2020-11-27 21:06 | 显示全部楼层 |阅读模式
不是想吐槽
发表于 2020-11-27 21:12 | 显示全部楼层
目前我们遇到的最大问题是资源部分
    导入的原始资源管理打包时的资源管理


第二点前面也有答主提到了。目前Unity里两条路,Resources的话官方本来就不推荐使用[1]: Best Practices for the Resources System——Don't use it.
AssetBundle这块也有不少的坑。目前的API隐藏了大量的细节,导致在控制粒度上比较粗[2];在控制包体个数和大小上各个项目需要自己去平衡;做不到一个版本仓库稳定打出精确一致的AB(别说不同机器,同一台机器有时候都会有细微差别...);打包速度也比较捉急;增量打包完全是靠manifest,有时候甚至会出错。
ps. 至于内存管理、异步加载API不好用这种我们自己写代码能解决的我就不算在上面了……


第一点是在我们项目不断推进中遇到的问题:Unity的资源管理是直接导入原始资源(fbx/tga/甚至有时候美术会给你塞一些惊喜譬如psd进来...),但是如果需要扣一些细节的时候就非常捉急了...
    FBX里的Mesh/Animation Clip无法修改,如果要做一些优化就令人头大[3]Main Asset + Sub Asset的概念导致没法更细的控制加载和释放每台机器遇到新资源都会走ImportAsset,这个有小概率遇到冲突(在使用Spine/Live2d这种插件的时候遇到过几次)


目前我们正在参考UE4,剥离原始资源(只在美术机器上留一份),在项目里使用生成的对应asset或其他Unity内置的格式。这个比分离美术/程序两个工程然后AB来传递的好处就是方便一些修改和使用。
ps. 评论区补充的资源检查我们目前是分为两部分,一个是导入的时候自动检查(譬如关掉Import Material之类的);另一个是用工具定时扫描自定义规则。


Reference
[1] Unity - The Resources folder
[2] Unity 5.x AssetBundle零冗余解决方案
[3] Unity动画文件优化探究 - UWA Blog
发表于 2020-11-27 21:17 | 显示全部楼层
最大的坑应该是暗坑,就是在大部分情况下正常,在小部分情况下不正常,一旦掉进去就很难爬出来的那种。
从业这几年,机缘巧合,还真遇到过几件,就拿记忆比较深刻的来说吧
被大V点赞了,我觉得应该认真严肃一点,去修改一下错别字
IL2CPP
想听故事的可以接着看,想看问题和结果的可以直接跳到最后
故事发生在16年的冬天,那天晚上我们准备提交iOS审核版本。
晚上10点的时候差不多测试妥当了(我们是先在编辑器上测试,然后在android上测试)。我们准备在iOS上跑一下功能就提审。
等第一个iOS版本出来之后,我们发现没办法登陆服务器了,下面就开始排查问题,前端后端一起上,最后定位到是玩家上线时的初始化消息包没有解开。那就从网络通讯开始查起吧。
这里要简单介绍一下我们用的通讯协议,这是个在protobuff基础上修改的二进制协议,前两位是包长,接着两位是消息id,后面是压缩标记,最后面是消息体。在unity这一端,我们用工具生成每个消息对应的消息类,包括解码,生成unity能用的c#结构等等。解码的过程可以认为是反序列化的过程。
首先从后端抓数据包,用工具解码,没有任何问题,那就是前端的问题了。虽然问题范围缩小了,同时问题也变得棘手了。编辑器和android上都没有问题,这些是比较好调试的平台,但是在iOS上调试就变得相当困难了。
iOS上为了支持64位的设备必须把代码从C#转换为cpp,这个过程就是IL2CPP。unity自带的有工具,但是生成的cpp代码简直不是给人看的,在这些代码上单步调试是个巨恶心的过程。还有我们的打包机器也不太给力,一台低配的二手mac mini,编译一次unity项目工程是相当的酸爽,这也是我们最后才测试iOS版本的原因,严重影响工作效率。(好在我们现在已经更换成了mac pro垃圾桶,快的让人不太习惯)。
我们先用最土最笨的方式进行排查,就是插桩打印log,然后从“海量“的日志中去分析是哪条消息包出的问题,最后定位出来是最近新添加的消息。找到了问题下面就是怎么办的问题了,当时已经凌晨两点了,小伙伴们又饿又困,就想耍小聪明,有人提议既然不是逻辑上有问题,那就先调整一下消息的编号,将出问题的消息的编号往前放一放。服务端迅速的做了调整,我们测试了一下,天呀,能进游戏了,还没来得及高兴,就发现有功能用不了,用不了的功能就是刚才交换消息号的功能。也就是几个消息号排在最后的消息对应的功能。难道是消息号太大的问题?当时我们已经有1000多条消息了,编号已经排到了3000多。难道要收缩消息编号,排列的更紧密一些?这显然是个馊主意,暂且不说问题是不是消息号太大,就算排列的再紧密随着我们后续功能的开发,总会到达3000多号的,这不是正解。直觉告诉我,问题应该出在那些生成的cpp代码上,于是我就开始去排查和单步相关的代码。
单步是个相当痛苦的过程,这里暂且不表,反正最后我定位到了一段神奇的代码里
在c#这边它是这样的,一段根据消息id找解码器的逻辑代码
public static Decoder GetDecoder(uint msgid)
{
        Decoder _decoder = null;
        switch (msgid)
        {
            case 1:
                _decoder = Msg_1.decoder;
                break;
            case 2:
                _decoder = Msg_2.decoder;
                break;
            ............
            case 10086:
                _decoder = Msg_10086.decoder;
                break;
            ............
        }
        return _decoder;
}
对应到IL2CPP那里,它是这样的
V_0 = (Decoder_t_1323225970_0 *)NULL;
uint32_t L_0 = ___msgid;
V_1 = L_0;
uint32_t L_1 = V_1;
if (((int32_t)((int32_t)L_1-(int32_t)3)) == 0)
{
        goto IL_01ff;
}
if (((int32_t)((int32_t)L_1-(int32_t)3)) == 1)
{
        goto IL_00c0;
}
if (((int32_t)((int32_t)L_1-(int32_t)3)) == 2)
{
        goto IL_0211;
}

if (((int32_t)((int32_t)L_1-(int32_t)3)) == 4)
{
        goto IL_abcd;
}


...
IL_01ff:
{
        IntPtr_t L_23 = { (void*)Msg_1_decoder_m_428528862_0_MethodInfo_var };
        Decoder_t_1323225970_0 * L_24 = (Decoder_t_1323225970_0 *)il2cpp_codegen_object_new (Decoder_t_1323225970_0_il2cpp_TypeInfo_var);
        Decoder__ctor_m2046815770_0(L_24, NULL, L_23, /*hidden argument*/NULL);
        V_0 = L_24;
        goto IL_bcef;       
}

IL_00c0:
{
        IntPtr_t L_25 = { (void*)Msg_2_decoder_m_428528862_0_MethodInfo_var };
        Decoder_t_1323225970_0 * L_26 = (Decoder_t_1323225970_0 *)il2cpp_codegen_object_new (Decoder_t_1323225970_0_il2cpp_TypeInfo_var);
        Decoder__ctor_m2046815770_0(L_26, NULL, L_25, /*hidden argument*/NULL);
        V_0 = L_26;
        goto IL_bcef;       
}

.....
上边那段c#代码switch里有1000多个分支,对应的就是我们的每一条消息。
当我在cpp代码里单步到出问题的消息id时,发现它没办法跳转到正确的逻辑分支里,我忍着眼疼看了这个上万行的函数,发现出问题的消息id直接跳转到默认的分支里去了,就相当于去直接执行了switch里的default分支。至今这个问题我也一直没时间去找原因,也没给unity官方提bug,因为可能只有我们才会傻到在一个switch里写1000多个分支加逻辑,而且后来我用简单的switch加好几万个分支也没办法复现,只有我们的这段用工具生成的消息解码函数才会有问题。
这时已经是凌晨6点多了,我没时间去深究问题,立即去改消息解码生成工具,把这个恶心的switch改掉。然后问题解决了,天亮了。
发表于 2020-11-27 21:26 | 显示全部楼层
最新版本的Resources终于改的不那么坑了……原来Resources资源加载真的是人神共愤……
资源加载是个很大的坑,尤其是异步流式加载。流式加载影响主线程性能一般有两个原因:依赖关系,实例化。解决依赖关系的办法是,最好加载时使用“纯资源”,比如贴图,二进制数据等。而解决实例化问题,就需要涉及到项目框架了,譬如场景内有一个水杯,有一个事件是主角过去喝水,那么要异步加载这个杯子,就应该异步加载场景(模型贴图材质),然后异步加载脚本数据,没错是数据,比如杯子的容量,样式等等,最后再统一进行逻辑控制。这样的办法有些反面向对象,但是传统面向对象的实例化方法对于加载是非常不友好的。
Unity的Multi Scene Edit是很容易出现这类问题的,因为Unity的Scene与UE的Level Streaming不同,其容忍度极高,各种花里胡哨的资源都可以聚集在Scene中,因此依赖关系与实例化的消耗很容易不被注意,这一点应靠团队管理(比如主程)抓起,对于需要加载的资源,保证数据纯粹性,保证实例化可控性,最后再依靠一些编辑器工具完成数据在开发阶段的序列化等。
发表于 2020-11-27 21:35 | 显示全部楼层
初学者,遇到过印象最深的坑就是UI要动静分离吧…曾经年少不懂事,把动态ui和所有静态ui放在同一个canvas下面,游戏跑着跑着越来越卡,还不知道为啥…
发表于 2020-11-27 21:41 | 显示全部楼层
资源管理。
讲道理这个坑你问了也只能吐槽……
因为无论别人怎么跟你讲,
如果这个坑可以绕过或者直接填掉的话,那还会被称为多数人的大坑吗……


话说你们自己玩的真不试一下godot吗?开源哦……
发表于 2020-11-27 21:44 | 显示全部楼层
Vector3.toString打印出来的xyz大小全是四舍五入到一位小数的,一开始Debug的时候还以为逻辑写错了。。。1.15之类的全进成1.2
发表于 2020-11-27 21:46 | 显示全部楼层
谢邀
Unity的最大的坑莫过于AssetBundle,各种内存泄露、各种资源冗余、各种异步加载坑爹。打包的时候需要买个垃圾桶、不然内存保证不够用……增量无法删除老资源。
反正市面上unity的 人员就没有不抱怨AssetBundle的。
而最大的坑就是思路贼多,公说公有理,婆说婆有理,难以统一江湖,很容易导致程序员撕逼,并且其中隐含的套路各式各样,有用WWW的,有用LoadFromFile,甚至自己写的。
编辑器下,有用Resources.Load的,有让人平时直接用AB的,Unity还给了他制定Simulate套路。
总之如同 1000个读者1000个汉姆雷特一样,而我们程序比较喜欢用一个原则概况比较多的东西,这个东西不用问绝对是噩梦。一个项目中可以有N多写UI的程序员,但是 写资源管理的只有一个,原因写过的人都懂。
资源释放的套路:
拿纹理来举例子,按照程序员的思路,这张纹理只要被一个组件或者材质球用到了一次,那么引用计数加1,而被销毁的时候引用计数-1,当引用计数为0的时候立刻释放。这似乎是个很美好的东西。利用Unity怎么做?只能用Unload(true),很合理,不过就有很多问题,你必须 封装所有的Clone 接口,每从prefab 的源GameObject clone一次引用计数必须加1,其次要绑个组件来监听这个clone出来的prefab是否被销毁。似乎很合理,做起来缺有非常多的隐患。比如 一个新人进来不知道你 用的Unload true方式,很随意的用GameObject.Clone 来复制prefab他能很好的完成功能交给项目验收,测试也基本发现不了,我敢保证就是项目上线正常运营,这种东西仍然有,强迫症主义者,很难受。
奔放一点,利用Resource机制来回收资源...使用Unload false,这个太依赖你的AB包打包方式了,一定不要出现说一个资源引用了同一张图片,如果是那就冗余一下。不然低版本依赖收集然后Unload false,你的图片纹理就野指针了,当然你可以自己去管理这些图片,走单独的读取机制,然后底层上赋值给材质球。
不过我敢保证,绝对还有第三种套路。
给tag起名字的套路:有根据场景的,有根据prefab的,有根据文件路径的,有一个文件单独打一个包的。
AssetBundle存放位置的套路:有放在 SteamAsset下首包解压到用户目录的,5.3之后有API可以直接读取SteamAsset下的,不过很多项目是老的,放的位置也是各式各样了,有Android平台下,利用Android的资源打包方式,IOS分IOS的,也有WWW直接下的。
总之如果讨论AB的话,我保证两个写AB的人能撕逼好几天,然后谁都无法彻底说服谁。
热更新的时候,有下载碎文件的,有拼接成一个文件,利用二进制diff的。
哎~~~AB还是只拿来存储 Assets(图片、声音、Mesh、动画文件之类的吧),剩下的程序员自己构造吧。为什么 Resources的依赖收集就不错,AB就写成这个鸟样子。
好吧,最后说下,我们可以找到非常优秀的,Lua插件、动画插件、UI插件总之各种插件,但是AssetBunle发展这么多年,非常多项目几乎都用过了,有谁能把自家插件搞成一个很成功的产品呢?

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2020-11-27 21:47 | 显示全部楼层
大概就是不专业的队友了吧……


基本上国内最早一批做unity的, 其实你按照他的设计理念去做项目是没啥坑的。


关键是后来unity火了以后, 一帮做flash的 一帮做页游的, 一帮做功能机小游戏的都跳了进来, 然后把他们的习惯带进来按照做flash啊 页游啊的思路做,当然觉得都是坑了……
后来培训班的也来了……


unity要说坑的话, 早期的OnGUI 是一个, 写着方便但是运行效率太低, 也不适合做动态UI效果。
不过UI这个基本上是整个软件业的大坑 so…


除此以外就是有些资源需要手动回收。 如果你做当年(08 09)那个年代的中低层游戏基本上是不用管的。 特殊效果才涉及这些。 早期unity其实只是个低端引擎, 不过好就好在unity扩展性强。


基本上你遇到的所有坑, 都可以通过写插件来解决。 除非你想做超过当前主流的效果和功能。不过这也是游戏引擎本身就不会提供的。 引擎提供的都是初始功能。
发表于 2020-11-27 21:49 | 显示全部楼层
可能会遇到比较多的:
    资源管理和打包
这部分大家说的比较多,不赘述了。


2. 编辑器的设计
在做一些小项目时这些可视化操作可能还蛮方便的。比如脚本中暴露字段在Inspector面板上,拖拽一下就能方便的指定引用,但是大型项目中这就是无底深坑。
如果一个项目比较复杂,一个界面上有几十个需要拖拽的字段,一不小心鼠标滑了那就悲剧了,慢慢找吧。
Unity中没办法看到这些拖拽的操作的记录,最好的办法还是在C#代码中来指定这些引用。
同理获取物体组件时也不能依赖物体本身的父子关系,万一有人动了节点顺序那又是一个灾难。


3. 脚本的初始化顺序
如果多个系统都把初始化写在Awake和Start里,你是不知道这些脚本启动的先后顺序的。这个时候如果脚本之间有依赖关系就会出问题。虽然Unity提供了Script Execution Order来解决:

但有时候并不确定初始化函数的运行时间,仍然要用异步或其他方式来处理。
看了下其他答主的回答问题大多集中在资源管理上,在大型项目上这的确是一个难以规避的深坑。特别是在Unity对相关API的技术细节隐藏较深的情况下。
至于其他一些做小型项目会遇到的坑,大部分都能想一些别出心裁的方式绕过去,比如无法自定义物体的轴心(Pivot),套上一个父节点调整之类的。
Unity近两年一直保持一个较高的更新频率,不断有新功能或是组件加入到新版本中。在使用过程中肯定会出现其他各种各样的细节问题,因此若是开发大型项目时还是使用一个相对稳定的版本比较靠谱。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-24 09:35 , Processed in 0.113155 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表