[Unity技巧]查Package包内的问题的小技巧

前言

这几天在查一个unity中inputsystem这个输入控件在有的手机端上出现ui的输入不响应,为了查找这个问题,我开始研究该怎么在package包内的文件输出一些关键日志,毕竟在手机端不太方便断点调试。在打印中我发现了两条路线,一个是用反射,这个是我最开始的方案,机缘巧合让我发现了可以把package包使用本地的文件打包进移动端。现在我就基于这两个方案讲讲我遇到的坑

正文

反射

因为我是为了获取到封装好的类中的一些数据,所以我所讲的只是我用到的一些功能,其他关于特性,动态赋值这些功能我应该不会讲。
当我们拿到一个对象,我们一开始想通过getType来获取它的类型,通过获取到的类型我们就可以通过它的实例来获取到它的一些属性值,比如我们想获取到变量的值,我们就可以通过type.GetFields(变量名).GetValue(实例)就能获取到变量的在当前这个实例中的数据。其他的什么属性呀,方法呀,都能这样获取到。
如果我们获取一个数组就有点麻烦了,首先我们得先判断这个对象是不是一个数组类型,如果是数组类型,我们就可以把这个对象转成迭代器(IEnumerable)这个迭代器是基础迭代器,它内部是object这个类型的,然后继续获取属性,获取对象,这样一点点的拿到我们想知道的数据。
在获取属性的时候记得要传入这个属性的类型,比如instance(实例化的),Public(公共),NonPublic(非公共)。

本地Package包


一个没有修改过的package包可以看到在他的说明左下角有个Develop这个选项,在我看来这个选项就是代表你可以把这个package包自定义开发。可以做成自己的自定义(可能会有些包不支持)。当我们选中了这个选项之后

这个包的位置就换到了Custom本地package了,它就被下载下来了,放到的路径在我们的Package路径下同名的文件夹下,然后我们重新加载我们的工程,就可以发现脚本的路径已经换掉了,还没有本地化之前的包是在Library/packageCache这里的。

然后我们去看Packages路径下的Packages-lock.json文件夹就会发现上面这样的修改。

结束语

我们在日常工作中经常会遇到需要去调试源码的情况,但是在手机端这种调试很麻烦,所以我们只能通过打印log去验证我们一些想法。调试麻烦不是说不能调试,我之前就在xcode上调试过unity工程,太麻烦了。

【unity】协程和迭代器

前言

在很早之前忘了是哪次面试还是什么时候,有人问我协程是什么,那时候我根本就没有这个概念,所以在网上找了一些资料,发现很多人都是讲协程和什么线程这种概念的区别,某一次我看到了某篇文章,上面详细讲解了一次协程的原理,和自己实现一个协程。那时候我也想自己去了解一下,但是因为各种各样的缘由,我在前段时间看书看到了迭代器这一章的时候,根据迭代器的定义,发现协程其实就是一个迭代器的一种实现。现在我就简单的基于协程的原理来通过迭代器实现下这个功能。

正文

迭代器

迭代器是一种设计模式,在所有讲设计模式的书上都有介绍的。它是对于遍历的一种封装,它依赖于一个叫迭代器的接口,我们通过实现这个迭代器的接口可以对各种集合的一个遍历,我们可以不用关系这个集合是数组,列表还是散列表的。

c#中的实现

  1. IEnumerable和IEnumerator
  2. IEnumerable<T>和IEnumerator<T>

上面的两个的区别是一个是基于泛型的接口还有一个是非泛型的接口

枚举源

借助c#语言的一个强大的功能,能够生成创建枚举源的方法。这些方法称为“迭代器方法”。迭代器方法用于定义请求如何在序列中生成对象。使用yield return 上下文关键字定义迭代器方法。可以在迭代器方法中使用多个离散yield return语句。迭代器有一个重要的限制:在同一个方法中不能同时使用return语句和yield return语句。

IEnumerable和IEnumerator

当我们自己实现一个迭代器的时候,我们需要创建两个类,分别继承IEnumerable和IEnumerator这两个接口,他们最重要的是IEnumerator,IEnumerable这个接口需要实现的方法是返回一个IEnumerator的对象,然后我们通过对IEnumerator进行遍历。在实现IEnumerator接口的时候我们需要实现一个是MoveNext这个方法,它是用来判断集合中是否还有下一个元素,Reset这个方法用来重置遍历,下一次从头遍历。Current这个方法是用来获取当前遍历的元素。Dispose这个方法是用来对迭代器进行回收。枚举器用来是读取集合中的数据,不能用来修改基础集合。在最开始的时候枚举数定位在集合中第一个元素的最前面,所以在reset的时候把这个枚举数定义为-1,当我们调用MoveNext的时候会判断当前的枚举数(++curindex)是否在集合的最后一位,如果已经在最后一位了,那就会返回一个false,然后current的值不变,如果不在最后一位,那就把current的值修改到下一个元素。因为是通过枚举数来确定是否在集合的最后一位,所以当我们对集合做出了修改,就会导致枚举器的行为是不确定的。在这里我发现了一个事情,就是默认的IEnumerator的current的返回值是一个object的类型,这个会不会就是人们常说的用foreach来遍历集合会有很高的消耗呢?毕竟如果是一个int这样的值类型会有一次拆装箱的操作。如果说被优化了,那优化点就是值类型的泛型他们单独实现。通过我查看了下他们的源码发现真的是这样的,他们对list的IEnumerator自己改成了一个Enumerator的一个结构(struct)内部实现的Current是一个T的泛型,所以这里就不会出现我看到的拆装箱的操作。

yield

  1. yield return <expression>
  2. yield break

使用yield return 语句可一次返回一个元素。
迭代器方法运行到yield return语句时,会返回一个expression,并保留当前在代码中的位置。下次调用迭代器函数的时候,将从该位置重新开始执行。
使用yield break来终止迭代器。
返回yield或IIEnumerable的迭代器的IEnumerator类型为object。所以当我们yield return返回一个int这样的值类型的时候会进行一次装箱操作。
yield break
是用来跳出迭代器的。

协程

    IEnumerator test2 = test();
                while (test2.MoveNext())
                {
                    Console.WriteLine(test2.Current);
                }
     IEnumerator test()
            {
                Console.WriteLine(DateTime.Now);
                yield return new test1();

                Console.WriteLine(DateTime.Now);
            }

            public class test1
            {
                public test1()
                {
                    System.Threading.Thread.Sleep(2000);
                }
            }

看到上面的代码有没一种很熟悉的感觉,这个是我实现的一个延时执行下一行代码的协程,在实现的时候,我一直在想,其实我们可以把expression当成是一个同步阻塞方法,当这个方法执行结束了之后,我们的迭代器再执行下面的方法。

结束语

万变不离其宗,掌握了原理其实很多东西都是很简单的。当前我就吃了研究不深的亏,很多东西都是用就完事了。现在回想起来,知识这个东西还是不能拖的,趁着热情还在,仔细研究透。

【插件】Wwise在unity中的使用

Wwise 内存管理

SoundBank

初始化Bank

我们现在打包出来的路径下能看到一个叫Init.bnk这个Bank,它包含了工程所有通用信息。是在生成SoundBank时自动生成的,每个工程只有一个。在启动游戏的时候加载的第一个Bank必须得是它。如果不是它会影响其他的Bank无法加载。

正常SoundBank

包含Event和播放他们所需要的对象和音频数据。如果当前的event所播放的音频数据是流播放数据,那它会记录到相应的.wem文件。

在Unity中使用SoundBank

使用标识类型

  1. 字符串

    • 提高代码的可读性
    • 把字符串转换成声音引擎使用的ID需要少量的CPU。
    • 声音引擎中不存储字符串。
  2. ID

    • 代码可读性不好
    • 声音引擎在内部使用ID,因此无需占用任何额外的CPU
    • 无需占用额为内存

LoadBank

我们使用AkSoundEngine.LoadBank来使用字符串加载SoundBank的时候,它会返回给我们一个SoundBank ID这个ID是存在SoundBank里的,所以这个ID和我们在wwise内看到的一样。

UnloadBank

使用UnloadBank来卸载SoundBank的时候,我们需要传入的是它的ID,根据上面的加载,我们需要好好保管这个ID,以便我们能正常的卸载ID。

通过内存加载

LoadBankMemoryView

这个方案其实就是通过使用Unity自带的本地资源二进制加载,然后通过AkSoundEngine.LoadBankMemoryView把这部分的数据序列化成声音引擎能使用的数据。它加载的资源是位于内存中的实际位置,不会拷贝资源,所以在使用的时候必须保证内存一直有效,直到卸载掉SoundBank。

LoadBankMemoryCopy()

也是通过序列化数据加载soundbank,这个和上面那个方法的不同之处,是会把数据拷贝到声音引擎的分配内存中,所以当数据被加载完成之后,就可以从unity中卸载掉。

PrepareBank

AkBankContent_All

这个是一个PrepareBank的一个枚举选项,使用这个选项的时候,在加载SoundBank的时候是把它所有的内容都加载进去,在加载媒体文件的时候,运用于类似于PrepareEvent的机制,先查询媒体文件在不在内存中,如果不在就加载媒体文件,如果存在了就不在加载。提高内存的利用率。

AkBankContent_StructureOnly

使用这个选项的加载方式,只会加载时间和结构元数据,会忽略SoundBank中的媒体文件。当SoundBank中不包含媒体文件的时候,它和上面的那种模式可以产生相同的结果。

清空SoundBank

调用ClearBanks()函数可以重置声音引擎中的内容。调用这个函数之后:

  • 所有声音停止播放。
  • 所有SoundBank均会被卸载,包括Init.Bank
  • 所有的State管理均会清空。

注意点

  1. 每个SoundBank只可加载一次。如果试图显式加载一个SoundBank两次的时候,它在第一次加载之后的返回值将会是AK_BankAlreadyLoaded,告诉你已经被加载过了。当你把媒体存在两个不同的SoundBank里,你就可以显示加载它两次,内存中就会出现两份资源,这个时候你就可以使用PrepareEvent的特性,让同一份媒体资源只在内存中存在一份。
  2. 在使用SoundBankID加载的时候,需要在设置中取消勾选“Use SoundBank Names”这个选项,然后生成出来的SoundBank就是ID.bnk格式的。如果使用Wwise自带的Unity插件,就需要注意,它里面初始化的Init.bnk。在使用ID格式是找不到的,所以需要把Init.bnk改成初始化bank的ID名字。
  3. 当把音效资源当成stream加载的时候,会在SoundBank中记录下来音效资源的流信息,在使用event的时候就会去使用Stream Manager来加载流音效资源播放。

流播放

有限的带宽往往会使得存储设备的访问速度成为游戏的瓶颈。因此,为了保持良好的数据传输请求顺序,游戏通常会集中访问 I/O。排序的依据是相对优先级、相对于传输大小的开销要求、吞吐量和延迟。音频一般需要大量的 I/O 资源:音乐和较长的声音在播放时通常从磁盘传输。这块后面单独写一个文章用来讲解,展示先放下不讲了。

使用wwise的event

AKGameObj

在播放wwise的时候,我们首先需要确定游戏对象,因为不管是听者还是发声器,我们都需要知道他们在世界坐标中的位置,通过调用AkSoundEngine.SetObjectPosition我们可以设置场景中物体的位置和朝向。这个物体记录到声音引擎中,当我们需要给一个听者或者一个发声器设置场景坐标的时候,就可以使用这个记录的信息。通过AkSoundEngine.RegisterGameObj这个方法,我们可以把场景中的物体在声音引擎中记录一个id,这样的话,我们就可以通过id找到这个物体。AkSoundEngine.UnregisterGameObj通过这个方法,我们就把这个物体从声音引擎里移除掉了。在wwise里去记录gameobject对应id是通过AkSoundEngine.GetAkGameObjectID这个方法来计算的。这样我们把unity场景中的一个物体的位置和旋转信息就在声音引擎通过id记录下来的。

AkAudioListener

对于声音我们需要一个收听音乐的节点,这个脚本就是为了给我们定义一个听者的信息。通过AkSoundEngine.AddDefaultListener来设置一个默认的听者,它将指派到所有的发声器。AkSoundEngine.AddListener可以给发声器设置一个听者,用来覆盖掉默认听者。RemoveListener和RemoveDefaultListener是用来取消听者设置。

AkEvent

播放wwise的事件调用的接口是AkSoundEngine.PostEvent它会返回一个id,这个id可以通过调用stopPlayingID来停止event的播放,但是在看unity内的代码的时候发现只在Windows做了拓展。还有一个停止Event的方法调用AkSoundEngine.ExecuteActionOnEvent传入枚举为stop

AkAmbient

当我们为了节省内存的时候,可以把使用相同Event实例只生成一个声音实例,以便节省内存,调用AkSoundEngine.SetMultiplePositions相当只用了一个声音实例却可以设置不同的位置。

AkEnvironment

当我们想给声音加一个Auxiliary Bus来达到一个混响的效果,我们可以调用AkSoundEngine.SetGameObjectAuxSendValues来添加一个混响的效果。

AkSwitch

当我们使用wwise内的音乐切换功能,相当于在同一组声音内根据某一种规则进行播放的切换。可以使用AkSoundEngine.SetSwitch传入组id和你要切换的id加上播放声音的对象。

Spatial Audio

这个是Wwise一个提供和空间音频相关联的服务的一个模块。普通的wwise提供了一些正常的根据位置,方向,角度的声音渐变效果,和一些声音左右声道的切换的效果,但是我们如果需要对声音进行一些反射,衍射和透射的效果的话,还是需要用到Spatial Audio这个模块。这些的处理很依赖游戏引擎,并且很消耗性能。

Room和Portal

通过Room和Portal进行一次简单的几何抽象,对发声体的声音传播进行准确的建模。对于房间驱动的声音传播,其主要特性为衍射,耦合以及混响的空间化。Room没有尺寸大小,他们通过Portal相互连通,并形成由房间和开口组成的网络。其他房间发出的声音可以通过开口传播至听者所在房间。

衍射

对于相邻Room内的每个发声体,Spatial Audio都会计算相连Portal最近边缘与Shadow Boundary的衍射角。通过衍射角映射衍射系数,通过获取这个系数,我们可以修改发声体的值,obstruction值或者Diffraction值。
还需要考虑到Room的漫反射能量。这部分的衍射也会同时计算出来,在Spatial Audio假定漫反射能量沿着Portal垂直方向从Room向外渗透。因此计算相对于Portal法向量的衍射角。

透射

当听者和发生源不在同一Room且两者无法直接通过Portal看到彼此时,Spatial Audio会向游戏对象应用透射损失系数,并据此对声音穿透墙壁的情形进行建模。通过设置Occlusion曲线或Transmission Loss内置参数。
Room之间的透射损失依据Room设置参数进行计算,并取听者所在Room和发声体所在Room当中的最大值。
若发声体被一个或多个几何表面阻挡,则应用几何构造的透射损失系数。若发声体和听者之间存在不止一个表面,则采用最大透射损失系数。
若声音即被几何构造所阻挡,又跟听者不在同一个room,则应用所有透射损失中的最大值。

在unity中使用

AkRoom

调用AkSoundEngine.SetRoom来设置room(gameobject的id),然后通过音频引擎获取到这个id的gameobject对象,从而得到坐标,设置room的朝向和辅助音频总线等信息,还有就是几何信息(比如collider这样的信息)。值得注意的一点,因为wwise的游戏对象和room的id都是通过AkSoundEngine.GetAkGameObjectID计算出来的,所以当这个对象被设置成游戏对象的时候,不要重复用在roomid。

AkRoomPortal

调用AkSoundEngine.SetRoomPortal来设置Portal的信息

  1. protal的id,通过这个id找到gameobject在wwise内注册的信息。
  2. 设置protal的collider的位置信息,中心点,正前方
  3. 设置protal的大小信息
  4. 设置protal是否激活状态
  5. 设置protal连接的两个room的id

AkRoomAwareObject

当wwise调用了SpatialAudio的时候,它会针对每个发声体和听者调用AkSoundEngine.SetGameObjectInRoom把他们设置到相应的room内。

使用AkEvent

我们可以将RoomID当成发声体的ID来设置PostEvent的gameobject对象。

Geometry

通过Geometry可以把三角形网格发送给Spatial Audio实现reflect模拟早期反射,和模拟声音从发声体到听者位置中产生的边缘衍射。

终结

上面我就讲解了下wwise的音频资源在unity内api的使用方案,其实很多声音的效果我们都是可以在wwise内做好的,比如声音的衰减方式,比如弹壳掉落地上的声音随机在左右声道播放,等等效果。它把很多需要我们在unity中自己实现的一些声音表现,使用wwise让音频制作人员给做好了,我们在游戏内就只需要播放一下声音的事件,和声音资源的管理方案。

【性能】ugui对象不可视

前言

在很早之前我做过一次性能分析,分析物体在各种不可视方案下的性能开销但是这个是在对于Gameobject的分析,现在在做ugui的界面显隐的时候,又多出了好几种方案,现在来最这几种方案做一个分析。

正文

测试代码还是和以前一样,对物体进行10万次显隐操作。

隐藏控件

Canvas

在ugui中,界面的渲染都是基于canvas的,所以当我们隐藏掉canvas这个控件的时候,界面也将看不见,所以我们先来做一次canvas的控件显隐操作

因为我的子物体有6个image所以当canvas被隐藏的时候,它会去回调image的渲染回调,所以我们可以看到调用image.OnCanvasHierarchyChanged调用了120万次。回调中因为会用到一些对象,所以它的GC压力很大。

Image

对于单一的物体,我们以Image为例,它的渲染是基于它的Image这个控件,所以当我们把控件隐藏了,它将不再被渲染

这里我们看到它的主要开销是OnEnable和OnDisable这两个方法。

设置透明

CanvasGroup

CanvasGroup是用来控制子控件的alpha等一些界面属性的,所以当我们把它的alpha设置成0的时候,子节点的alpha的值也就变成0了。

看到ugui放出来的CanvasGroup的代码可以看到它有一部分的实现在c++里的,当CanvasGroup的alpha的值发生变化,应该是要发送消息告诉子节点的透明度的变化,所以这里的120万的调用我们猜测是因为子节点有六个,所以要抛消息到所有的子节点上。

image

当image的alpha的值被设置成0的时候,我们就看不到这个组件了

可以看到它就只需调用自身,没有其他的函数调用。

设置大小

这个就在之前的测试里做过的,因为大小为0的时候,它将不会被渲染出来,所以也就不会可视化出来。改变的是矩阵。

结论

在我看来我觉得还是设置大小是最优的显隐方案,用代码测试一次就好了。大家一起来看看还有什么情况是没有测试到的呢。

【编程基础】值类型和引用类型

前言

值类型和引用类型,对于使用了托管内存的编程语言来说,他们的最大差距就是在分配和回收上的差距。

  • 值类型
    它是在存储在栈上的,当你在创建它的时候,它被压进栈内,在它出栈前,它都将可用,把它传进别的函数内使用的时候,这个时候复制了一份它的内容,然后把这个新拷贝内容给压进栈内。所以我们在函数内改变的值,只是改变了这个新压入栈的值,当然了我们也可以把这个值的引用传入进来。这里说将的值类型其实都是在函数中的局部变量。

  • 引用类型
    它是存在托管堆上的,我在这里所说的类型区分都是基于托管内存的编程语言来说的。当我们创建一个对象的时候,先告诉托管堆给我们分配一块内存,然后它把这个内存的地址引用返回给我们,然后我们就在这块内存内做一些操作,存一些数据,一些别的托管内存的一些指针。

引用类型中包含的值类型

如果引用类型中有值类型的变量实例,那它会在分配给它的内存中开辟值类型所需要的内存大小,给值类型用。那这个时候只值类型就被放在托管堆上了。

正文

看完上面的一些东西,是不是感觉很迷茫呢?有些迷糊值类型和引用类型究竟有哪些区别,究竟是放在堆上还是在栈上,它们之间啥关系呢?下面我就来讲讲。

一位仙气少年

在编程世界里,我们程序启动有一个虚拟机在跑。当我们把世界转向一个玄幻的世界里,那这个虚拟机我们让他叫做小m,是一个刚刚从师门出发,将要去入世,去闯荡一番。那现在我们来给他一些入世需要的装备吧。

绝世武功?

在给小m发放装备的时候 ,我们先去了解小m有哪些功法,他可以克隆一切物体,还会一种世界上绝无仅有的功法,那就是空间术。当他在学习这门功法的时候,他也继承了前人开辟的一大块空间(这个就是托管堆的空间大小),他可以自己去这块大的空间里切割一部分归自己所用。在了解了这些之后,我们来给小m发放装备了,俗话说得好的好,功夫再好也怕手枪,所以我们先给他一把枪,这把枪有个特殊的属性,就是能发射任何东西,但是,大家也知道,弹夹有个特性,就是最后压进来的子弹是最先发射出去的(这就是栈)。其实有了这把枪我们就可以不需要再去给他发放什么装备了。

空间戒子

小m在收拾东西的时候,发现自己想带的东西太多了。这个时候他想起来了自己的空间术。他在仔细研究之后,发现在自己切割下来的空间内,能放和自己相关的任何东西,那他就开始创建了一个空间指环,但是有个不同的情况就是,空间术创建的时候,它的空间大小是不能更改的,当你创建的时候是多大,那就是多大,当你创建的空间指环不够你使用的时候,你就需要重新创建一个更大的空间,然后把原来空间的东西给搬到新空间里,在这个操作是,需要很耗费小m的功力,需要花费他很多时间。现在小m已经创建了一个空间戒子了,他现在把这个戒子带到身上去了(获取引用)。

Hello Word

现在小m带齐了自己的所有装备开始下山了,他下山后看着这个未知的世界,他不知道怎么的就像对着这个世界说上一句“Hello Word”。那现在它就开始使用它的枪打出这几个字了,它开始一个字符一个字符的压入枪内d,r,o,w o,l,l,e,h。然后一口气打出了九发,但是它觉得太不爽了,连续扣动扳机太累的。转念一想,自己有个空间术吗?这把枪不是可以射出任何东西,是不是可以把我这个空间术内的这一整块空间射出去呢?它就开始使用空间术来创建了一个特殊的空间,就是为了能把字符全部装进,这个空间他为了保险,还做了一个限制,就是我只能放一次字符的集合,后面再放进去,那就只能重新在创建一个新的空间,把之前的字符集合给拷贝进去,然后再添加。好了,现在我们开始把这个字符串空间打出去了。一枪出去,天空上炸出了一整句“Hello Word”。小m现在太兴奋了,感觉自己创造了功法的一种新的使用方法。然后疯狂的往天上打枪。

子弹不够了?

突然小m发现他打出来的子弹是空弹,啥都看到。这个时候他也发现了不正常的情况,就是他不能再使用空间术了,这个时候小m慌了,他觉得他最大的一个依仗没有了,他懊恼自己的浪费。这个时候,他疯狂的翻自己的秘籍,就像知道,怎么让自己重新获得空间术。他查了一遍又一遍,终于发现自己的功法不能用是因为他没办法自己开辟空间,只能用继承下来前人的那一整块空间,但是在刚刚那一波疯狂输出的时候,这块继承的空间以及被消耗殆尽了。然后他再一次翻阅前人所遗留下来的文档,发现了,这个空间术在开辟空间的时候,只是暂用了前人的空间,前人空间有个特殊的属性,就是,当发现你没有再使用的时候,它就会把这被你借走的空间给拿回来,把里面的东西清空了。但是这个属性跑起来的这段时间内,小m就没有办法使用空间术了。小m发现了这样的一个属性的时候很高兴呀,感觉自己将再一次能掌握到空间术了,他开始研究起来,究竟什么情况下会被认识这块空间不被自己使用了。原来是自己把空间术所返回过来引用给遗弃掉,就是当自己从枪里发射出去,其实就是把引用给抛走了。现在小m就开始等待空间恢复了。

这颗子弹真奇怪

小m在探险的时候,发现了一种特殊的材质(接口),我们称这个材质为I。这个材质自带一种空间属性,小m用这个材质做了一枚子弹(struct),当这枚子弹在使用的时候被当成I类型使用的时候(装箱),它就突然变成了一个引用类型,它会把它所有的东西都包装到空间槽内,然后把自己变成一个引用类型的子弹。当把它当成一个struct类型的子弹使用的时候(拆箱),它会把它放置在空间槽内的东西都打包拿出来,变成一个真正的子弹,那部分被它用来放物体的空间槽就被遗弃了。等待回收。

后记

这篇太难产了,想换个写法,感觉自己还是写不出来,这种的写法不太适合我

[设计模式]单例模式初探

前言

最近在审核自己以前的代码,看来下自己之前为了方便,很多小的功能都用单例模式来进行编写。因为已经距离现在有一定的时间了,有些东西没有考虑清楚,今天我就初级的讲一下单例的几个坑。

正文

我需要一个对象

当我们在使用一个引用对象的时候,我们需要把它实例化,然后才能使用,实例化的概念就是在内存上给你分配一块内存,然后根据对象的构造函数,往这块内存里写数据,最后把这块内存的引用返回给你。所以当我们实例化之后,我们就在内存上占用了一块内存。

我是个渣男

当我们这个对象在使用完了之后,我已经不再需要它了,那我就需要让它消失,把它所占用的内存给回归到内存池里去,可以让别的对象所用。但是我不想每次都要自己去管理让它被销毁。它能不能自觉点,我不需要的时候就去自我销毁,减少我的工作量呢?在现在很多高级语言里,他们的虚拟机都给我们集成了这项功能,它的名字就叫GC(垃圾回收)。当我需要一个对象的时候,我就向托管堆里去申请一块内存来使用,当我们不在使用的时候,我们把对这块内存的引用给释放了。然后通过root节点找到它引用对象的再引用,一直找到最后的引用节点(就是一层一层的递归寻找)把它们标记成被引用,然后当GC回收的时候,它们找到这块内存的标记是引用就会过滤掉,把那些不再被引用到的内存给回收掉了。正是因为在打标记的时候需要做递归寻找,这就是为啥会说GC有很大的消耗的原因,需要我们管理好引用对象的生成。

我觉得你很有用,你可以一直跟着我

当我们需要经常使用一个对象的时候,我不想经常性的实例化这个对象,我希望它能永远的存在。这个时候静态类就出现了,我们可以直接通过它这个类名去直接调用它里面的方法,它的所有方法和变量都应该做成静态的。当一个静态对象的出现,它就进化成root节点之一。将从它开始查找打标记。

你不要一开始就出现

当我们把一个类定义成静态类的时候,它里面的所有变量,属性,方法都将成为静态的,也就是说只要程序一启动,它就把它所需要的内存所占用了。有的时候,我们可能会在很晚的时候调用它,或者在这一次应用启动的时候都不会调用它里面的方法,那它在启动的时候就分配内存,就会造成内存泄漏了。这个时候我们就可以在在普通类里声明一个静态变量,它的类型就是这个类。给一个静态属性,当我们调用这个属性的时候才开始实例化这个变量,这个时候才开始分配内存,当它是一个静态属性的时候,它还是一个能永远存在内存中的root节点。

你是一个成熟的对象,用完就要放手

当我们生成了一个静态属性的时候,它是一个root节点,所以它所引用的引用也会一直在内存中,不会被释放。所以对象我们在使用完了之后要把它释放掉,我们就要定义好这种类里的方法都不需要缓存数据的。要不然内存爆炸的锅就要你来背一背了。

哦!同时出生了这么多个

当我们使用了多线程的时候,就会发生这样的事,这个对象被同时要求做好几件事,当第一次调用的时候,它就会同时生成好几份对象,因为在调用的时候它是没有实例化的,所以我们就会同时进行实例化,那它就同时会创建好几个对象。这个时候我们就要把这个方法设置为同步方法。让它在同一时间只能被一次调用。

你干活可真慢

当我们把这个获取对象的方法设置成同步方法的时候,它在同一时间只能被调用一次,当我们在多线程的时候,其实我们只是为了规避它被创建多次,这个时候,我们可以把它的同步方法放在第一次创建生成对象的时候,当对象已经存在了,那就算被同步使用,其实也不会对结果产生什么差异,毕竟我们在这里是尽量使用方法内部的变量 ,而不会去使用类内的局部变量。

总结

class Singlecase{
    private static Singlecase instance = null;
    private static object locker = null object();
    public static Singlecase Instance{
        get{
            if(instance == null){
                lock(locker){
                    if(instance == null){
                        instance = new Singlecase();
                    }
                }
                return instance;
            }
        }
    }
}

上面的实例代码就是单例模式,单例模式在我的理解力就是为了能做一个全局的工具类,为了做一些通用的简单计算,不涉及一些数据的保存,只是为了一些数据处理。当我们使用单例模式的时候,需要保证我们这个类对一个不需要的对象,一直保持着引用,而使它的内存没有被回收掉。

【Unity】IL2CPP初探

前言

在上一篇文章发出来之后,一大佬和我说,他详细的看了好几遍,完全不知道我在说什么。给我提了一个建议,就是用问题带领读者去思考,我所讲的知识是什么。这次我就按这个思路来写一篇文章。试试效果怎样。

正文

一堆数字的困惑

不知道大家有没有看过这样的一种错误堆栈信息,全都是一些数字,看不出来它所代表的类和函数,还有函数中的哪行代码的问题。然后脑袋里有一个大大的问号,这个究竟是啥呀,该怎么解析呢?这个东西就是elf文件的符号,它需要靠一张符号表来解析成我们人类可以认识的内容。选项现在我们来看看它的字典是什么?

符号表

大家应该知道,真正在计算机上跑起来的东西是一组组二进制文件。我们所编写的什么c#这样的语言其实是属于高级语言,它需要通过一系列的编译变成机器所能执行的机器码。当不同的文件相互糅合的过程,就是将它们之间对地址的引用,或者可以说是对函数和变量的地址的引用进行合并。那怎么来进行这一步的合并呢?人们就想到了这样的一种方法,把每一个文件都解析出一张表,用来记录这个文件中所用到的所有符号以及它们所对应的符号值。当在链接的时候,文件就会去互相寻找自己需要的符号来完成链接。这张表就是符号表。

符号表长啥样子呢?

在汇编阶段,就是生成目标文件的时候,就会产生这张符号表,每一个生成的elf文件都有符号表。符号表中的符号和源码中的变量名和函数名是一一对应的(这种对应关系不是说它们名字是一样的,而是说它们的一种映射关系,在c++编译的时候偶他们生成的就不是完全一样的)。这里是一段测试代码:

//main.c

void test2();
int main(){ 
    test2();
    return 0;
}

//test.c
static int a;
int b ;
static void test1(){
    return;
}

void test2(){
    return;
}

生成目标文件之后,用read -s test.o main.o命令即可查看其中的符号表,如下:

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     8: 0000000000000000    21 FUNC    GLOBAL DEFAULT    1 main
     9: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND test2 //注意这里,Ndx是UND的,且value为0

// test.o的符号表

Symbol table '.symtab' contains 12 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS test.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     5: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 a
     6: 0000000000000000     7 FUNC    LOCAL  DEFAULT    1 test1
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
    10: 0000000000000000     4 OBJECT  WEAK   DEFAULT    3 b
    11: 0000000000000007     7 FUNC    GLOBAL DEFAULT    1 test2

其中,value叫符号值,对于变量和函数而言,符号值就是他们的地址,size是一个符号值所占字节数,type是符号的类型,像变量的类型就是OBJECT,函数的类型就是FUNC。Bind列是符号的作用域,LOCAL表示是局部符号,GLOBAL表示是全局符号,WEAK表示是弱符号等等。Vis列表示符号的可见性,一般用的比较少。Ndx列
表示符号所属的段的index(.text段,,data段等等)。Name列就是刚才说的与变量函数名一一对应的符号名。
注意一下,在没有链接之前,main.o并不知道test2函数的定义和地址,所以在main.o的符号表里将test2标记为UND(undefine的意思),地址值也缺省为0000000000000000,等到链接的时候寻找到定义了test2函数的目标文件的符号表,就提取出它的地址值。
现在用gcc test.0 main.o将它们链接到一起。导出a.out的符号表

//a.out的符号表

Symbol table '.dynsym' contains 6 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     5: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)

Symbol table '.symtab' contains 66 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
    ......
    31: 00000000000005f0     0 FUNC    LOCAL  DEFAULT   13 frame_dummy
    32: 0000000000200df0     0 OBJECT  LOCAL  DEFAULT   18 __frame_dummy_init_array_
    33: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS test.c
    34: 0000000000201018     4 OBJECT  LOCAL  DEFAULT   23 a
    35: 00000000000005fa     7 FUNC    LOCAL  DEFAULT   13 test1
    36: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
    37: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    38: 0000000000000834     0 OBJECT  LOCAL  DEFAULT   17 __FRAME_END__
    49: 0000000000201010     0 NOTYPE  GLOBAL DEFAULT   22 _edata
    50: 0000000000000694     0 FUNC    GLOBAL DEFAULT   14 _fini
    51: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    52: 0000000000201000     0 NOTYPE  GLOBAL DEFAULT   22 __data_start
    53: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    54: 0000000000201008     0 OBJECT  GLOBAL HIDDEN    22 __dso_handle
    55: 00000000000006a0     4 OBJECT  GLOBAL DEFAULT   15 _IO_stdin_used
    56: 0000000000000620   101 FUNC    GLOBAL DEFAULT   13 __libc_csu_init
    57: 0000000000201020     0 NOTYPE  GLOBAL DEFAULT   23 _end
    58: 00000000000004f0    43 FUNC    GLOBAL DEFAULT   13 _start
    59: 0000000000201010     0 NOTYPE  GLOBAL DEFAULT   23 __bss_start
    60: 0000000000000608    21 FUNC    GLOBAL DEFAULT   13 main
    61: 0000000000000601     7 FUNC    GLOBAL DEFAULT   13 test2 //重点关注
    62: 0000000000201010     0 OBJECT  GLOBAL HIDDEN    22 __TMC_END__

这里看到有两个符号表,一个是.dynsym,一个是.symtab。这里简单介绍一下,.dynsym是动态符号表,是动态链接的时候用到的,而.symtab是静态符号表,金泰链接时用到的。

那在Unity生成的符号表是啥格式的呢?

通过上面我们知道了,我们要把二进制的那串信息解析成我们能看到的,需要用到符号表来对应起来,那我们就开始去Unity里看看这个小可爱藏在哪里的。

Android 的符号表

官方库的符号表
除了上面去找直接到unity的安装路径下找符号表,我们还能在打包之后在Temp\StagingArea\libs[architecture]路径下的。
我们还可以通过打包选项选择Create symbols.zip之后,打完包的符号表会生成一个.zip文件放在apk同级路径下。

IOS的符号表

当我们在xcode中使用Archive来编译的时候,会生成一个.xcarchive文件,这个文件可以在window页签里点击organizer这个选项看到。然后再.xcarchive文件右键显示包内内容,我们就能看到一个.dsym文件。这个就是我们需要的ios的符号表。

人工找太麻烦了

有些时候我们在使用第三方的bug记录工具的时候,需要上传这个符号表,通过堆栈的解析,我们就可以在第三方的工具上看到错误的类和函数信息。但是如果每次我们都通过手动找到文件,然后去上传。且不说我们上传的版本是不是和我们发布的版本一样,每次都要靠人工去操作,这是对人力的一个极大的浪费。这个时候,我们就可以通过Jenkins这样的流水线的工具来自动搞定它。对于androi的符号表我们还好操作,毕竟它出包了以后,会放在一个固定的路径下。但是ios你就能发现,通过直接生成的.xcarchive文件是放在一个默认的文件夹下,它的名字是根据时间来变化的。年月日时分秒都上了,这如果我们需要匹配到文件名那得循环遍历了,虽然能够解决,但是处理方法及其的不优雅。那我们是不是可以直接把.xcarchive文件生成到我们指定的路径下呢?稍微一查,嘿嘿,我找到了,可以用过官方的编译命令行来处理这件事xcodebuild -exportArchive 这个命令指定.xarchive的生成路径,那后面我们直接拼接固定路径就能找到.dsym文件了。

让我们去定位问题吧

我也不会解析符号,都是通过第三方的工具来解析,网上一堆,就不说了。右手问题,左手是字典,这翻译很快了吧。

怎么会找不到方法

通过翻译之后的堆栈,我们定位到问题是找不到一个静态方法,通过对比代码,我们发现这块用了反射,而且在mono环境下却不会出现。难道是因为打包的姿势不对?那我们去看看IL2CPP究竟是个什么样的可爱玩意。

IL

来我们来进行一波说文解字,头两个单词是IL,那我们去查下IL是个啥东西。通过微软官方的解释,IL是微软出品的一种基于.NET平台的中间语言。那我们来讲讲为啥需要这个中间语言。因为机器能认识的语言是汇编语言,这个语言太晦涩难懂了,入个门都要好长时间,对于当代这么快节奏的社会来说,平常人根本不能忍受,所以这种苦难还是那些牛人去体验吧。所以微软就出了一个稍微能让人看懂的语言IL语言,它比汇编要好认一点,最起码有一些高级特性,泛型呀,类呀方法呀,继承,字符串等等都能认识。那我这种垃圾码农就很容易写出我想要的代码。然后通过解释器,把IL转换成汇编,让CPU去跑吧,反正我才不管你汇编语言是啥玩意,也才不管你机器的CPU架构是啥玩意。某一天,我觉得IL语言也写的很不爽,不自由了。这个时候微软掏出了高级语言(c#,vb,F#),它帮我们把很多需要关注的点(内存管理等等)给管理起来的,我们就可以只去写逻辑(这个时候大部分人就可以进化成逻辑狗),去关注算法怎么实现,不去管理内存究竟是怎么管理的,不需要知道内存怎么回收的,反正高级语言都给你安排的明明白白了。

IL2CPP

那我们知道了IL是什么,那我们来看看IL2CPP是干啥吃的,它是把我们的IL语言编译成机器能跑的机器码。可能有人会问了,上面讲了IL就是会被转换成汇编呀,那现在这步和上面那个步骤有啥区别吗?其实也没事区别上面讲的是一张实时的编译,就是在运行的时候在把IL编译成机器码,而IL2CPP就直接在离线的时候把IL编译成机器码,一个是离线操作,一个是实时操作的。那我们知道了离线操作,和实时操作,那我们来看IL2CPP是怎么把我们的IL编程机器码了。

它通过了一个叫il2cpp.exe这个离线编译器把IL编译成c++的语言,然后再去生成对应平台的机器码。c++那被优化的可太好了,各个平台的c++编译器都优化到极致了,所以相当于站在巨人的肩膀上,歇一口气,我们很多事都可以少做了,直接用现成的。

那丢方法的原因在哪里呢?

通过上面我们了解到,我们的代码是通过AOT来编译的,那在AOT编译的时候,对于那种没有直接引用的方法或者类(比如反射等),它不知道要不要生成机器码,毕竟离线的也就是做一次扫描代码的活,又不是真的跑了一遍代码。所以当它觉得你没有用,它就会通过代码托管剥离来删除它觉得你没有用到的代码。你说它为啥还自作主张去删减代码呀,这不是吃饱了撑的。其实它这样做也是为了能加快我们打包的效率,毕竟代码少了转换成c++的时间也就变少了,构建机器码也就变快了。那我们能做什么操作来明确告知它我们这个代码是要用的,你不能删除。

我告诉你这块代码我要用,上帝来了也不能动

那我明确使用这个方法,扫描的时候,就可以知道这个代码块被引用了,不能被删除了,还有一个通过一个叫Link.xml的文件配置不能被删除的代码块。

结束语

也不知道这样的写的风格大家认不认可。快给一点建议呀!

【Unity】关于unity中IL2CPP遇到的一些情况

前言

在很早之间因为ios和android需要支持arm64位的时候,我就已经在打包的时候接触到了IL2CPP,但是那个时候也就只是为了能打出一个arm64位的包,所以对于它的一些原理都没有去了解过,碰到的一些问题也没有去详细解答,用了最蠢的方式来规避问题,刚好最近也用到了IL2CPP所以在这里记录下,最近对它的一些理解

IL2CPP的前世

首先我们需要知道Unity的跨平台的一些知识点,为啥Unity出的包能在ios和android这两个平台上跑,而我们不需要做啥特殊的操作,只需要在选择出包平台的时候,选对应的打包平台。这是因为在unity会把我们所编写的c#翻译成CIL(公共中间语言),然后再汇编成字节码。

CIL

CIL是一种属于通用语言框架和.NET架构的低阶的人类可读的编程语言,它是有微软公司所提供的。目标是把.NET框架的语言(c#,VB等等)被编译成CIL然后在汇编成字节码。CIL类似一个面向对象的汇编语言,并且它是完全属于堆栈的。它运行在虚拟机上。是一种独立于具体CPU和平台的指令集。然后由JIT编译器转换为特定的计算机环境特定的机械码。这个是在执行CIL之前完成的。它的转换的根据需求转换的,并不是编译整个CIL代码。

上图展示了CIL的创建和转换为机械码的过程。

  • 源代码在CLR的编译时由特定的语言编译器转换为CIL,并且元数据也在这个时候生成。元数据包含代码中类型的定义和签名,运行时信息等内容。
  • CIL是一个用于安全,部署,版本控制等编译代码库,它有两种类型,进程程序集(EXE)和库程序集(DLL)。
  • JIT编译器将CIL转换为特定与JIT编译器运行的计算机环境的机械代码。按需转换。
  • JIT编译器转换的机器代码由计算机处理器执行。

这块内容其实我也没有详细去研究(虽然我买了这本书,但是包装壳还没有拆,等我看完再水一篇)。但是看完上面的这些基础的信息,我们应该对于C#的跨平台有个简单的了解,它通过CIL这个中间语言来打包成程序集,然后再通过JIT来转换到各个平台所能跑的机械码。所以跨平台的重要点就是CIL和解释CIL的编译器。

关键点

但是在ios平台上机器码被禁止映射到内存中,就是不能把JIT实时编译出来的机械码放在一个地方,所以就不能实时在平台上跑CIL的程序集。
所以在这个时候,我们就需要提前做好机器码的编译(AOT)。所以在这个时候,我们打包成功以后,我们在包里的代码就已经变成了机器码了。

Unity中的IL2CPP干了啥

AOT编译器

IL2CPP AOT编译器名为il2cpp.exe。它是由c#编写的受托管的可执行程序,它接受我们在Unity中通过Mono编译器生成的CIL,并把它生成指定平台下的c++代码。
它的工具链如下:

运行时库

它还支持虚拟机的运行时库。几乎都是用c++代码实现的这个库。运行时库的名字叫ilbil2cpp,它作为链接到播放器可执行文件的静态库提供。这个是IL2CPP技术的主要优势,简单且可移植的运行时库。它还有一个关键部分就是垃圾收集器(GC)。使用的是libgc一种BoehmDemers-Weiser垃圾收集器。但是libil2cpp的设计允许我们使用其他垃圾收集器(GC)。

为啥转成CPP呢

  1. 运行效率快:官方实验数据,换成IL2CPP以后,程序运行效率有了1.5-2.0倍的提升。

  2. Mono VM在各个平台移植,维护非常耗时,有时候不可能完成。

  3. 可以利用现成的在各个平台的c++编译器对代码进行编译期优化,这样可以进一步减少最终游戏的尺寸并提高游戏运行速度。

  4. 由于去除了IL加载和动态解析的工作,使得IL2CPP VM可以做的很小,并且使游戏载入时间缩短。

脚本限制

System.Reflection.Emit

AOT平台无法实现System.Reflection.Emit命名空间中的任何方法。System.Reflection的其余部分是可接收的。只要编译器可以推断反射使用的代码需要在运行时存在。

序列化

AOT平台可能会由于使用了反射而遇到序列化和反序列化问题。如果仅通过反射讲某个类型或方法作为序列化或反序列化的一部分使用,则AOT编译器无法检测到需要为该类型或方法生成代码。

通用虚拟方法

如果使用泛型方法,编译器必须做一些额外的工作,才能将编写的代码扩展到设备上执行的代码。如果使用虚拟方法,将运行时而不是编译时确定行为,存在虚拟方法时,编译器可在不完成明显的地方轻松要求从源代码生成运行时代码。

从原生代码调用托管代码

需要编组到c函数指针以便可以从原生代码调用的托管方法会在AOT平台上有一些限制:

  • 托管方法必须是静态方法。
  • 托管方法必须具有[MonoPInvokeCallback]属性

    线程

有些平台不支持使用线程,因此任何使用System.Threading命名空间的托管代码都将在运行时失败。此外..NET类库的某些部分存在对线程的隐私依赖。

异常过滤

IL2CPP不支持c#异常过滤器。应该将依赖于异常过滤器的代码修改为正确的catch块。

TypedReference

IL2CPP不支持System.TypedReference类型和__makerefc#关键字

MarshalAs 和 FieldOffset 属性

IL2CPP 不支持在运行时反射 MarhsalAs 和 FieldOffset 属性。它在编译时支持这些属性。应正确使用它们以进行正确的平台调用编组。

动态关键字

IL2CPP 不支持 C# dynamic 关键字。此关键字需要 JIT 编译,而 IL2CPP 无法实现。

代码托管剥离

托管代码剥离将从构建中删除未使用的代码,从而可以显著减小最终构建大小。使用 IL2CPP 脚本后端时,托管代码剥离还可以减少构建时间,因为需要转换为 C++ 并进行编译的代码减少。托管代码剥离将从托管程序集(包括从项目中的 C# 脚本构建的程序集、包含在包和插件中的程序集以及 .NET 框架中的程序集)中删除代码。

托管代码剥离的工作方式是对项目中的代码进行静态分析,检测出在执行过程中永远无法访问的类、类成员甚至函数的某些部分。可以通过 Player Settings 窗口中的 Managed Stripping Level 设置(在 Optimization 部分)来控制 Unity 删除无法访问的代码的激进程度。

重要信息:当代码(或插件中的代码)使用反射来动态查找类或成员时,代码剥离工具不能总是检测出项目是否正在使用这些类或成员,因此可能会删除它们。要声明某个项目正在使用这样的代码,请使用 link.xml 文件或 Preserve 属性。

Unity代码的托管剥离级别设置

使用项目的 Player Settings 中的 Managed Stripping Level 选项来控制 Unity 删除未使用代码的激进程度。

C#所引用的代码剥离

UnityLinker

Unity 构建过程使用一个名为 UnityLinker 的工具来剥离托管代码。UnityLinker 是 Mono IL Linker 的一个定制版本,专为 Unity 设计。UnityLinker 基于我们的项目分叉,此分叉密切跟踪上游 IL Linker 项目。(请注意,该分叉中未维护 UnityLinker 的 Unity 引擎特有自定义部分。)

UnityLinker 的工作方式

UnityLinker 将分析项目中的所有程序集。首先标记顶级、根类型、方法、属性、字段等,例如,向场景中的游戏对象添加的 MonoBehaviour 派生类便是根类型。然后,UnityLinker 分析已标记为要进行识别的根,并标记这些根所依赖的托管代码。完成此静态分析后,所有剩余的未标记代码都无法通过应用程序代码中的任何执行路径来访问,并将从程序集中删除。

请注意,这一过程不会对代码进行混淆处理。

反射和代码剥离

UnityLinker 不能总是检测出项目中的代码通过反射时来引用其他代码的实例,因此可能会误删除实际在使用的代码。将 Managed Stripping Level 设置从 Low 提升为 High 时,代码剥离导致游戏中发生意外行为变化的风险也会增加。这种行为变化小到细微的逻辑变化,大到调用缺失方法造成的崩溃。

UnityLinker 能够检测和处理一些反射模式。如需查看该工具可以处理的最新模式的示例。但是,如果使用的不仅仅是简单的反射,必须给 UnityLinker 一些提示,说明哪些类不应该被处理。可以通过 link.xml 文件和 Preserve 属性的形式提供这些提示:

Preserve 属性 — 直接在源代码中标记要保留的元素。
link.xml 文件 — 声明应如何保留程序集中的元素。

UnityLinker 在分析程序集中未使用的代码时,会将使用属性或 link.xml 文件保留的每个元素视为根元素。

Preserve 属性

在源代码中使用 [Preserve] 属性可防止 UnityLinker 剥离该代码如下属性:

  • Assembly:保留程序集内的所有类型(就好像您为每个类型输入了 [Preserve] 属性一样)。要为程序集分配 Preserve 属性,请将该属性声明放在程序集包含的任何 C# 文件中,但需在所有命名空间声明之外

  • Type:保留类型及其默认构造函数。

  • Method:保留方法、其声明类型、返回类型及其所有参数的类型。

  • Property:保留属性、其声明类型、值类型、getter 方法以及 setter 方法。

  • Field:保留字段、其声明类型和字段类型。

  • Event:保留事件、其声明类型、返回类型、add 方法以及 remove 方法。

  • Delegate:保留委派类型及其所有方法。

请注意,相比使用 Preserve 属性,在 link.xml 文件中标记代码实体可以提供更强的控制。

托管堆栈跟踪

使用发布版本配置时,IL2CPP 可能会生成缺少一个或多个托管方法的调用堆栈。这是因为 C++ 编译器已经内联了缺少的方法。方法内联通常对运行时的性能有好处,但可能会使调用堆栈更难理解。IL2CPP 始终在调用堆栈上提供至少一个托管方法。此方法便是发生异常的方法。调用堆栈上还包括其他未内联的方法。IL2CPP调用堆栈不包含源代码的行号信息,我们需要通过一个叫符号表的文件来对应起来

符号表

其实吧这个东西我也不太清楚是为了什么,网上查到的资料说,这个东西就用来保存函数地址映射信息的中转文件。当程序崩溃的时候,会生成一份崩溃日志,这个日志里所记载的东西就是app出错的函数内存地址,而这些函数地址是可以在符号表中找到具体的文件名,函数名和行号信息。每个app在导出的时候都会生成对应的符号表。

总结

现在我就说说我因为IL2CPP所遇到的一些问题

  1. 在之前使用pb做配置二进制序列化的时候,因为在反序列化时候,使用了反射的功能,导致在IL2CPP中解析出错,那时候,我没有仔细去研究为啥会出现问题,因为这是第一次升级IL2CPP的版本,所以我很强制的和同事一起写了脚本,把反射的所有方法都给翻译出来,强行当c#脚本调用。

  2. 在bugly上看崩溃日志的时候,发现在IL2CPP版本中显示的堆栈信息是字符码,不像之前使用MONO一样显示的函数名这样的信息。那时候知道有符号码这样的东西,但是经常上传的符号码和奔溃的版本不匹配,导致解析还是有问题。

  3. 在含有第三方插件的工程导出的时候,因为少做了一个Link.xml的代码裁剪过滤,导致因为第三方库使用了反射功能使得在IL2CPP的包上出现错误。

[Unity]unity中的音频插件Wwise

基本类型

这个类型是按照projectExplorer中来划分的

  1. Audio(音频对象)

  2. Event(事件)

  3. SundBank

  4. GameSyncs

基本结构

Audio

声音对象(Actor-Mixer Hierarchy)

它可以是语音(Sound Voice),也可以是音效(Sound SFX)。每个声音对象都包含一个源。这个源在wwise中有两种:音频源和插件源。
Sound Voice:这个是用于对白
Sound SFX:这个是用于Actor-Mixer Hierarchy内所有其他声音
音频源:是一个独立层次,将导入的音频文件和声音对象分割开来。音频源链接到导入到工程中的音频文件。

分组

将不同的声音对象编为一组,就能统一设置整个组的属性和行为了。

  1. 用Container(容器)来按照特定行为(随机,顺序或者切换等)来播放一组对象。
  2. Actor-Mixer(角色混音器)通常用来设定一组的整体属性(音量,音高)。
    Container处于Actor-Mixer Hierarchy中的第二层级,可以说它们是父对象和子对象。可以使用容器来为声音和其他容器进行分组。通过在容器内“嵌套”容器,可以得到多样化的结果,模拟真实声音表现。
    Actor-Mixer它比容器高一个层次。也可以说,Actor-Mixer可以是容器的父对象,但反之不可以的。Actor-Mixer可以是任意数量的声音,容器以及其他Actor-Mixer的父对象。
容器

鉴于游戏中的音频需要按照不同行为播放,可以选择不同的Container类型:Random(随机),Sequence(序列),Switch(切换)和Blend(混合)。

  1. 随机播放容器:可以进行标准随机选择,也可以进行洗牌选择,还能为随机容器内的每个对象指派一个权重值。
  2. 随机和序列播放都包含多个对象,所以必须得指定播放模式
    • Step-每次播放容器时,仅播放容器内的一个对象。
    • Continuous-每次播放容器时,会完整播放容器内的所有对象。
  3. 由于可能有多个游戏对象使用同一容器,因此需要决定游戏中一个容器的所有实例是作为一个对象处理,还是单个实例单独处理。
    • Global全局-将游戏内使用的所有该容器作为一个对象进行处理,因此可以针对所有游戏对象避免重复声音或者语音对象。
    • Game object游戏对象-将该容器的各个实例最为单独实体进行处理。即不会在所有游戏对象间共用声音内容。
  4. 在Switch Container中Switch或State和RTPC(实时参数控制)被用来代表各个不同条件。每个切换Switch/State/RTPC对应于与该条件有关的声音对象将会播放。
  5. 在Switch Container中切换变化的时候可以选择播放行为
    • Play-决定是每次切换被触发时都重新播放对象,还是仅当触发的Switch/State发生变化时才重新播放。
    • Across Switches-对于被指派至多个切换开关的同一对象,当触发新的Switch/State时是否继续播放。
    • Fade In-触发的新的Switch/State时,新对象是否应用淡入。
    • Fade Out-触发的新的Switch/State时,已有对象是否应用淡出。
  6. 在混合容器里可以添加:Blend Container,Random Container,Sequence Containers,声音,switch Container,Voices(语音)
  7. 在混合容器里可以同时播放多个对象。在Blend Track中启用交叉淡变会改变这些对象的听感和振动。
  8. 在使用交叉淡变过度时,需要注意以下:
    • 音频文件长度必须大于或等于0.2秒,交叉变淡时间最短为0.1秒。
    • 从声音A交叉变淡至声音B时,声音引擎所允许的淡变时间最长为音频文件A长度的一半。如果设置的值超过了,会自动调整到最大值。
    • 如果使用RTPC设置容器的音高值,或者在播放的时候触发了Set Pitch事件动作,则对声音应用交叉变淡时可能会产生意外结果。
    • 对源插件应用交叉淡变时,无法确定源的结束时间,那么淡变可能会被忽略。
    • 当Switch Container作为Sequence Container的子容器时,会根据指派给切换开关的对象数量,区别应用交叉淡变过度。
    • 交叉淡变期间,声音引擎会使用两个不同的声部。
    • 在低于音量阈值或超过播放数量限制时,Play from Beginning和Resume虚声部行为会影响声音持续时间,这不在交叉淡变时间机制考虑范围之内。
    • 当声部的音量低于阈值时,声部会变成虚声部。对于任何声音,会使用其所有音频通道的实际有效音量与阈值相比较。这个有效音量包括Actor-Mixer Hierarchy,淡变过度,互动音乐过渡,RTPC,状态,定位和衰减等所有的音量影响。
  9. 同时播放的流越多,要求的Pre-fetch(预取时间)越多。
  10. 在使用“Global”来避免声音重复,但是如果容器包含的声音数量有限,并且同时要播放很多该容器,可能会出现重复问题。所以我们要确保容器内的声音对象至少为播放实例数量的两倍。
  11. 在使用Blend Container播放的时候,因为该容器内的所有对象都会同时播放,因此会消耗大量内存,可以使用虚声部设置来降低CPU占用,但是如果频繁地进行短暂的交叉变淡,则可能会导致出现错误。
  12. Switch Container和Blend Container均可配合RTPC使用,并会产生相似的效果。如果希望在不同时间播放不同的对象,并且不需要交叉变淡,那就可以使用switch Container。如果希望一直播放所有对象,并且需要交叉变淡那就可以使用Blend Container。

输出总线(Master-Mixer Hierarchy)

总线是一种用来对工程中对象进行分组的机制,目的是管理混音和最终声音输出。可以为工程中的通路定义相对属性,状态,RTPC以及效果器。
这里有三种类型:

  1. Master Audio Bus(主音频总线):它决定了音频的最终输出。
  2. Audio Bus(音频总线):可分组在Master Audio Bus之下,以有助于组织和提供混音。可以在总线上加效果器。
  3. Auxiliary Bus(辅助总线):可分组到任何辅助总线或音频总线之下。可以对辅助总线应用效果器。在辅助总线中无法做闪避,HDR混音和声部调节。
    Actor-Mixer Hierarchy的声音会被默认连通到Master Audio Bus,但是可以在修改到我们自己建立的总线上去。
定义总线的相对属性

相对属性是积累的,总线的属性值会累加到其下的子对象中

  1. Bus Volume-总线音量,用于直接调整总线的音量大小。
  2. Voice Volume-声部音量,总线中正在播放的音频对象所应用的衰减。
  3. Voice Pitch-声部音高,总线中正在播放的音频对象的播放速度。
闪避信号

在游戏的不同时刻,希望某些对象比其它对象更加突出。可以修改以下属性和行为来控制信号闪避方式:

  1. Ducking volume(闪避音量)
  2. Fade out(淡出)
  3. Fade in(淡入)
  4. Curve shape(曲线形状)
  5. Maximum ducking volume(最大闪避音量)
    总线无法要求自己或其直接父总线闪避

定位

  • 局部环境音-发声体保持在一个位置上。
  • 非局部环境声音-发声体可移动,并不依附于某个特定游戏对象。
  • 移动对象声音-发声体随着某个特定游戏对象一起移动。
  • 游戏界面声音-此声音与特定游戏界面要素或者其它保持在屏幕固定位置上的道具相关联。

声音和音乐对象选择是否3D空间化的区别在于如何将源声道映射至输出扬声器。不空间化,那么输出声道将于源声道完全匹配。空间化,则可将各个输入声道输出至环绕声环境中的任何扬声器。在运行中可通过Positioning Type的RTPC更改定位方法。无论是否使用空间化,都提供衰减功能,即模拟音频信号在发声体原理或背离听者时的自然衰减。
虚发声体:
为了计算源对各个扬声器的声道贡献大小,“虚发声体”以半圆形状环绕听者设置。半圆的大小取决于散布(100%代表整圆,0%代表听者正前方的点)。然后将半圆划分为与声道数量相同的,大小相等的区域,源的各个原声声道都作用于指定的区域。一旦完成,将计算各个虚发声体对各个扬声器的实际贡献。

空间定位

空间化确定游戏3D环境中对象的实际位置或定位。
两种空间化:

  • Position(位置):仅使用游戏对象的三维定位信息。
  • Position+orientation(位置+朝向):同时使用游戏对象的三维定位和朝向信息。
    三种定位类型:
  • Emitter(发声体):由实时游戏位置数据来定义“发声体”游戏对象的空间定位。在选择Emitter时,WWise中所定义的传播属性将直接关联至游戏中听者和发声体的位置或朝向。
  • Emitter with Automation(发声体自动化):通过在Wwise中创建路径来定义“发声体”游戏对象的空间定位。定义忽略游戏对象位置和衰减的一组固定的传播行为。
  • Listener with Automation(听者自动化):通过在Wwise中创建路径来定义“听者”游戏对象的空间定位。定义忽略游戏对象位置和衰减的一组固定的传播行为。

Attenuation(衰减)

除了空间化外,还可以定义对象的衰减设置。衰减设置用于模拟当信号原理听者时的自然衰减。
衰减基于以下两个属性:

  • Distance attenuation(距离衰减):根据发声体和听者之间的距离来影响信号的强度。
  • Cone Attenuation(声椎衰减):根据发声体相对于听者的朝向来影响信号的强度。
    距离衰减使用一系列曲线进行定义。这些曲线将距离值映射到音量等Wwise属性值。通过定义曲线各个点的属性,可以在对象远离听者时控制对象的音量衰减。
    锥形衰减使用一系列夹角进行定义。这些夹角用于定义声源前方,侧方和背后的区域。通过定义声源周围的这些不同区域,可以更具游戏中的朝向来模拟对象的衰减。
    Attenuation ShareSet

当游戏中的许多对象拥有相同的衰减属性,因此可以先创建一份衰减,然后使用Attenuation ShareSet在工程中的许多对象之间共享。

定义各种对象属性的衰减曲线

可以为以下Wwise属性创建衰减曲线:

  • Output Bus Volume-输出总线音量。连接到音频输出总线的信号的衰减或振幅。
  • Auxiliary Send Volumes-发送到游戏定义和用户定义的Auxiliary Bus的信号的衰减或振幅。
  • Low-Pass filter-根据指定值来衰减高频的递归滤波器。低通滤波器的单位代表已经应用的低通滤波比例,0表示无低通滤波,100代表最大衰减。
  • High-pass filter-高通滤波器。根据指定值来衰减低频的递归滤波器。高通滤波器的单位代表已应用的高通滤波比例,其中0代表无高通滤波,100代表最大衰减。
  • Spread-散布。扩散到附近扬声器的音频量或百分比,以使声音能够随着距离的增加从低扩散的点声源变为完全扩散的传播源。值0表示某扬声器附件的发声体的所有声道只通过该扬声器播放。值100表示将扩散声源的声道,以便通过所有扬声器都能够听到或感觉得到。
  • Focus-聚焦。百分比值,用于收缩由扩散值生成的虚拟发声器。焦距0%表示虚发声器保持不变,值越高,各个虚发声器越靠近源声道的原点。
使用锥形边界模拟方向性

在现实中声音通常具有一定的方向性。在wwise中模拟声音的方向可以使用声锥。声锥使用不同的夹角模拟声音在特定方向上的传播。当听者一道这些夹角外卖时,Output Bus Volume将衰减。
以下角定义声锥的区域:

  • Inner angle:内角。此夹角定义的区域中输出总线音量不衰减,也没有低通滤波效果发生。
  • Outer angle:外角。此夹角定义的区域中输出总线音量发生衰减,低通滤波效果保持在最高水平。
    Output Bus Volume在过度区域中发生滚降。在无衰减发生的内角边界和达到最大衰减值的外角边界之间,使用线性插值法对音量进行衰减。在外角定义的区域中,音量衰减始终等于最大衰减值。
Speaker Panning和3D Spatialization交叉淡变

当我们需要做一种从声像摆位的环境过渡到需要空间化的环境。好比玩家在一个房间里听音乐,然后从房间走到户外,户外在播放环境音的情况。在做交叉淡变时,必须同时计算两种定位类型,因此消耗的运行时CPU资源也比只选择一种类型时更多一些。

Spread的影响
Spread的衰减曲线可以根据距离来做修改。这个距离是发声体和听者之间的距离长度。

朝向的影响

受到两种不同朝向因素的影响:

  • 发声体相对位置
  • 发声体相对朝向
    发声体的朝向仅影响多声道声源。

Focus

Focus(聚焦)参数能够在应用旋转和扩散之前,将每个输入声道的虚声源集中在一起。在对离散多声道文件进行摆位时,Focus参数用于减少声道信号在其他输出通道中的泄漏。但是不适用于Ambisonics声像摆位,因为其设计即包含输出声道间的泄漏,尽管是均匀泄漏。因此,当声源的声道配置是Ambisonic时,将会忽略Focus值。

斜面声源在平面声道配置中的摆位

虚声源坐标在扬声器平面上的投影,用于计算摆位。
听者的旋转会影响到:

  • 发声体声场的相对朝向;
  • 点声源的入射角。

Height Spread的效果

在典型的Distance Spread曲线设计中,声源距离听者越远,使用的Spread值越小(反之亦然)。这样的话,在距离较近时,声源会像个圆;在距离较远时,就会像个点。在声源靠近时,其射入方向会突然改变,听起来可能不太自然。
在声源位于听者上方或者下方很远的位置并被摆位到2D声道上时,也会出现类似的失真。在距离较远的情况下,Spread曲线会估算出一个较小的值;这时声源若穿过听者所在平面上的路径,其声像就会从一侧突然摆位到另一侧。对此可在对象从听者上方或下方穿过时,借助四周的扬声器来对声音进行散布处理,以此在第三维度上没有扬声器的情况下传达高度的变化。

使用动画路径定义空间定位

设置预定义位置时,无论听者在游戏中的位置和朝向如何都会遵循以下情况:

  • 声音将总是通过相同的扬声器播放。
  • 通过相同的电机感受到振动。
    使用动画路径定义定位信息。动画路径由每次定义源位置的若干个控制点构成。如果是创建了多个点后,对象则将随着时间沿着路径进行运动。

优先级

在游戏中,可以同时播放很多对象,对象的数量甚至有可能超出由项目团队设置的数量上限。为了有效管理播放对象的数量,必须规定同时最多可以播放多少个对象,已经哪些对象会被优先播放。
三个属性可以确定在游戏中将同时播放哪些对象:

  • Playback limit-限制同时运行播放的对象数量(不包含虚声部)
  • Playback priority-一个对象相对于另一个对象的重要性。
  • Volume threshold-低于这一特定音量的对象将不会播放。
    声音引擎的各个内存池设置内存阈值,当启动之后,声音引擎会定期检查所用内存百分比是否低于指定的阈值。如果超过了,将会忽略优先级比较低的声音,用来播放优先级比较高的声音。
    当音量降低至音量阈值,或当声音的数量超出Playback Limit上限时,会为对象执行以下操作:
  • 继续播放
  • 终止
  • 移至虚声部列表
    虚声部列表是一种虚拟环境,在这个环境中,声音引擎会监视列表里的声音的特定参数,但不会执行声音处理。

Virtual Voices:

在决定使用哪个虚声部的设定之前,需要了解下它们的内存占用和CPU占用

  • Play from beginning-该选项仅使用少量内存和CPU,但如果对声音进行了流播放,则从虚声部返回时可能存在延迟。
  • Play from elapsed time-改选项可以节省部分CPU和内存,但如果流播放声音,则声音从虚声部返回的时候可能出现延迟。
  • Resume-改选项使用较少的CPU,但会占用较大的内存,因为当声音从虚声部返回时将会保留内存缓冲区。
    通过以下方式来管理工程内同时播放的各种对象:
  • 限制每个游戏对象可播放的实例数量,或对于Actor-Mixer Hierarchy\Interactive Music Hierarchy 对象及值对象,在全局范围内限制其可以播放的实例数量。
  • 限制可通过特定总线的对象;
  • 限制整个游戏中的对象总数。
    当在Actor-Mixer 或者Interactive Music层级设置了播放数量限制,如果子对象忽略了父级对象的Playback Limit那么子对象的数量不记录在父对象限制里;可播放的实例数量是它们的总和。
    由于各个对象的优先级已经在角色混音器或者Interactive Music层上进行了指定,因此总线上不存在播放优先级设置。

Event

Wwise使用Event(事件)来驱动游戏中的声音,音乐,对话和振动。由此提供了两种Event

  • 动作Event
  • Dialogue events(对白事件)
    动作事件包含若干个动作(Action),这些动作将指定Wwise对象是否播放,暂停,停止等。
    Event中的每个动作都有相应的作用范围设定。作用范围确定Evnet动作是应用于全局所有GameObject还是触发该事件的特定GameObject。全局的GameObject的话相当于应对的是总线。
    在创建动作事件后,这些事件可通过打包成SoundBank集成到游戏引擎中,在相应的条件下调用这些事件。由于游戏引擎使用名称或ID去使用,所以在创建完事件之后,就可以集成到游戏中去,之后就算进行事件内容的微调,只要名称和ID没有更改,就不需要额外的编程。

将游戏事件和Wwise中的事件进行匹配

使用SoundBank定义文件来跟踪哪些时间已经集成到游戏中,确实哪些事件以及还需要在Wwise中创建哪些事件。

以编程方式停止,暂停和恢复声音

事件信息存储在声音引擎的默认内存池中。为了避免占用默认内存池中的过多空间,可以使用SDK中的ExecuteActionOnEvent()功能,以编程方式停止,暂停和恢复声音。

Game Object

Game Object(游戏对象)是Wwise中的核心概念,因为声音引擎中被触发的每个Event都与一个GameObject相关联。Game Object通常指的是游戏中能够发出声音的特定对象或元素。
Wwise为每个GameObject 存储了各种信息:

  • 与GameObject相关联的音频对象的属性偏置值,包括音量和音高。
  • 3D位置和朝向
  • Game Sync信息,包括State,switch和RTPC
  • 环境效果
  • 声障和声笼
    在使用GameObject之前,程序员需要在代码中注册GameObject。

Listener

Listener(听者)在游戏中代表话筒。Listener在游戏3D空间中拥有位置和朝向。在游戏期间,Listener的坐标与GameObject的位置进行比较,以便将与GameObject相关联的3D声音指定给相应的扬声器,来模拟3D环境。

GameSyncs

在完成初步的游戏设计后,您可以开始考虑如何使用被称为 Game Sync(游戏同步体)的 Wwise 元素来串接和处理互动音频中的变化和替换行为,这些行为也是游戏内容的一部分

State

使用State可以优化声音和音乐素材,允许为同样的声音灵活创建不同的“Mixing Snapshot”(混音快照),响应游戏中的变化并改变全局属性。通过改变声音或音乐对象的属性,无需添加新素材就可以创造性地匹配各种游戏场景。

使用State

State需要隶属于StateGroup(状态组),才能供Wwise对象使用。可以按逻辑将各种State划分StateGroup来简化管理。
在同一个StateGroup内各State之间能平滑过渡,可以定义状态过渡的时长。
State可以通过两种机制集成到游戏中。一种集成机制是调用含Set State动作的事件,另一种是调用State Group和State本身。

Switch

在使用层次结构组织对象之外,Wwise中的Switch可以简化声音,音乐和振动对象的组织。Switch代表游戏中特定元素的不同条件,可以用来管理这些条件下的相应对象。将特定条件下的对象指派到特定Switch,这样播放该游戏元素时,将播放当前switch对应的对象。
将Game Parameter值映射到Switch
可以使用Game Parameter的值来驱动Switch的切换,在映射之前需要创建并定义游戏参数。

RTPC

在游戏中实现一些动态效果,可能会需要将特定对象的属性与游戏中的某些参数值绑定。在Wwise中可以使用实时参数控制(RTPC)来实现这一点。使用曲线沿线上的一系列点创建RTPC。该曲线表示Game Parameter和Wwise中音频属性之间的关系。当游戏中的Game Parameter发生变化时,Wwise使用RTPC曲线来确定相应的属性值。
对于Wwise中的对象,总线,效果器,衰减和切换开关使用RTPC。
在将RTPC作用于现有属性值的时候,最终的属性值采用以下两种方式中的一种:

  • Absolute:将使用RTPC确定的值,忽略对象现有的属性值。
  • Relative:RTPC确定的值将与对象的现有属性值相加。
    绝对和相对设置为预先定义,无法更改。

Trigger

一种Wwise元素,跟其他Game Sync一样会被游戏调用。触发器会定义Wwise将做出怎样的特定响应来反应游戏中的情节变化。Trigger在响应游戏突发事件时,将播放Stinger(插播乐句)。Stinger是一种短乐句,它会与当前音乐叠加并混合播放,以音乐的形式来对游戏做出响应。

SoundBank

有两类Bank

  1. Initialization (Init) bank: 基础的init.bnk,它是初始化库。一种特殊的库,其中包含有关工程的所有通信信息,每次wwise生成SoundBank都会自动创建init.bnk。只需要在游戏开始前加载一次,就能得到游戏期间工程的所有通用信息。所以它必须得是最先加载的声音包。
  2. SoundBank:这个文件中同时包含事件数据,声音,音乐和振动结构数据或音频文件。它可以在需要使用内部信息的时候才加载出来,在不用的时候去卸载掉。提高平台内存的利用率。

内容

  1. Event data:事件数据
  2. structure data:声音,音乐或振动结构数据
  3. Media files:媒体文件

加载

  1. LoadBank
    此方法显式加载SoundBank中的所有内容,而不验证媒体文件是否已经加载到内存。这可能导致同一媒体文件被多次加载到内存中。
  2. PrepareBank
    用于加载SoundBank中的所有内容,此方法不是立即加载媒体文件,而是通过使用PrepareEvent()机制来将所有媒体加载到内存中。通过使用此机制加载媒体,Wwise首先查看媒体文件是否已经存在内存中,然后再加载它。这可以避免内存中出现媒体文件重复,从而将内存占用保持在最低水平。
  3. PrepareEvent,PrepareGameSync
    在动作事件被游戏调用前,使用PrepareEvent来prepare这些事件。prepare事件时将从文件系统中加载所有被引用的媒体文件,如果所有被引用的结构元数据尚未加载,则还将从SoundBank中加载这些结构元数据,当动作事件不再需要的时候,可以将它Unprepare(接触预备),相应媒体文件于是将从内存中被清除掉。这可以避免内存中出现重复文件。

注意

使用Prepare相关的加载方式会降低媒体加载到内存中的速度。读取时间延长是因为声音引擎需要搜索磁盘。

结论

Wwise的核心由五个主要组件构成的

  • Audio object(音频对象)

  • Event(事件)

  • Game Sync(游戏同步体)

  • Game Object(游戏对象)

  • Listener(听者)

Event 和 Game Sync 是 Wwise 和游戏中都不可分割的两个组件。这两个组件在游戏中负责驱动音频,在 Wwise 的音频素材和游戏中的组件之间架起必要的桥梁。

【系统】Unity3D中的AssetBundle资源管理器(终)

前言

水不动了,一篇小小的资源管理器已经水了好几篇了,太让人难堪了。但是也让我知道了,沉淀没有想象的那么容易,就在以为自己思考的很全面的时候,测试下来又会冒出很多问题。

正文

问题

这次遇到的问题是在以下情况下出现的,上次我们用的异步做资源加载,但有一种情况我没有考虑到(也是我实际使用时碰到的一种情况)我们需要给一个 sprite 赋值一张图片,如果是在异步的情况下,这个资源回调还没有加载回来时,又因为刷新问题需要给这个 sprite 重新赋值一张图片,如果这时旧的图片在新的图片前面加载出来,那么最终显示的图片就是我们不想要的旧图片了。这也是异步的加载的一个缺陷,我们不能保证加载回来的顺序。

解决办法

我会在加载的时候给加载的接口一个当前需要赋值的对象的索引 id —— 这个索引 id 是唯一的。将这个 id 传到资源管理器里做一次判断,如果这个唯一索引已经对应上了一个 assetname 的话,就把之前记录下来的 assetname 所对应的 asset 卸载,然后在资源管理器里和需要加载的 asset 做一次绑定。这样的话,就能保证所加载的资源不会因为异步加载的顺序所产生一些显示错误。

更新

说明

之前在讲异步加载的时候,有一个特性我遗漏了,就是异步相当于开了一个线程池来同时加载所有的东西,这样在设备高速发展的今天,我们能高效的运用多核技术,使我们的游戏更流畅的运行。

技术更新

unity 的异步接口有个很有用的功能,就是如果你直接去获取 assetbundleCreaterequest 中的 assetbundle 属性 或者 assetbundleRequest 中的 asset 属性时,它在获取这个资源的时候,属于阻塞获取,除非 assetbundle,asset 加载回来,否则进程会一直阻塞在这里。但是我们说了,异步加载其实是线程池在跑的,所以这个阻塞不会影响到其他资源已经创建的加载。我们可以根据这个特性,来做一个同步接口,这个接口的好处就是,我们可以避免在打开 ui 界面的时候 因为资源没有加载 而实例化界面对象所产生的显示问题。这是这个特性的一种好的使用方案。

还有一种使用方案是,我们可以在界面打开的时候预加载一组 AssetbundleRequest 的对象,它是异步方法,所以当我们在界面打开的时候,它可能就已经加载了一部分了。打开的时候,我们可以直接去获取它的 asset,如果没有加载成功,它就会一直阻塞在这里,直到加载成功返回,这样做的好处是能让我们感觉界面打开的更流畅。

后语

这个资源管理器本来是为了沉淀自己的技术而做的一个技术文档,却没想到最后成了每周完成一篇blog的任务了。但是在写这篇文章的时候,我也感觉到了自己对于技术的把握还是差了一点,有些东西还是要多看、多学,很多很有用的特性都可能是自己一下子想不出来的。但是后期的文章,我将改变下方案,我会把周更变成月更,这样给自己更多的时间去对于相关的技术做一个查缺补漏。