1 确认Unity版本是否为最新版本。旧版本可能会出现此问题。
2 确认您的Unity账户与打开的Unity项目链接是否出现问题。尝试在Unity编辑器中登录您的Unity账户,然后重新启动Unity项目。
3 确认您的网络连接是否正常。无法连接到Unity服务器可能会导致此问题。
4 尝试在Unity编辑器中手动获得授权代码。在Unity编辑器中,选择“帮助”>“检查更新”,然后单击“获得授权代码”。按照提示进行 *** 作。
5 如果以上步骤仍然无法解决问题,可以尝试从Unity官方网站下载所需的包,并手动导入到Unity项目中。
如果仍然不能解决问题,你可以联系Unity的技术支持获取帮助。这是涵盖Assets,Resources和资源管理的一列文章中的第二篇。
这篇文章涵盖了Unity序列化系统的内部原理和Unity是如何在不同的Objects间保持健壮的引用的,包括Unity编辑器与运行时。也讨论了Objects和Assets在技术上的区别。这里涉及的主题对理解如何高效地加载和卸载Unity资源起到很重要的作用。合适的资源管理的关键是hi保持短的加载时间和较低的内存使用。
为了明白Unity是如何合理地管理数据的,了解Unity是如何标识和序列化数据是很重要的。第一个关键点就是区分Assets和UnityEngineObjects。
Asset就是硬盘的上的一个文件,存储在Unity工程的Assets文件夹。Textures,3d模型或音频都是常见的Assets类型。一些Assets包含了Unity的内部数据类型,如材质,而另一些Assets需要处理成Unity内部数据类型,如FBX文件。
UnityEngineObject或具有大写'O'的对象,是描述特定资源实例的一组集合。可以是Unity使用的任何资源类型,比如mesh,sprite,AudioClip或AnimationClip。所有这些Objects都是继承自 UnityEngineObject 。
虽然大多数类型都是内置的,但有两种特殊类型:
Assets和Objects是一对多的关系,也就是说任何给定的Asset文件包含一到多个的Objects。
所有的nityEngineObjects可以引用其他UnityEngineObjects。这些Objects可能是在同一个Asset文件,也可能从其他Asset文件引入。比如一个材质通常引用一个或多个Texture Objects。这些Texture Objects通常从一个或多个texture Asset文件引入(比如Pngs,Jpgs)。
当序列化的时候,这些引用由两个独立的数据组成:File GUID和Local ID。File GUID标识目标Asset文件保存在哪里。局部唯一的Local ID标识Asset文件内的每个Object,因为Asset可能包含多个Objects。
File GUID保存在meta文件。这些meta文件是在Unity引入Asset的时候生成的,并且存储在和Asset同一个目录下。
以上的标识码和引用关系可以通过文本编辑查看:创建一个新的UnityObject,修改Editor Setting暴露显示Mate文件,序列化Assets为text。创建一个material,然后导入texture到工程。把这个material指认给一个场景中的cube并保存。
使用文本编辑器打开跟上满material关联的meta文件,靠近顶部会出现标记为"guid"的信息。这一行定义了material Asset's文件的GUID。而查找Local ID需要使用文本编辑器打开material文件。material Object's 的定义是这样的:
在上面的例子中,&之后的数据就是material的Local ID。如果这个material Object在Asset中GUID标识为"abcdefg",可以将material 对象唯一地标识为文件GUID "abcdefg"的组合,Local ID为"2100000"。
为什么需要Unity的File GUID和Local ID 回答的坚定的,因为它提供了一个灵活的、不依赖平台的工作流。
文件GUID为文件特定路径提供了抽象描述。只要一个特定的文件GUID与特定的文件关联起来,那么文件在磁盘的路径变得无关紧要了。文件可以自由移动,而不需要更新所有引用这个文件的Objects。
任何一个Asset可能包含(或通过import产生)多个UnityEngineObject 资源,这个时候需要Local ID明确的区分不同的Object。
如果一个File GUID关联的Asset文件丢失了,对该Asset的所有对象的引用也会丢失。所以meta文件必须要保存在跟它关联的同名文件目录下,这很重要。注:Unity会重新生成删除的或丢失的meta文件。
Unity Editor有Paths到GUIDs的映射表。无论Asset加载还是引入,都会记录在映射表。映射表由Asset指定路径链接到Asset File GUID。如果Unity Editor打开的状态下,meta文件丢失,并且Asset路径没有变化,Editor能够确保Asset保留相同的File GUID。
如果Unity Editor在关闭状态下丢失了meta文件,或者Asset路径变化了但meta没有跟着移动,那么所有引用该Asset的Objects都会被破坏。
在11内部Assets和Objects章节说过,非Unity原生Asset类型必须导入到Unity。这是通过asset导入器来完成的。虽然这些导入器通常自动调用,但它们也可以通过 AssetImport API暴露接口。比如, TextureImporter API在导入单个texture Assets(如PNG文件)提供了访问设置的接口。
导入的结果就是引入一个或多个UnityEngineObjects。这些Objects在Unity Editor是可见的,以父Asset下有多个子Asset形式显示,比如多个sprites嵌套在一个texture Asset,会当成sprite图集导入。其中这些每个Object共享一个File GUID,因为他们的源数据保存在相同的Asset文件。它们将通过导入的texture Asset内部的Local ID进行区分。
导入进程会将源Asset转化为适合所选择目标平台的Asset。导入进程可能包含了一些重要的 *** 作,比如texture压缩。因为导入进程通常是一个耗时的进程,被导入的Asset缓存到Library文件夹,避免下次启动Editor又重新导入Assets。
切确的说,导入后的内容存储在以对应Asset的File GUID前两位数字命名的文件夹中。这个文件夹存储在Library/metadata/文件夹中。Asset中单个的Objects被序列化到一个名字与Asset的File GUID相同的二进制文件中。
导入进程对所有的Assets有效,不仅仅对非原生Assets。原生assets不需要漫长的转换过程或重新序列化。
File GUIDs和Local IDs都是健壮性比较好的,但GUID比较慢,运行时需要消耗更多性能。Unity内部维护一个缓存,它将File GUIDs和Local IDs转化成简单的,唯一的整数。当新的Objects在缓存注册的时候,会被分配一个简单的,单调递增的整数,这就叫Instance ID。
缓存维护了给定的Instance ID,File GUID和定义Object源数据位置的Local ID与 Object在内存中的instance(如果有的话)之间的映射。解析Instance ID可以快速返回Instance ID代表的加载Object。如果目标Object还没有加载,File GUID和Local ID可以解析到Object源数据,可以让Unity及时加载出Object。
项目启动的时候,Instance ID缓存初始化所有的Objects(如场景中的引用),只要这些Objects在Resources文件夹。另外,当新的assets在运行时导入进来或从AssetBundle加载Objects时,Instance ID会加进缓存中。当提供访问指定File GUID和Local ID的AssetBundle卸载(uploaded)时,Instance ID才会从缓存中移除。发生这种情况的时候,Instance ID,File GUID,Local ID之间的映射会被删除(节约内存)。如果这个AssetBundle重新加载了,会从这个重新加载的AssetBundle为加载出来的每个Object创建一个新的Instance ID。
深度讨论AssetBundles卸载的含义,这看 Managing Loaded Assets 章节和 AssetBundle Usage Patterns 文章。
在一些平台上,一些事件可以强制让Objects强制从内存中移除。比如,当IOS app暂停的时候,图形Assets会从图形内存中卸载。如果这些Objects是从AssetBundle加载出来的就会被卸载掉,并且Unity不能够重新加载这些源数据。任何现存的引用这些Objects的对象视为无效。前面的这种情况,场景中可能出现不可见的meshes或紫红色的。
实践Tips:运行时,上述控制流并非字面上那么精确。当运行时,有大量大量加载 *** 作的时候,File GUIDs和Local IDs表现没有那么好。当创建一个Unity项目的时候,File GUIDs和Local IDs切确地映射成了一种更简单的格式。1、先尝试在游戏大厅工具条上点击“修复”选择清理缓存。
bf62c576efbee7af920e76cacb2bf7e8png
2、若还是没有改善,建议进入IE浏览器,选择“高级”下的重置IE浏览器,需注意的是重置会删除您的个人配置和cookie。
eb3b73388caf22fd6f6be78f32c47de5png
fcf9e05e3f39a5da361e561c11b4bcadpng
bb805d9754d0f39d5d8f34777ed9f48fpng游戏经常掉线
游戏经常掉线可能是由于当前游戏的服务器不稳定造成的。
当使用变速进行游戏加速的时候,有可能会出现频繁掉线的情况。此时请谨慎使用变速。
添加游戏
游戏大厅支持各个平台的网页游戏。玩家可通过360游戏大厅首页中的“添加游戏”按 自己想玩的游戏地址。
bf893ac13a89abd66fca5fc36b501415png
9c7ece77ed0382cfe0a0dabdef4d858bpng
bb805d9754d0f39d5d8f34777ed9f48fpng重装电脑、更换电脑如何备份账号
游戏大厅首页有备份账号的按钮,支持游戏和小号的导入导出。各位玩家可以通过导入导出
当出现验证码而无法自动登录时,可以手动输入验证码之后点击登录完成登录。Unity的资源加载及管理,基础很重要。此篇文章作为近期梳理项目内资源管理器的一个小总结,尝试尽量用人话将Unity对资源管理的关键点梳理清楚,个人觉得比较适合像我这样刚入门且对AssetBundle还不甚了解的家伙。
我理解的资源管理
举一个不恰当的例子来描述我所理解的资源管理(因为我实在想不出更合适的例子了),想象一个画面:一个表演者,站在一个台子后面,面向观众,按照规定的剧本, *** 作着台子后面不被观众看到的箱子,从里面不断的取出和放回各种新鲜的玩意儿,一会这么组合,一会那么拆散,博观众的眼球,最终完成表演。
我没有当表演者的经历,虽然我很想尝试,但想想也觉得这肯定不容易:
1、如果箱子里的东西都太大,拿起来会很费劲。
2、如果太小呢?恐怕会拿很多次。
3、不用的道具不收?放在台子上会影响接下来的表演。
4、用过的道具收了吧。万一收了后面需要的道具,一会儿用的时候还要再费劲拿一次,不拿的话吧还容易导致表演失败。
带着问题看文章
是选择合适的时间取出资源,并在合适的时候释放它们,尽可能保持较低的内存占用;还是选择让资源常驻内存,换取更快的读取和计算速度?如何在时间和空间上做出平衡,才能最大的提升游戏体验?我以为这些就是资源管理的目标和意义。可惜这并非易事。这不仅需要结合项目的实际情况,更需要丰富的实战经验。
但是在这里,你将不会看到任何可以参考的经验或建议,因为我也不知道啊。
无论你是否单身,在Unity的世界里,你都不愁找不到对象,因为一切都是对象。
无论是纹理、音乐还是预制体,在进入Unity的世界后,都变成了各种对象供我们使用,例如纹理转变为Texture2D或Sprite,音效文件转变为AudioClip,预制体变成了GameObject等等。这个由Asset(资源文件)转变为Object(对象),从磁盘进入内存的过程,就是实例化。而对资源进行的管理,本质上是对Object的管理。
“小当家,这个黄金炒饭是怎么加载出来的?”
简单介绍一下Unity加载资源的流程
在介绍Unity的资源加载机制之前,先举一个生活中的例子,来辅助我们了解Unity是如何工作的。
因为我从小热爱欧洲文学,所以在这就拿我最喜欢的《三国演义》做例子,我们都知道书中多次提到 “集齐七颗龙珠,就可以召唤神龙,并帮你实现一个愿望” 这种说法。
咸鱼都有梦想,何况一个上了岁数的程序员呢?但是很可惜,我们一颗龙珠都没有,为了凑齐这七颗龙珠,我们首先要知道它们分别在哪。一摸左兜,哎?发现了一本《召唤神龙的小诀窍》,里面记录了召唤神龙所必须七颗龙珠的 所在位置、大小、颜色以及如何使用等非常关键的信息 。
根据《召唤神龙的小诀窍》指引,我们知道原来第一颗龙珠藏在了素有小巴黎之称的北京通县,可是通县在哪儿呢?一摸右兜,原来这还有一本1986年出版的《中国地图》。那就放心了,出发吧!
终于,历经了81难,我们来到了目的地并最终找到了这颗龙珠。费这么大劲找到的龙珠,当然应该认真记录下来,于是我们马上掏出一个黑皮小本本,认真的记下: “第一颗龙珠放在背后小书包的左边缝有一个机器猫的侧兜里” 。
最终,经历了无数艰难险阻,我们凑齐了七颗龙珠(所以说人只要肯努力,老天就一定回馈你,至少让你知道你浪费了时间啊)。金光一闪,我们召唤出了神龙 后面实现了什么愿望我们不谈,因为谁没有点小秘密呢。
现在,让我们来回顾一下整个过程
1、这条召唤出来的神龙,就好比我们想要实例化的对象,就比如游戏对象吧,因为它相对复杂些。而这七颗龙珠呢,就好似组成这个游戏对象所必须的各种组件(Component)、纹理(Texture)、网格(Mesh)等等。
2、《召唤神龙的小诀窍》就好比我们读取的这个prefab文件,它记录了组成这个GameObject所必须的其他对象以及它们的位置。
重点来了:File GUID 及 Local ID。
File GUID
Unity会为每一个加入到Assets文件夹中的文件,创建一个同级同名的meta文件,虽然文件类型的不同会影响这个meta的具体内容,但它们都包含一个用来标记文件身份的File GUID。
例如,如果一个资源引用了另一个外部资源,比如一个Prefab引用了其他脚本、纹理或Prefab等,则一定会标明引用资源文件的File GUID。
Local ID
如果说File GUID表示为文件和文件之间的关系,那么Local ID表示的就是文件内部各对象之间的关系,打开一个Prefab文件可以很清晰的看到:
(点击上图,可放大查看)
一个对象通常是由 一个或多个 对象构成,每个记录在&符号后面的数字都是一个Local ID,每一个Local ID也表示这它将来也会被实例化成一个对象。也就是说,当一个prefab文件要实例化成一个GameObject时,它会自动尝试获取其内部Local ID所指的那个对象。如果这个所指的对象当前还没有被实例化出来,那么Unity会自动实例化这个对象,如此递归,直到所有涉及的对象都被实例化。
3、我们可以发现手中没有龙珠,是因为我们手中的黑色小本本,并没有记录龙珠装在书包的那个位置里;同样,Unity通过 Instance ID ,来获取或判断一个对象是否已经被加载完毕。 Instance ID由File GUID和Local ID转换而成,可以简单理解成是记录了资源所在内存地址的写着数字的钥匙牌。
每当Unity读入一个File GUID和LocalID时,就会自动将其转换成一个简单好记的数字牌,因为通过File GUID和Local ID定位资源的效率并没有直接解引用一个地址那么快。
如果发现这个牌上并没有挂着一把钥匙,表示当前这个这个资源还在磁盘中,尚不在内存里(没有加载);相反,如果这个牌子上有一把钥匙,表示这个资源已经被加载完毕,你可以快速的找到并使用它。
Unity会在项目启动后,创建并一直维护一张“映射表”,这张映射表记录的就是File GUID、Local ID以及由它们转换而成的Instance ID之间的关系,这样下次在请求资源时就可以快速的通过查看钥匙牌来获取资源了。
4、刚才的例子里,因为没有龙珠(资源没有加载),因此我们必须经历一场前往小巴黎的历险(LoadingAsset),而能够帮助我们准确定位北京通县的86版《中国地图》,可以近似理解成是 Unity维护的一套将GUID和FileID解析为数据源地址的机制, 这套机制中的信息,来自于:
(1) 场景加载时,Unity收集了与该场景关联的资源信息。
(2) 项目启动时,Unity收集了所有Resources文件夹下的资源信息。
(3) 读取AssetBundle时,Unity获取了AssetBundle文件的头部信息(Header)。
可以理解为:随着Unity知道更多的信息,这套机制将能够解析并定位更多的GUID和FileID。
5、当我们费劲千辛万苦找到龙珠后,记录在小本本上的7条位置,就好比7个能帮助够准确定位内存位置的Instance ID。想象一下,当我们下次再看到诸如《三颗龙珠召唤小神龙》这样的小诀窍( 另外一个prefab ),便可直接打开小本本( 查询映射表中的Instance ID ),对着编号及位置从书包里掏出龙珠( 对InstanceID所指的内存地址进行解引用 ),啪啪啪一 *** 作,小神龙这个游戏对象就能很快被召唤出来了,再也不用去什么通县了,可以节省大把时间,想想就觉的美滋滋呢。
AssetBundle
AssetBundle(阿赛特邦豆)是Unity官方推荐的资源加载方式,网上对AssetBundle的介绍有很多,且在了解了Unity对资源的加载机制后,其本身没有什么特别难以理解的地方了,因此在这不过多介绍,仅挑选几个关键点进行阐述。
AssetBundle的生成
生成AssetBundle有很多种方式,在此仅简单说一下比较常用的方式,使用BuildPipeline生成AssetBundle文件。
每一次调用BuildPipleLineBuildAssetBundles时,将会生成一批AssetBundle文件,具体数量根据传递AssetBundleBuild数组决定, 每一个AssetBundleBuild对象将对应一个AssetBundle及一个同名+manifest后缀文件 。其中AssetBundle文件的后缀用户自行设置,比如"unity3d","ab"等等;而manifest文件是给人看的,里面有这个AssetBundle的基本信息以及非常关键的资源列表。
除了AssetBundleBuild数组所定的AssetBundle外,还将额外在output路径下生成的一对与output文件夹同名的文件及一个同名manifest后缀文件。这个同名文件可厉害了,它记录了这批次AssetBundle之间的相互依赖关系。当然manifest文件还是给人看的,我们可以用它分析资源间的依赖关系,但是在项目实际运行时,Unity并不会关心它。
(点击上图,可放大查看)
可以通过这张图来看一下每次Build后资源的对应关系,当然这都不如你自己亲自Build一次看的清楚。
AssetBundle的加载
根据AssetBundle文件所在的位置(本地、远端),AssetBundle有不同的加载方式,在此仅总结最常用的本地AssetBundle文件加载。
我个人将AssetBundle拆分理解为: Bundle加载 和 Asset加载 两部分。因为AssetBundle文件可以从功能上分为两大块:
1、记录文件标记、压缩信息、文件列表的Header部分;
2、记录资源实际内容的Data部分。
当使用AssetBundleLoadFromFile或LoadFromFileAsync时,在pc平台及移动平台上,unity 仅会为我们读取AssetBundle的header部分,并不会将bundle的data部分整个读入内存。
当调用上一步生成的AssetBundle对象读取具体资源时(LoadAsset, LoadAssetAsync, LoadAllAssets),Unity会参考已经缓存的文件列表,找到目标资源在data部分的位置并读入到内存中。
如果一个资源引用到了其他资源,则必须要先读入被引用资源的AssetBundle文件,否则就会发生引用Miss。 这就好似召唤神龙时,通过《召唤神龙的小诀窍》得知第一颗龙珠在北京通县,但是当打开《中国地图》时,北京的地方被抠了一个窟窿,我去,这样我们就无法通过它准确定位龙珠位置了,只有六颗龙珠召唤出的神龙,当然有一部分是Miss喽。
为了避免上面Miss的情况,在加载资源时, 首先需要将该资源的依赖项全部加载完毕,不过仅需加载依赖资源的AssetBundle文件。 也就是说,我们只要将该依赖AssetBundle的Header部分加载(AssetBundleLoadFromFile或LoadFromFileAsync)就可以,这样在真正读取Asset时,Unity会自动处理好真实依赖的Asset,我们不用 *** 心。
AssetBundle的依赖关系如何读取呢?加载上面提到的那个很厉害的文件就可以了。
(点击上图,可放大查看)
非常简单的获取依赖关系的方法,通常会在项目启动时将全部依赖关系保存下来。
AssetBundle的使用
当AssetBundle被成功加载后,调用该Assebbundle对象的LoadAsset、LoadAllAssets或对应的异步版本即可加载资源,也就是实例化对象。如果这个对象已经被加载过,Unity并不会重复加载,还记得之前所说的映射表么,被加载过的资源就好比挂上了数字牌的钥匙,直接对地址解引用即可。
AssetBundle的卸载
如果说AssetBundle真的有什么容易出问题的地方,那恐怕就是卸载了。
在这里只说最常用的这个卸载方法吧:
public void Unload(bool unloadAllLoadedObjects);
一个被加载过的AssetBundle可以通过调用Unload来卸载这个Bundle下所有的Asset。但是调用这个函数时传入的参数 对卸载结果影响甚大。
Unity官方对这个函数的讲解非常详细,配图也非常直观,因此我只是简单总结一下。
相同点:
无论传入参数为 true 或是 false,调用Unload都可以Destroy当前AssetBundle对象,释放之前从AssetBundle文件中的Header部分所获取的信息。当然,被释放的AssetBundle对象无法再使用诸如LoadAsset、LoadAllAssets等函数加载资源。
不同点:
unloadAllLoadedObjects == true:
不仅Destroy了AssetBundle这个对象,而且 这个AssetBundle下包含的所有对象,只要实例化了,有一个算一个,统统释放掉。
感觉就像
foreach(Object asset in assets)
{
if(asset != null)
{
delete asset;
asset = null;
}
}
比如你通过abLoadAsset(apple)后,将apple设置给go_0的一个Renderer,如果这时候abUnload(true),那go_0就傻了,咋回事儿啊,图咋没了呢?WTF啊。
它的好处是: 不会有重复资源问题的情况发生,每次都处理的干干净净。
unloadAllLoadedObjects == false:
仅仅Destroy了AssetBundle这个对象,但是并没有释放这个AssetBundle下的任何Asset,因此如果有对象引用了这些Asset,也不会有问题。
它的风险(代价)是:下次再Load这个AssetBundle,并且通过这个AssetBundle重新读取了这个Asset,会在内存中重新创建一份,这样如果之前的Asset没有被释放,那么现在内存中就有两份Asset了。
这种情况如果频繁发生,便意味着内存中有很多资源将“不受控制”,容易引发内存占用过高的问题,而释放这种不受控的资源,仅有两种方式:
1、当没有对象引用到这些不受控资源时,每次调用ResourcesUnloadUnusedAssets,回收之。
2、加载场景时,如果加载模式没有设置为LoadSceneModeAdditive,则会自动调用ResourcesUnloadUnusedAssets。
同样,再举一个生活中的小例子以阐述这两种释放的差异吧:
小A交女朋友时喜欢送心形的石头给对方,这天小A认识了一个女孩,并确定了关系,送了一个精心挑选的心形石头给她,海誓山盟又云雨一番后,第二天由于感情不和等原因两人分手了。小A是个暖男,他为了女孩能彻底忘记优秀的自己并开始一段新的感情,约见了女孩,将之前送给女孩的石头拿(搬)走了,从此注销了微信消失在茫茫人海中。
确实,小A喜欢强壮的女孩,因为这样比较有安全感。
小B交女朋友时也喜欢送石头给对方,周一小B认识了一个女孩,并确定了关系,送了一个精心挑选的石头给她,海誓山盟又云雨一番后,第二天由于感情不和等原因两人分手了。但是小B家里是开石材加工场的,他并不关心这块石头,”送了就送了吧,至少我经历了浪漫的爱情“,小B这么想。并注销了微信消失在茫茫人海中达1天之久。
周二的时候小B重出江湖,并认识了一个新的女孩,确定了关系,第三天第四天啪啪啪第七天,第二周的时候,江湖上就出现了一个传说,集齐小B凑齐的七颗石头,便可以召唤神龙,于是就回到了文章开头我们提到的那个故事。
没错,小A对应的就是Unload(true),而小B对应的则是Unload(false)。
补充三点
1、移动Unity资源时,要在Unity编辑器内拖动,不要在 *** 作系统下剪切粘贴。因为这样Unity会为这个文件生成一个新的File GUID及meta文件,它会打破之前建立好的关系,让所有引用过这个文件的prefab出现miss的情况。
2、实际上在项目build完成后,就已经不存在File GUID和Local ID的概念了,转而用相对简单方式建立映射,这也是为什么我们在项目运行的过程中无法获取到File GUID的原因,不过原理上它们是一样的。
3、尽管一个AssetBundle的Header部分非常小,通常只有几十KB,但是Unity并不能保证读入大量AssetBundle的Header部分后资源的加载效率。因此还是按需读取AssetBundle吧。Unity会下载Assetbundle本地中,它的工作原理是先通过(版本号和下载地址)先在本地去找看有没有这个Assetbundle,如果有直接返回对象,如果没有的话,在根据这个下载地址重新从服务器或者本地下载。这里版本号起到了很重要的作用,举个例子,同一下载地址版本号为1的时候已经下载到本地,此时将版本号的参数改成2 那么它又会重新下载,如果还保持版本号为1那么它会从本地读取,因为本地已经有版本号为1的这个Assetbundle了。你不用担心你的资源本地下载过多,也不用自己手动删除他们,这一切的一切Unity会帮我们自动完成,它会自动删除掉下载后最不常用的Assetbundle ,如果下次需要使用的话只要提供下载地址和版本后它会重新下载。
我们在聊聊Assetbundle 中的脚本,在移动平台下Assetbundle里面放的脚本是不会被执行的,还记得我们打包前给两个Prefab挂上了脚本吗?在手机上将Assetbundle下载到本地后,加载进游戏中Prefab会自动在本地找它身上挂着的脚本,他是根据脚本的名来寻找,如果本地有这条脚本的话,Prefab会把这个脚本重新绑定在自身,并且会把打包前的参数传递进来。如果本地没有,身上挂的条脚本永远都不会被执行。
在Prefab打包前,我在编辑器上给脚本中的变量 name 赋了不同值,当Prefab重新载入游戏的时候,它身上脚本的参数也会重新输出。
如果你的Assetbundle中的Prefab上引用的对象,那么这样做就会出错了,你需要设定他们的依赖关系。或者运行时通过脚本动态的载入对象。大地形数据加载方案
大地形加载考虑到现有的内存机制, 不可能一次性将其加入到内存中,这个问题是显而易见的,其实在游戏开发中经常遇到,比如我们常见的进度条,加载进度条的目的就是等待程序加载场景,进度条只是一个蒙板遮罩而已。大地形的加载,别无他法,只能用分块,这个是大方向,因此作为程序来说,要做的事情是如何分块?块的大小是多少?这些具体的问题我们要根据需求划分,比如飞行模拟器块大小可能就要大一些,因为俯瞰的视角比较大,场景漫游块可以小一些等等,下面我们就以游戏的经典之作——魔兽世界地形加载方案为例给读者先介绍一下它的实现原理,魔兽世界这款游戏实现的就是无缝地图的拼接,所以非常具有参考价值,先看下图所示:
魔兽世界是如何实现无限地图的?其实它也是很多的场景块拼接而成的,我们通过编辑器分析魔兽世界的地形块的大小划分,魔兽世界场景我们称为MapWorld是由一系列MapTile组成,这些MapTile的大小是1600/3 ≈ 53333m,而每个MapTile又是由 16x16 个MapChunk组成,由此可以计算出每个MapChunk≈3333m。再就是每个MapChunk又由9x9+8x8个地形顶点高度,法线,若干贴图层(一般为4层)组成的地表纹理。魔兽世界地形的大小,在这里我们就不讨论了,但它划分块的思想我们是可以借鉴的。
继续分析魔兽世界的分块方法:它们是根据矩阵的方式进行划分的,在XZ平面上进行的,每个块都会包含一定的信息数据的,比如:在XZ(3,3)位置的MapTile,每个MapTile都包含了该tile内使用的贴图、模型实例等等。所谓模型实例也就是我们的道具,可以理解成相同模型在tile内不同摆放位置、大小、角度的信息,它们都是被保存在二进制文件中的,为了节省文件尺寸,模式实例是通过index模型方式保存的,同顶点索引类似,在每个MapTile里面还有贴图信息比如贴图的名字和UV信息等等。本篇课程的分块思维方式跟魔兽世界的类似,会在后面的章节中详细介绍,块分好了以后,下面就是实现原理了。
实现原理:在任何时刻,程序总是保存着玩家所在的及其周围的3x3个MapTile,随着玩家的移动,这些MapTile会被动态更新,新的MapTile被加载以替换被卸载的旧MapTile。为了提高调度效率,魔兽引入了Cache机制,Cache中保存着最多16个MapTile数据。需要加载新的MapTile时,首先会在Cache中查找;卸载的旧MapTile也不会被立刻删除,而是保存在Cache中以备再次调用。由于一段时间内玩家的活动范围通常不会有太大变化,这一Cache策略在应用中表现的非常出色,这是无缝地图的基本原理。地形的动态加载卸载我们会使用多线程去实现,我们会整两个线程:一个线程专门用于加载地形,另一个线程专门用于卸载或隐藏地形MapTile。让我们再来回忆一下游戏的经典之作,游戏场景效果如下所示:
本篇课程实现的方法可以使用两种方式处理块的加载显示问题,一种是利用对象池的方式,预先加载分块地形,根据视距进行检测判断显示那些地块以及隐藏那些地块,在这里并不删掉它们。这样只需要一个线程就可以。另一种方式是利用多线程,起一个线程专门用于移除卸载不在视线范围内的地块,这样可以提升效率,下面介绍使用多线程的加载方案。
多线程实现大地形加载方案
多线程在PC端游戏中使用的比较多,比如可以起一个线程专门进行资源的加载,游戏服务器中同样也会有多线程的使用,下面给读者介绍多线程实现方案,多线程处理问题就是把所有的加载逻辑放到了新的进程中,和主线程做一些进程间的通信,接受主线程的加载建议,做按需加载,也会自主做一些提前预加载,放进分配的内存,就跟魔兽世界的处理方式一样,通过进程间的内存共享机制,把加载的地形数据,共享给主进程使用。主游戏进程,永远只要维护一个很小的内存即可,大量的内存数据,都在另一个进程中处理。这样就可以优化大地形块的加载,实现方式如下所示:
首先主线程会先加载九块地形,主线程只负责维护这九块地形,无论角色怎么移动,角色所在的整个区域永远是九块地形,如上图所示的,这九块可以直接使用主线程加载到内存中,剩下的16块我们通过另一个线程将其放到缓存中,角色的位置是在已经加载好的九块地形中间,也就是在A所在的位置。随着角色的移动,会有新的地形块加入进来,同时现有的地形块会被置换出去,这样一直显示九块地形,被置换的地形并不会马上卸载掉,会根据角色移动情况做预判,它会等主线程通知,按照一定的规则进行卸载地块和加载地块。其实这种实现方式就是我们通常所说的双缓存-多线程技术。实现的效果如下所示:
地形分块加载完事了后,下面就要考虑地形上面的纹理贴图问题了,地形的贴图资源也会占用大的内存,下面介绍如何加载海量贴图数据。
大地形海量的加载方案
大地形中的场景非常多,地形中的贴图至少会有四层,这么多贴图我们在加载时需要考虑的,我们分块时也需要考虑这些因素,另外场景中使用的LightMap烘培也是要考虑的问题,为了缓解内存压力,我们事先会将不同块中的地形材质以及建筑物材质进行打包,先介绍如何分块加载场景贴图?它实现方式如下所示:
该思路就是将场景中的贴图根据我们划分的块打成不同的图集,当然也可以将两个块中的贴图打成一个图集,图集大小对于PC端来说,最大是4096,在移动端最大是2048。这个也是为了避免内存频繁的加载卸载会导致很多内存碎片,不利于后面大内存的分配。在打图集之前我们需要做点事情就是需要将地形块中的纹理贴图与我们的打包图集之间建立一一对应关系,方便对号入座。因为我们打包的图集跟实际地形之间不会有任何关系,要确立二者之间的对应关系我们需要在它们中间再整一张索引文件表格,它是连接图集与实际地形纹理的桥梁,通过我们建的索引文件,我们可以找到实际地形中纹理与图集纹理之间的对应关系,我们建的索引表格是要加载到内存中的,而我们的图集是根据加载任务后期才加到内存中的,这就要求我们的索引文件尽可能的少,因为它们是常驻内存的,除了海量的加载,我们还需要处理密集建筑的加载。
- 密集建筑的加载方案
密集的建筑加载,大家试想一下,如果把场景中所有的建筑一次性加载到内存中,内存瞬间就会占满,帧数瞬间下降,这也是为什么大家在游戏场景中移动时,遇到密集的建筑就会卡顿一下的原因。以前处理方式是使用LOD处理,被遮挡的物体使用简模,这样也会加大内存的负载效果,如果角色一直在建筑物之间来回穿梭,这样不同LOD模型就需要来回切换,对内存也是一个负担,效果不理想。这些问题对于程序员面来说必须解决的问题,如何解决呢?很多人想到了合并大Mesh,这种方法行不通的,大网格并不适合做裁剪 *** 作,试想一下,我们合并的网格,如果摄像机只看其一小部分,因为它们是一个整体这样就需要把他们一起加载到内存中,而实际上我们并不需要这么多模型数据,在合并网格时,在这里也给读者一个建议,尽量把靠的很近的模型进行合并,避免上述问题发生。其实最有效解决方案还是划分块,这个划分块可以利用地形划分的思想进行,它是与地形块紧密相关的,每个地形块中的建筑物跟随地形块一起加载。如果块中的建筑非常密集,这种方法还不能够完全解决,还需要进一步的处理,就是要加入OC遮挡算法结合LOD算法,这样就可以完全解决我们当前的问题了,这也是本篇课程
要讲解的方法,再进一步的优化方法是可以将OC遮挡算法和LOD算法放到GPU中计算,这样效率还会提升,在Siggraph2015发表了一篇文章GPU-Driven Rendering Pipelines,它的思想就是使用GPU进行遮挡裁剪处理,主要分两个阶段,使用的是DX12图形API,如下图所示:
它的思想就是第一步先做一个初略的遮挡裁剪列表,而后在此基础上再根据视线距离或者射线检测做进一步的细化裁剪 *** 作,这个思想跟我们的碰撞检测算法类似,引擎中碰撞检测算法也是基于这个原理实现的,给读者介绍一下:实际可用的碰撞检测算法,一般要分2个阶段:
第一阶段,broad phase 快速找出潜在的碰撞物体对列表,不在这个列表里的是绝对没可能碰撞的。broad phase确定了一批需要进一步检查的物体对。
第二阶段,narrow phase 准确找出发生碰撞的物体对列表。因为上一个阶段的部分物体对实际上是没有碰撞的,需要在这个阶段剔除。
broad phase其中有一个简单算法叫sweep and prune(SAP),本质上是利用了排序算法。第一步是初始化排序列表,列表中的元素是包围盒,可以用任意排序算法完成,例如快排;之后的排序就不是用快排了,而是用冒泡排序,为什么用冒泡排序更好呢?是因为一个默认的前提:物体的运动有时间相关性(temporal coherence),即当前帧和下一帧的位置是相近的,所以在冒泡排序过程中,发生的位置交换预期都很靠近。
其实算法中有很多类似的地方,这里我们也要互相借鉴它们解决问题的思想用于解决我们的问题。笔者以前做的是端游,端游中很多优化思想同样适用于移动端,移动端跟PC端比,就是一台配置比较低的电脑而已。接着我们的遮挡裁剪继续给读者介绍,论文作者也做了一个效率测试,以250’000物体,1G的网格为例,测试效果如下所示:
是不是很酷啊!在项目开发中完全可以用它解决问题,下面我们再谈谈使用GPU去优化我们的大地形场景。
GPU大地形渲染优化解决方案
我们的大地形首先会有自己的地表贴图,常用的地表贴图是四张纹理融合,最多可以有八张贴图融合,地形纹理渲染会涉及到LOD算法,远处的地形网格可以简化一些,对应的贴图也是最低的,这就是MipMap的使用。另外肯定有草有花以及其他大量相同的物件渲染,先说说草和花的绘制,他们在游戏中会非常的多,常用的做法是引擎提供的面片或者是十字交叉,或者三张交叉,然后将带有Alpha通道的贴图映射在上面,如下图所示效果:
CPU绘制这些草或者花在PC端是可以的,因为现在的电脑都是多核的,在手机端就会影响到效率问题了。使用CPU绘制,DrawCall会非常的多,而且草或者花还需要摆动,计算量很大的,这严重影响了运行效率,CPU有难,GPU可以帮忙,我们可以将草或者花的绘制放到GPU中执行,效果如下所示:
如果对楼主有帮助,给个采纳好不,谢谢啦
Photon和KBEngineunity3d是最适用Unity3d游戏开发的两个服务器引擎,但它们还是有区别的,只有清楚地了解区别在哪才能正确使用,下面简单描述下两者的共同点和不同点。
语言
对于大部分的程序员语言简直就是宗教信仰。
Photon使用C#开发,当然使用者也是用C#进行各类游戏功能开发。前后端同种语言,这对使用Unity3d游戏开发也有很大的好处。
KBEngine使用C++开发,逻辑开发是用python,也是很不错很快速的。
开源与收费情况
Photon是Exit Games公司的产品,不开源,有好多种收费模式,官网上可以看到。开发阶段可以用免费的license,后期可以看流量用户活跃度来选择付费模式。后续的支持,似乎是免费的,你可以选择邮件或是到论坛发帖求助,当然是E文。
KBEngine是国人开发,开源免费,但从官网上并没有看到商业使用的案例。有中文论坛,你可以在论坛上向开发者求助。
虽然两者的模式不同,但作为一个Unity3d游戏开发者,我们最希望的其实是把游戏引擎当作一个安全稳定的黑箱。
*** 作系统
之前说了Photon使用C#开发很自然的,配套的工具也是使用C#,比如最重要的PhotonControl。所以开发环境和生产环境最好都是windows。
虽然在跨平台上有mono,在服务器代码部分是系统无关的,但是不管你信不信,我是不信它的一套窗体工具也能运行在Linux下。反正,官网说法是,开发和生产环境都是用windows。
KBEngine建议开发环境选择Windows,生产环境选择linux。毕竟你总不希望开一组服务器打开9个Console窗体,一不小心把哪个点X了吧~
协议
Photon有自己的序列化反序列化方式,你也可以使用protobuf这类的来做应用层传输协议。
KBEngine在这方面表示不支持自定义协议,它帮你选择了有效的方法来处理,如果你习惯了他规定的方式,会喜欢上的。
看法
在功能上,我毫无疑问地更喜欢KBEngine,脚本化和自动持久化是极富魅力的功能。而Photon几乎没做这方面的功能,可能和老外的观念有关系。就目前我对两者功能的理解看来,Photon其实是个和SuperSocket差不多的东西,而SS是作为轻量级服务器框架存在的,Photon却是说自己是Unity3d游戏引擎,除去提供的MMO示例代码(未解读),没看到什么游戏引擎的魅力。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)