弹指间,2023年已接近尾声,这一年UWA社区为游戏行业开发者和期望步入游戏圈的学子们提供了多类型的社区服务,包括技术博客、问答、开源库、学堂、训练营、技术沙龙等。让我们一同回顾这一年来UWA的脚步,希望未来的每一步也都能有大家的陪伴。
UWA问答:帮助游戏行业开发者找到更好的答案。UWA从2016年开始整理发布问答,以技术交流栏目【厚积薄发】系列每周进行推送,精选了开发团队中实际遇到的问题以及解决方案,至今已经发布了367期,累计发布约1800条精选QA,相信帮助了不少开发者们答疑解惑。
UWA学堂:传播游戏行业的体系化的进阶学习内容。UWA学堂作为面向开发者的在线学习平台,目前已经上线272门课程,涵盖了3D引擎渲染、UI、逻辑代码等多个模块,拥有完整的学习体系,一直致力于为广大的开发者提供更丰富、更优质的学习资源。
UWA DAY技术沙龙:开发者技术沙龙。我们带着最新的技术、工具和经验去到了广州、成都、杭州、北京、上海、深圳、厦门七座城市,希望与更多的开发者面对面的交流,让更多开发者受益。
PART 1 / UWA问答
2023年,UWA共推出47期「厚积薄发」专栏,分享了174个问答,这里我们精选了5个热门话题,供大家参考。
Q1:xLua在执行DoString时候死循环,C#端如何处理
我使用xLua作为游戏的内嵌脚本语言,允许玩家自定义脚本。现在有个玩家在脚本里面写了一个死循环,当我用DoString去加载这个脚本时候会直接卡死,于是我在外面加入了一个Task:
var isSucceed = false;
var task = Task.Run(() => {
isSucceed = sandbox.DoString(xxxx)//这里简化一下.核心就是加载这个脚本执行DoString
});
bool isCompletedSuccessfully = task.Wait(TimeSpan.FromMilliseconds(1500)); //1.5s timeout
if (!isCompletedSuccessfully)
{
CELog.LogError("加载脚本超时");
isSucceed = false;
luaEnv.Dispose(); //Kill掉当前的Evn
return; //不再加载后续脚本
}
现在是只要调用‘luaEnv.Dispose()’编辑器就直接闪退,无论是这里Timeout了调用,还是等几秒以后。
我感觉是因为Task的Timeout只是在超时后回调,但是此时Lua还在死锁中,所以我无法Dispose。
官方的FAQ提了一句:
https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/faq.md
调用LuaEnv.Dispose崩溃。
很可能是这个Dispose操作是由Lua驱动执行,相当于在Lua执行的过程中把Lua虚拟机给释放了,改为只由C#执行即可。
感觉和我这个情况是一样的,这种情况我应该怎么处理?
A:在外部结束Lua虚拟机肯定是有很大崩溃风险的,我觉得有两个思路供参考:
- 建立一个完整的沙盒环境,类似Dock,这个环境内运行逻辑,外部可以把这个环境销毁掉,不会对外部造成任何影响,这个在Unity下可能难度比较大,要做逻辑和表现的分离,然后构建一个纯逻辑层的运行环境。
- 在Lua虚拟机内部监测死循环的情况,然后自主中断,抛出Error。这个是我们现在在用的方式,改造Lua虚拟机,监测一定时间内一直在执行某些字节码超过一定阈值就认为自己是在死循环把自己结束掉。当然还有一些别的条件,比如栈深度检测等等。这种自主中断并且抛错的方式其实更加完美。具体实现可以参考这个文章的思路:
检测 Lua 脚本中的死循环 - 链滴
个人是比较推荐第二种方案的,也是很多项目验证过的,对于UGC类型的游戏,检测并能提醒用户编辑出来了死循环非常重要。
Q2:多个小资源包合并为大资源包的疑问
看有文章说可以利用接口LoadFromFile(string path, uint crc, ulong offset);来实现资源包合并为一个打包,且能减少IO操作,不知流程是否是如下理解:
合并前:
资源包A:assets_res_a.bundle
资源包B:assets_res_b.bundle
合并前对资源包的加载方式:
AssetBundle a = AssetBundle.LoadFromFile("assets_res_a.bundle");
AssetBundle b = AssetBundle.LoadFromFile("assets_res_b.bundle");
合并后:
资源包AB:assets_res_ab.bundle
并且记录了a、b在大包(ab)中的offset(A包的offset=0,B包offset=12)
合并后对资源包的加载方式:
AssetBundle a = AssetBundle.LoadFromFile("assets_res_ab.bundle", 0, 0);
AssetBundle b = AssetBundle.LoadFromFile("assets_res_ab.bundle", 0, 12);
从接口上看,合并后并没有减少IO操作,仍是两次对assets_res_ab.bundle的读取,除非引擎内部有做优化,当第一次读assets_res_ab.bundle有IO,在没释放情况下第二次读就不是IO了,求指点迷津。
A:AssetBundle已经是一个VFS了,再在上面多管理一层,从各方面来说不合理,还有可能出现IO句柄持有冲突的问题。
上面所谓的减少IO,不是减少IO本身的读取量,只是少了一个IO句柄,直接把两个原始资源打到一个Bundle内,其实是一样的。那么问题就变成了怎么合理打包Bundle。
Q3:多个小资源包合并为大资源包的疑问
利用Addressable进行热更,采用Disable Catalog Update on StartUp方式,当更新进度没有完成时杀掉游戏,再重新启动游戏,此时catalogs.Count已经等于0,并不会在此触发更新,但其实还有部分资源未更新到,这种情况下应该怎么处理?
1. 首先不能放玩家进入游戏,因为资源不匹配。
2. 触发游戏重新更新,此时catalogs.Count=0,并不会更新资源。
- 这种情况下是否可以使用Addressables.ClearResourceLocators()和 Caching.ClearCache()清理缓存和已经下载好的资源,再次触发热更,这时catalogs.Count 应该是不等于0的吧?
不知道大家都是怎么处理:资源更新一半杀掉游戏,重新登录游戏?
另外,AddressableAssetSetting->Catalog->Player Version Override这个设置使用的是默认的time stamp还是指定了一个版本号?我这里指定了一个固定版本,导致catalogs.Count=1,永远只是更新这次和母包的差异,比如中间已经有几次热更了,会把中间几次的热更给丢弃。针对这个问题,更新策略是怎么样的?
A1:不使用Addressables.UpdateCatalogs(),自己去服务器上下载Catalog文件,如果下载到一半了,取消,不修改版本号。下次进来接着去服务器下载Catalog文件。然后自己下载的Catalog文件可以用Addressables.LoadContentCatalogAsync()加载,然后得到IResourceLocator的所有Key,把所有Key传入下载列表中进行下载资源文件。
A2:进入游戏第一次Check,之后的那些异步操作都用缓存保存起来即可。
A3:第一,你用Disable Catalog Update on StartUp方式启动,就是默认不更新Catalog,但是在走后面更新的时候,Catalog其实已经更新完了,再调用API更新Catalog自然没有了。这时只要去GetDownloadSize然后直接更资源就行了。
Catalog的更新和资源更新是两个阶段。最好分开管理。
第二,AA其实只比较资源的Hash,因此没有版本号概念,所以不存在中间版本,永远是最新版本和母包。这个PlayerVersionOverride指你打Player的那个版本号,一般是更新母包时才递增。这种做法可以保证不管玩家手里是哪份资源,都能更到最新的,并且避免中间的逐版本升级流程。
再有,Keys基本还是需要自己去管理,比如你自己管理了20个Keys,然后调用Addressables.UpdateCatalogs()后告诉你其中5个更新了。那么如果你立即调用GetDownloadSizeAsync和DownloadDependenciesAsync,可以马上传入这5个Key。另外一种情况可能你这一次游戏中UpdateCatalogs获得5个更新。但是要到下一次启动游戏才调用GetDownloadSizeAsync和DownloadDependenciesAsync,那你可以把这20个Keys都传进去。
Q4:大家现在都是怎么实现热更新的
除了C#反射这个老办法、或者Lua,大家还用了什么办法吗?以及有人不用热更新吗?想在立项阶段先做个调研,避免之后填坑,谢谢各位!
A1:Lua缺点是很多,原因很大一部分是因为大家用的是ToLua这种一揽子解决方案。本身Lua就慢,ToLua为了让大家像C++上使用Lua那样快速上手,在底层做了不少事情,包括对UserData的处理(导致了Lua的GC和C#的GC冲突)和对LuaState的OOP化。对C#的无脑Wrap和对Lua的OOP改造也进一步加剧了这些问题。更何况,Lua和C#之间并没有大家想象中那么契合。一个最简单的例子,任何Lua传给C#的String都是新的GC Alloc。然而,对于热更新方案,我们并没有太多的选项,Lua算是一种比较成熟、性能上勉强能用的方案,最重要的是,用的人要真的会(不仅仅是会语法),那么性能上才不会有大问题。当然,还是要期待大牛研发出更优雅和优秀的方案。
A2:ILRuntime本质上就是为了iOS热更(代码)而开发的,Android热更可以用ILRuntime,也可以完全不用。
说回正题,一般热更新分两块:代码+资源。资源热更新几乎都是通过AssetBundle来实现,代码热更新可以用某种解释器+解释执行的语言来实现,可供选择的有Lua、AS3、Python、JS、C#等,据我所知,主流是Lua,次主流是C#.
热更新的选择上,我们项目最初所有代码都是C#写,不考虑热更新是因为团队没一个人会Unity 3D,全是边学边做,"做出来"是第一要务。上线后自然顶不住运营的压力,在中国很多Android渠道的情况下,运营的这个需求是合理的,否则每次版本更新,各个渠道审核-上线的时间同步是非常困难的。
然后我们首先用xLua打补丁修Bug,这样成本最低,后来运营要求能通过热更新加功能,而不仅仅是改Bug,那么要么用Lua写所有可能会被热更新的代码,这需要把现有的大量C#代码翻译过来;要么想办法让C#能热更新。很自然就选择了后者+ILRuntime的方法。
最后的技术方案是这样:
1)基本限制
热更新部分的代码都不继承MonoBehaviour,也就是都不挂脚本,非热更新部分随意,热更新对MonoBehaviour这种比较特殊的东西的支持都挺麻烦,要么不用;要么只是做个不可热更新的消息转发层;要么开发时挂脚本,打包时用某种特殊的方式把它变成代码里动态AddComponent。
2)Android
不用任何第三方的热更新方案,用C#反射执行DLL,性能和代码写法和纯C#基本一样。
19年9月Google Play强制要求APP都支持64位,Unity的应对方案是Android IL2CPP,暂时没有支持Mono Backend 64位的打算,而且Unity IL2CPP实现的真不怎么样。所以到时候只能是IL2CPP + ILRuntime的方式,性能差的一大截,主要慢在ILRuntime上。
3)iOS
ILRuntime + DLL解释执行,当然是在IL2CPP下。
4)优点
语言(C#)开发环境工具链统一,随时可以变成不支持热更新形式,如果苹果未来不允许任何解释执行的方式。
框架搭好后,满足一些限制条件(非硬性限制,主要是避免麻烦,限制主要是1个,可热更部分的代码不要继承不可热更的代码,不继承MonoBehaviour是这个限制的子集),写逻辑的同学开发方式和原生C#开发完全一样,包括调试。
第三方插件直接可用(大部分插件都是基于C#写的)。
5)缺点
稳定性的坑还是有一些,通常发生于一些相对高级的语言特性组合,特别是各种反射代码。另外.net4.6的asyncwait支持现在的版本应该也还不够稳定。
纯计算的性能弱于Lua,计算密集型的代码还是想办法放在不可热更新的部分吧。
历史短,Git贡献者少,项目考验少(据我的了解,上线的商业项目在x-1x之间,具体的项目有MMO,有SLG,有休闲,也有棋牌),原理上大的优化空间没有,小的优化空间还是有一些;另外整合了各种常用Feature的框架也少,虽然我感觉这块基本都会自己写。
A3:当前以及未来主流热更方案:HybridCLR
UWA学堂 | Unity和Unreal游戏引擎的从业者学习交流平台
Q5:获得AssetBundle内部依赖关系的方法
之前发现从Manifest里面获得的依赖关系在很多情况下都不准确。
比较明显的是,当Prefab嵌套时,Manifest数据会显示Prefab依赖被嵌套的Prefab;而SpriteAtlas和Sprite依赖关系是反的。其他还有很多情况会额外依赖不需要的资源。用WebExtract解开AssetBundle本身,里面的数据是正确的依赖数据。
因此,现在想找到一个方法,脱离Manifest,自行建立依赖关系表。
现在的问题在于,如果对每个输出的AssetBundle执行解包和正则分析,会花费大量时间,并且本身文本匹配也有风险。
有没有什么干净的流程,可以获得AssetBundle内部记录的依赖关系?
A:用UnityEditor.Build.Content.ContentBuildInterface下面的方法获取到的依赖信息是准确的。如下面两个函数:
ContentBuildInterface.GetPlayerDependenciesForObjects
ContentBuildInterface.CalculatePlayerDependenciesForScene
如果用SBP,Manifest可以在编译完成后加个Task读m_DependencyData,自己建立一个。或者直接用ContentBuildInterface预构造的也可以满足了。
更多内容欢迎登录 UWA问答:answer.uwa4d.com 进行查看。
PART 2 / UWA学堂
2023年,UWA学堂上新了22门课程,目前总计有272门课程在售。这里我们精选了本年度热销课程,供大家参考。
本年度热销课程付费榜:
《【ET 8.0版本】ET框架 - C#全栈式网络游戏开发框架》
本课程主要介绍了在GitHub上有着极高人气的基于.Net的跨平台、开源、高性能、全栈式开发与多进程多线程分布式部署的网络游戏开发框架 - ET框架8.0版本的入门使用与原理学习。
ET框架经过长期的实践和更新,升级至最新8.0版本,拥有了更加强大和灵活的架构,由于ET框架8.0版本进行了一系列重大的升级,架构得到了全新的迭代。大多数初学者与客户端开发人员会在初次接触ET框架时,会因为缺乏网络编程通识和其自身固有的面向对象开发习惯,从而被劝退或者编写出极其”不规范“的ET框架业务代码。并且由于ET框架8.0版本引入的多线程纤程的机制,进一步加深了ET框架的理解和使用的门槛。
所以本门课程重点围绕ET框架8.0的全新特性,对其原理与设计思路展开讲解,并通过小案例的形式,帮助读者梳理ET框架的设计思路与开发思想,从而进一步学会和掌握ET框架的基本使用,并带领读者掌握和熟悉使用ET框架8.0版本进行游戏前后端全栈式开发的基本流程。
《【E世界】MMORPG全栈式网络游戏开发之旅-战斗篇》
本课程的主要学习内容是如何使用ET框架(在Github上有着极高人气的基于.Net的跨平台,开源,高性能,全栈式开发与多进程分布式部署的网络游戏开发框架)进行MMORPG网络游戏的战斗系统开发。
本课程将会从实际的MMORPG类型的战斗系统开发着手,重点讲解如何正确的使用ET框架进行大型MMORPG的战斗系统开发。在课程当中,我们将会触及MMORPG网络游戏战斗系统开发的方方面面。首先我们将会带着同学们手把手地实现一整套的MMORPG战斗技能管线系统,并且在此基础上,通过抽象和组合的方式,以最为通用的设计来实现各种MMORPG网络游戏中的战斗与技能系统。
《DOTS-ECS系列课程》
本系列课程共分为四个章节,笔者希望能以一种愉快的方式带着大家去学习DOTS,拥有更好地使用DOTS-ECS的能力,了解DOTS中的各种“坑”以及各种解决问题的技巧。
第一章基础内容篇,以深浅适度的独到方法使大家快速学会学习DOTS所必须的基础知识。
第二章系统设计篇,以一个项目中最必须的关键系统(战斗环境重置系统)为实战用例,带领大家初步尝试DOTS的使用方式。
第三章资源系统篇,再次以一个项目中必须准备的大型方案(GameObject与ECS混用方案)为实战用例,手把手带领大家从会用DOTS到掌握DOTS。
第四章是不定期更新的支持章节,主要会更新一些难以归类,项目中又必须涉及的琐碎知识。目前更新了BlobAsset相关内容(提供ECS端使用配置的方法)。
本年度热销课程免费榜:
《Unity开发实战经验分享》
本课程主要记录了笔者项目初期碰到的并值得一说的实际业务问题,涵盖了比较多客户端框架设计的内容,以及有效提升开发者编程体验的内容。
主要包括以下内容以及一些其他配套的小工具。
- Luban配表工具的使用介绍
- ECS设计下的加载管理
- 设计项目资源规范化
- 设计本地化组件
- C# Task使用指南
- 设计技能系统
- Unity Android多渠道管理
《Unity实现水墨山水画风格》
本文介绍了一些3D渲染技术的组合思路,并用来对中国传统绘画中的宋代水墨山水画风格进行模仿实现。文章思路同样可以用于油画、水彩、素描以及漫画等其他美术风格方向的渲染。
《OpenGL教程-中文版》
本文是LearnOpenGL教程的中文翻译,英文版的地址为:https://learnopengl.com
学习(和使用)现代OpenGL需要用户对图形编程以及OpenGL的幕后运作有非常好的理解才能在编程中有很好的发挥。所以,我们会首先讨论核心的图形学概念,OpenGL怎样将像素绘制到屏幕上,以及如何利用黑科技做出一些很酷的效果。
除了核心概念之外,我们还会讨论许多有用的技巧,它们都可以用在你的程序中,比如说在场景中移动,做出漂亮的光照,加载建模软件导出的自定义模型,做一些很酷的后期处理技巧等。最后,我们也将会使用我们已学的知识从头开始做一个小游戏,让你真正体验一把图形编程的魅力。
前往UWA学堂:edu.uwa4d.com 搜索即可查看对应课程。
PART 3 / UWA DAY技术沙龙
UWA通过与国内大量游戏团队进行深入的合作,就如何快速搭建性能保障体系和团队内部的监控流程做了大量的实践与尝试,分享了游戏团队在日常游戏研发中应该关注哪些重要的性能质量参数、如何对这些参数进行高效获取和定位问题、如何在团队内部进行及时的反馈、沟通和问题解决等重要事项,从而让游戏团队快速搭建起科学的质量保障体系,提升项目研发的效率和质量。
我们从大量研发团队的日常使用场景出发,分享日常研发中需要注意和掌握的性能和质量保障的关键环节,以及各大游戏团队在使用UWA Pipeline过程中的最佳实践,并对UWA Pipeline流水线进行了现场展示,和开发者们互动交流。
在成都、杭州、北京三站,设立了项目分析1对1,面对面帮助开发团队解决项目性能瓶颈。
同时,还邀请到了多位行业大咖在现场分享业内的资讯。
这一年,我们在不同的城市与各位开发者们交流探索,一步步走来,既是成长也是沉淀。我们希望能有机会可以走到更多开发者的面前,更加深入地了解项目的性能问题,更好地助力游戏开发团队打造完整的工具链。
借此机会,小编预祝各位新年快乐,2024年我们继续一同乘风破浪!