确定所能支持的最大/最小性能指标,入允许的内存使用量、可接受的CPU消耗量、并发用户数量等,,确定目标受众,来确定运行游戏的硬件限制是什么,需要达到什么性能目标
U3D的Profiler中的GC ALLOC 项让人很麻烦,一直搞不清楚它是什么,因为 GC 是垃圾回收,而alloc是内存分配,那么 GC ALLOC 是 垃圾回收内存分配?
这个名字起的太TM烂了,其实这是U3D的不知哪个二货程序员起的,除了U3D中,其它任何文献中都没有这个名词。
GC_FOR_MALLOC 这是安卓中的名词,它表示 means that the GC was triggered because there wasn’t enough memory left on the heap to perform an allocation. Might be triggered when new objects are being created.,完全不是一个意思。
其实U3D的GC ALLOC就是 heap alloc,只要new了对象,就会有heap alloc
堆,英文是 heap,在内存管理的语境下,指的是动态分配内存的区域。这个堆跟数据结构里的堆不是一回事。这里的内存,被分配之后需要手工释放,否则,就会造成内存泄漏。
栈,英文是 stack,在内存管理的语境下,指的是函数调用过程中产生的本地变量和调用数据的区域。这个栈和数据结构里的栈高度相似,都满足“后进先出”(last-in-first-out 或 LIFO)。
从现代编程的角度来看,使用堆,或者说使用动态内存分配,是一件再自然不过的事情了。下面这样的代码,都会导致在堆上分配内存(并构造对象)。
// C++
auto ptr = new std::vector();
在堆上分配内存,有些语言可能使用new
这样的关键字,有些语言则是在对象的构造时隐式分配,不需要特殊关键字。不管哪种情况,程序通常需要牵涉到三个可能的内存管理器的操作:
- 让内存管理器分配一个某个大小的内存块
- 让内存管理器释放一个之前分配的内存块
- 让内存管理器进行垃圾收集操作,寻找不再使用的内存块并予以释放
C++ 通常会做上面的操作 1 和 2。Java 会做上面的操作 1 和 3。而 Python 会做上面的操作 1、2、3。这是语言的特性和实现方式决定的。
注意在图 1e 的状态下,内存管理器是满足不了长度大于 4 的内存分配要求的;而在图 1f 的状态,则长度小于等于 7 的单个内存要求都可以得到满足。
当然,这只是一个简单的示意,只是为了让你能够对这个过程有一个大概的感性认识。在不考虑垃圾收集的情况下,内存需要手工释放;在此过程中,内存可能有碎片化的情况。比如,在图 1d 的情况下,虽然总共剩余内存为 6,但却满足不了长度大于 4 的内存分配要求。
幸运的是,大部分软件开发人员都不需要担心这个问题。内存分配和释放的管理,是内存管理器的任务,一般情况下我们不需要介入。我们只需要正确地使用new
和delete
。每个new
出来的对象都应该用delete
来释放,就是这么简单。我们先来看一段示例代码,来说明 C++ 里函数调用、本地变量是如何使用栈的。当然,这一过程取决于计算机的实际架构,具体细节可能有所不同,但原理上都是相通的,都会使用一个后进先出的结构。
void foo(int n) { … }
void bar(int n) { int a = n + 1; foo(a); }
int main() { … bar(42); …}
在我们的示例中,栈是向上增长的。在包括 x86 在内的大部分计算机体系架构中,栈的增长方向是低地址,因而上方意味着低地址。任何一个函数,根据架构的约定,只能使用进入函数时栈指针向上部分的栈空间。当函数调用另外一个函数时,会把参数也压入栈里(我们此处忽略使用寄存器传递参数的情况),然后把下一行汇编指令的地址压入栈,并跳转到新的函数。
新的函数进入后,首先做一些必须的保存工作,然后会调整栈指针,分配出本地变量所需的空间,随后执行函数中的代码,并在执行完毕之后,根据调用者压入栈的地址,返回到调用者未执行的代码中继续执行。
注意到了没有,本地变量所需的内存就在栈上,跟函数执行所需的其他数据在一起。当函数执行完成之后,这些内存也就自然而然释放掉了。我们可以看到:
- 栈上的分配极为简单,移动一下栈指针而已。
- 栈上的释放也极为简单,函数执行结束时移动一下栈指针即可。
- 由于后进先出的执行过程,不可能出现内存碎片。
在比较大的项目中,启用Profiler时,有时会导致严重不一致的行为。可以通过Profiler查看性能分析
通常快速体验一遍程序运行,感觉性能变差时持续关注一段时间,并进行进一步分析,,,这叫做基准分析,,感兴趣的重要指标时渲染帧率、总体内存消耗和CPU活动的行为方式(寻找活动中较大的峰值)、CPU/GPU的温度,不应该接受通过Editor模式生成的基准数据作为真正游戏时的数据,因为Editor会带来额外的开销。应该在应用程序以独立格式在目标硬件上运行时,将分析工具挂接到应用程序中。
编辑器或独立运行的实例
其他连接到笔记本的设备或者通过远程方式,都可以使用这个方法进行性能分析,,远程需要输入IP
普通的分析只记录常见的Unity回调方法(如Awake、Start、Update、FixedUpdate)所返回的事件和内存分配信息。启用Deep Profile选项可以用更深层次的指令重新编译脚本,允许它统计每个调用的方法。这导致需要使用大量的内存,因为在运行时收集的是整个调用堆栈的数据,因此大型项目中,使用Deep Profiler甚至是不可能的。
注意:切换Deep Profiler需要完全重新编译整个项目,才能再次开始分析,因此最好避免在测试中来回切换该选项
void Update()
{
transform.GetChild(0).transform.Rotate(Vector3.up, 0.05f);
DoSomethingCompletelyStupid();
}
//循环100w添加到list,然后无操作
void DoSomethingCompletelyStupid()
{
Profiler.BeginSample("My Profiler Sample");
List<int> lt = new List<int>();
for (int i = 0; i < 1000000; i++)
{
lt.Add(i);
}
Profiler.EndSample();
}
作为语句
①用于定义一个范围,在此范围的末尾将释放对象。
using (FileStream fileStream = new FileStream(FilePath + @"\" + files[i].Name, FileMode.Create, FileAccess.Write))
{
//做一些操作
fileStream.Write(bytes, 0, bytes.Length);
}//关闭fileStream
(1)作用
在Using关键字后的括号内实例一些类,然后在下方的方法体内就可以直接调用实例好的类,这些类在Using外部是无法调用的,并且在Using方法体的执行完毕后,该类会被自动释放掉,注意,这个实例的类必须继承IDisposable接口,不然无法编译,释放的时候会自动调用类重写的Dispose方法。
(2)简单示例
首先创建一个继承自IDisposable接口的类,并添加一个开放的函数
using可以多引用,,括号里面实例多个类
public class Test1 : IDisposable {
public void PrintData(string str)
{
Debug.Log("--------------:"+str);
}
public void Dispose()
{
Debug.Log("释放Test1");
}
}
public class TestSave : MonoBehaviour {
void Start () {
using (Test1 test1 = new Test1())
{
test1.PrintData("This is a test1");
}
print("Using语句结束");
}
}
四个最常用的回调Awake(),Start(),Update(),FixedUpdate(),包括其他回调OnGUI等,,MonoBehavior在场景中的第一次实例化时,Unity会将任何定义好的回调添加到一个函数指针列表中,,,,即使函数体是空的,Unity也会挂接到这些回调中,而Unity没有办法意识到这些函数体可能是空的,它只知道方法已经定义,执行空的回调将浪费少量CPU
可以通过正则表达式regex查找回调是否为空
下图挂载30000个组件有无Update的测试结果
反复计算一个值时常见的错误,特别是使用GetComponent时,,最好不要把GetComponent赋值放到函数体中,最好在Awake中获取,,这样可以缓存组件引用,不用再每次需要的时候都重新获取,可以节省CPU开销
如果相同的计算结果,并且计算过程昂贵复杂,,,多个位置都调用了它,那么重构代码例如从字典或文件中读取数据分发给需要的对象,就比较明智
void Update()
{
ProcessAI();
}
//修改为:
private float _aiProcessDelay = 0.2f;
private float _timer = 0.0f;
void Update()
{
//通过计时器降低频率
_timer += Time.deltaTime;
if(_timer > _aiProcessDelay)
{
ProcessAI();
_timer -= _aiProcessDelay;
}
}
//修改为协程
void Start()
{
StartCoroutine(ProcessAICoroutine);
}
IEnumerator ProcessAICoroutine()
{
while(true)
{
ProcessAI();
yield return new WairForSeconds(_aiProcessDelay);
}
}
void Start()
{
InvokeRepeating("ProcessingAI", 0f, _aiProcessDelay);
}
GameObject执行空引用检查会导致一些不必要的性能开销。因为MonoBehavior和GameObject存储的内存空间的移动会产生开销(跨越本机-托管的桥接),许多微妙的方式都会触发这种额外的开销(开销很小,但是如果大量进行空引用检查,还是不妥)
if (gameObject != null)
DoSomething();
//修改为
if (!System.Object.ReferenceEquals(gameObject, null))
DoSomething();
GameObject中检索字符串属性例如name和tag也会产生跨越本机-托管的桥接,,,,也会产生开销
tag属性最常用与比较,使用CompareTag()可以避免上述问题
if(listOfObject[i].tag == "Player")
{}
//修改为
if(gameOjbect.CompareTag("Player"))
{}
GC.Collect是垃圾回收,清理无用的内存,,,,使用tag循环100w次,内存大了400mb,使用了1892ms,回收内存都花费了449ms,,,,,,compareTag就明显时间短并且没有垃圾回收的时间
遍历对象用list,,,,两个对象相互关联用Dictionary,,,, 需要同时处理两种情况:快速找出那个对象映射到另一个对象,同时还能遍历组。最好在列表和字典中存储数据,虽然需要额外的内存,维护多个数据结构。但迭代列表的好处和迭代字典形成鲜明对比。
unity会把同父元素的transform根据顺序存储再预先分配的内存缓冲区中,便于迭代,,,但是如果重新指定了父物体,如果与分配的内存空间不够容纳鑫子对象,就需要扩展缓冲区,这会导致开销,尽量避免。
transform组件只存储与父组件相关的数据,如果是绝对坐标的position、rotation等的修改就会导致大量未预料到的矩阵乘法计算,,因此使用localPosition、localRotation等成本较小。当然将数学计算从世界空间更改为本地空间,会把问题变复杂,因此牺牲一点性能也是值得的。,,不断更改Transform组件的属性也会向Collider等组件发送通知,通知组件进行处理。
FixedUpdate在Update之前执行,因此可以在Update中的函数使用成员变量记录要修改的transform属性,让FixedUpdate在下一帧开始前通过成员变量修改ransform,从而保证在这一帧物理和渲染系统与当前Transform状态保持同步
1、静态类不能继承和被继承!(严格点说是只能继承System.Object)也就是说你的静态类不可能去继承MonoBehaviour,不能实现接口。
2、静态方法不能使用非静态成员!如果你大量使用静态方法,而方法里又需要用到这个类的成员,那么你的成员得是静态成员。
第2点需要注意:如果你想在Unity的编辑器下调整某个参数,那么这个参数就不能是静态的(哪怕你自定义EditorWindow去修改这个值也没用),解决的办法是通过UnityEngine.ScriptableObject去存放配置(生成*.asset文件),然后在运行中通过LoadAsset去加载,然后再改变静态成员。至于原因,相信不难理解——你看到的所有Unity组件都是一个个实例,你要通过Unity的编辑器去配置,那么你就得有一个这样的可配置实例。
从面向对象上想一下:静态方法或者静态类,不需要依赖对象,类是唯一的;单例的静态实例,一般就是唯一的一个对象(当然也可以有多个)。差别嘛。。。好像也不大。。。
场景初始化阶段调用Find可以接受,例如Start和Awake回调期间,,,但是场景变大之后,开销非常大,,SendMessage开销也很大
一、功能:用于向某个GameObject发送一条信息,让它完成特定功能。
1、执行GameObject自身的Script中“函数名”的函数
**SendMessage (“函数名”,参数,SendMessageOptions) **
2、执行自身和子节点GamgeObject的Script中“函数名”的函数
BroadcastMessage (“函数名”,参数,SendMessageOptions)
3、自身和父节点GamgeObject的Script中“函数名”的函数
SendMessageUpwards (“函数名”,参数,SendMessageOptions)
可以用GetComponent函数来低成本替换SendMessage,见书P68
使用[SerializeField]让private属性在Inspector表现为一个公共字段用来提前赋值,但安全的封装在代码库中
_enemyManager字段是一个特定的MonoBehavior类类型的引用,如果拖拽包含了该类类型的GameObject放到字段上,将引用给定对象上的组件,而不是引用GameObject本身(赋值脚本)
unity可以序列基本类型int等和内置类型Vector3等,记忆枚举、类、结构和包含其他可序列化类型的数据结构如list,,,但是静态字段、只读字段、属性、字典不能序列化
定义整个代码库全局可访问的类
单例模式对于管理共享资源或繁重的数据流量(如文件访问、下载、数据解析和消息传递)非常有用
静态类每个方法、属性、字段都附加static和public
因为StaticEnemyManager是静态类,所以无法调用非静态的类型,因此Unity的Inspector无法赋值,所以需要一个伙伴组件用来序列化一个private变量手动复制后,通过函数把这个变量引用传给静态类
静态类很难和Unity相关的功能交互,不能直接利用MonoBehavior的特性,如事件回调、协程、分层设计和预制块等,在Inspector窗口中没有选择的对象,无法在运行时通过Inspector窗口检查静态类的数据,,,,单例可以实现。。。。
不确定Singleton实例对象是否是最后销毁的,因此如果在OnDestroy回调使用单例组件执行操作,可能使已销毁的Singleton再次创建实例,导致波坏场景文件,因为实例留在了场景中**,不要再OnDestroy中调用单例组件**
使用Observer设计模式,对象在创建时通常会注册到系统,在运行时使用它,然后再使用结束时从系统注销,或者为了清理而在关机时注销,,,单例组件的初始化不应发生在场景初始化期间,,第一次获取实例调用Find会由性能峰值。最好使用God类简单的访问每个单例对象的instance属性,来确保场景初始化期间重要单例对象的实例化
消息发送者可以广播消息而不关心正在听的人,侦听器的职责是确定他们感兴趣的消息,,,,,在程序变复杂时保持对象通信的模块化、解耦和快速。
第一人称和赛车游戏,,,视野之外或者太远而显得不重要的东西,因为玩家活动在开阔的区域,而非可视对象可以临时禁用,而不会产生任何影响
Unity自带内置的渲染功能,视锥剔除
使用OnBecameVisible和OnBecameInvisible()回调,必须附加一个可渲染的组件MeshRenderer必须保证接受可见性回调的组件和渲染对象连接在一个GameObject上
距离玩家足够远,以至于看不见或者并不关注,可以关闭
CPU擅长浮点数相乘,不擅长计算平方根,,可能会损失一些使用平方根值的精度,但是性能收益可观,如果不是非常高精度的场景,都可以使用
sqrMagnitude替换Distance
Unity序列化系统主要应用于场景、预制体、ScriptableObjects和各种资产类型,当其中一种对象类型保存到磁盘是,就用YAML格式转换为文本文件,稍后可以反序列化为原始对象类型
反序列化活动都伴随着显著的性能成本,反序列化在调用Resources.load时发生,用于在Resources文件中查找路径,一旦数据从磁盘加载到内存,后续重新加载相同引用会快很多,但是第一次总需要磁盘活动
层次越深,需要反序列化的数据越多,,UI预制块尤其是个问题
场景开始时加载会造成CPU峰值和加载时间,,,如果在运行时加载他们,可能会掉帧
UI预制块很适合分割,通常不需要整个UI可以一次加载一个
Resources.LoadAsync()以异步方式加载预制块和其他序列化内从,吧磁盘读取的任务转移到工作线程上,从而减轻主线程的负担,,通过检查这个方法调用返回的ResourceRequest对象的isDone属性判断是否完成序列化对象加载
序列化对象加载到内存中,会保存在内存中,需要显示调用Resources.Unload释放内存,,,如果内存预算很大,可以保存在内存中,将减少磁盘读取
游戏设计值,如命中率、力量、速度等这些公共数据序列化到ScriptableObject中加载,,,及那个减少储存在预制文件中的序列化数据量,并可以避免过多的重复工作,显著减少加载时间
如果让玩家尽快进行后续操作,或没有时间等待场景对象出现,最好使用同步加载模式(主线程将阻塞,知道场景加载完成),如果在游戏第一关加载或者返回主菜单,通常使用同步加载
异步加载模式让场景逐渐加载到背景中,而不会对用户体验造成明显的影响,,,SceneManager.LoadSceneAsync()并传递LoadSceneMode.Additive以加载模式参数
场景不遵循游戏关卡的概念,比如第一个场景1-1a在玩家接近下一个章节时,异步并叠加式加载下一个场景1-1b,通过系统检测玩家在关卡中的位置,广播“玩家即将进入下一个章节”,并开始异步加载,需要确保出发场景的异步加载有足够的时间,以便玩家不会看到对象弹出到游戏中
成千上万的MonoBehanvior在长久那个开始时一起初始化,每个都启用协程,既有可能在同一帧触发,导致一段时间内出现一个巨大的峰值,希望时间分散调用
批处理:将大量任意数据块组合在一起并将他们作为单个大数据块进行处理的过程。。这对于CPU非常理想,因为可以使用多个内核同时处理多个任务,在内存中的不同位置来回切换内核是需要时间的,因此切换内核所花的时间越少越好。
Unity中的批处理,通常指的是两种用于批处理网格数据的主要机制:动态和静态批处理。。。本质上是集合体合并的两种不同形式,用于多个对象的网格数据合并到一起,并在单一指令中渲染他们,而不是单独准备和绘制每个几何体。
将多个网格批处理为单个网格是可以实现的,,,因为没有规定网格对象必须是3D空间中连续的几何体,Rendering Pipeline可以接受一系列没有共同边的顶点
Draw Call只是一个从CPU发送到GPU中用于绘制对象的请求。Unity也称为SetPass Call,可以将Draw Call理解为初始化当前渲染过程之前的配置选项
请求Draw Call之前,需要一下工作:
渲染对象前,必须为准备管线渲染而配置的大量设置常常统称为渲染状态(Render State)
渲染状态本质上是通过材质呈现给开发者的。材质是着色器的容器,着色器是一种用于定义CPU应该如何渲染输入的顶点和纹理数据的简单程序。着色器需要注入漫反射纹理、法线映射和光照信息之类的输入,并有效地规定为了呈现传入地数据需要设置那些渲染状态变量。每个着色器都需要一个材质,每个材质都必须有一个着色器。。。新导入场景地网格,如果没有赋予材质,会被自动赋予默认材质,提供基本的漫反射着色器和白色色彩。
最小化渲染状态修改地频率:减少场景中使用的材质数量(CPU每帧花费更少时间生成指令传给GPU,GPU不需要经常停止重新同步状态的变更)
详见P86,修改了8个物体的材质为一个材质Batches,但依然是9个Batches(一个用来渲染背景),因为渲染管线不够智能,无法意识到在重复写入完全相同的渲染状态
Window-Analysis-Frame Debugger,,,左边列出了所有的Draw Call行为,
绘制阴影,Clear清除屏幕,在一个job的Draw Call中绘制网格,Draw Call绘制天空盒,,,左边面板每一项后面的数字表示一个Graphics API调用,Draw Call只是一种API调用,调用API和Draw Dall的开销差不多,但复杂场景中大多数的API都采用Draw Call的形式,因此最好先关注Draw Call的最小化,再去担心注入后处理效果等API通信开销。
动态批处理优势:
Player Settings 开启Dynamic Batching
4个立方体合并到一个名为Dynamic Batch的Draw Call中
Unity查看顶点需要到MeshFilter组件,在Inspector窗口的Preview区域查看verts值
300个顶点的网格,最多对应3个顶点属性(位置、发现、一组UV坐标)即便少于3个属性,也最多300顶点,组成900属性数
方块8个顶点,共24个属性,,,球515个顶点共1545个属性,因此动态批处理只能节省3个Draw Call,查看没有加入到Dynamic Batch的项,会有Why this draw call can’t be batched with the previous one
对象应用统一的等比缩放,或每个对象都有不一样的非等比缩放,才能包含到动态批处理中。等比缩放意味着向量的3个分量都相同(不同的网格不需要买组这个条件),非等比缩放意味着3个分量中最少有一个和其他是不同的
分量3个值中负数值的奇数或偶数相关,0个轴/2个轴负数缩放一个批处理,,1个轴/3个轴负数缩放一个批处理,,尽量减少是否负数缩放
应用动态批处理的可能情况如下:
如果组织两个对象动态批处理的唯一条件是:他们使用了不同的纹理,就应该花点时间和精力合并纹理(通常称为图集),重新生成UV,以便进行动态批处理。牺牲了纹理质量或纹理文件变大,也是值得的
使用该标记才能使用静态批处理,但是不能修改对象的变换,因此,任何想要使用静态批处理的对象不能通过任何方式移动、旋转、缩放。
静态批处理在工作时,将所有标记为Static的可见网格数据复制到一个更大的网格数据缓冲中,并通过一个Draw Call传到管线渲染中,并忽略原始网格,如果所有进行静态批处理的网格都不相同,那么与正常渲染对象相比,不会增加内存使用量,因为存储网格需要的内存空间量是一样的,,,,,如果渲染1000个相同的对象,消耗的内存是不使用静态批处理渲染相同树的1000倍(相同的网格重复复制到缓冲区)
使用不同材质的网格会划分到各自的静态批处理中,,,缺点:静态批处理渲染所有静态网格时,使用Draw Call数量最多只能等于所需的材质数量。
大多数情况下,应该尝试让任何期望被静态批处理的网格出现在场景的原始文件中。如果需要动态实例化、或者使用叠加方式加载场景,就可以使用StaticBatchUtility.Combine()方法控制批处理,这个函数不会将给定的网格与预先存在的任何批处理组合并在一起,即使拥有相同材质,无法通过实例化或叠加加载的静态网格来减少Draw调
静态批处理的显著优势:用于不同形状和巨大尺寸的网格
艺术资产质量和运行速度的平衡
运行时音频处理会称为CPU和内存消耗的重要来源,产生音频瓶颈的缘由多种多样。过度压缩、过多的音频操作、过多的活动音频组件、低效的内存存储方法和访问速度都是导致内存和CPU性能低下的原因。
导入设置包括加载行为、压缩行为、质量、采样率,以及是否支持双声道音频
一下三种设置可以指定音频文件的加载方式,3种方式有很大的不同
音频文件的典型用例:分配给AuidoSource对象的audioClip属性,该对象将音频文件包装在AudioClip对象中,通过AudioSource.Play()或AudioSource.PlayOneShot()触发播放,以这种方式分配的每个音频剪辑都将在场景初始化期间加载到内存种,因为场景包含对这些文件的即使引用,需要这些文件之前必须先解析这些引用。这事Preload Audio Data的默认情况。如果禁用Preload Audio Data就会再场景初始化期间跳过音频文件加载,从而加快场景加载,当Play时, 阻塞主线程,访问磁盘,检索文件,加载到内存,解压缩播放
禁用Preload Audio Data后,应使用Load In Background选项,让音频加载更改为异步任务,在线程中完成加载之前,文件还没准备好用于播放,需要加载到内存。因此Play()和音频播放会有延时。通常在不涉及操作的电梯或长走廊加载或卸载音频
Load Type包含三种选择:Decompress On Load压缩磁盘上的文件,再首次加载时解压缩到内存,这是标准方法,解压缩会导致加载过程中的额外开销,但会减少播放音频文件时所需的工作量;Compressed In Memory加载时直接将音频从磁盘复制到内存种,再播放音频时,才解压缩,播放时牺牲CPU但是音频休眠时提高加载速度,减少运行内存消耗,适合大型音频文件或内存消耗上遇到瓶颈愿意牺牲CPU周期;Streaming在运行时加载、解码和播放文件,具体做法是将文件推过一个小缓冲区,一次只缓存整个文件的一小部分数据,对特定音频剪辑使用的内存量最小,但是运行时CPU使用的内存量最大。定期播放的单实例音频剪辑,不能与自己或其他流式音频剪辑重叠,例如该设置与环境音乐背景音效一起使用。
Inspector中查看音频剪辑的属性,有Compressed Format选项决定三种格式:Compressed(取决于目标平台)、PCM(无损未压缩,适用于极短暂且需要高清晰度的音效)、ADPCM(大小与CPU消耗比PCM高效多,但是压缩会产生相当大的噪声,用做爆炸、碰撞、冲击会掩盖噪声),,,,音频的节省值根据目标平台而不同。
ADPCM
纹理是简单的图像文件、一个颜色数据的大列表,已告知插值程序,图像的每个像素应该是什么颜色
精灵是网格的2D等价物
运行时,这些文件加载进内存推送到GPU的显存,并在给定的DrawCall期间,由着色器渲染到目标精灵或网格上
通道总位数越多消耗越大,例如α通道
左边开启MipMap,右边关闭MipMap
可以看到,当我们开启MipMap时,摄像机远处的纹理变模糊了。而右边关闭MipMap的纹理,远处没有模糊。这就是MipMap的作用,在游戏中,我们距离玩家视角较远的模型(比如树)会有质量上的递减效果。
MipMap就是对纹理进行了LOD处理。
如果没有特别倾斜,不要用
Resource System以Nlog(N)的方式从序列化文件中获取数据,难以基于每个设备提供不同的素材数据。
Asset Bundle可以为应用程序提供小型的、定期的自定义内容更新。而Resource System需要完全替换整个应用程序,才能达到相同的效果。Asset Bundle还提供了更多的功能,如内容流式传输、内容更新、内容生成和共享,可以减少应用程序提高性能。维护更复杂
如果错过了重要的碰撞事件或者计算复杂物理时间卡顿,或者玩家摔倒可能回产生负面影响。除了喜剧物理类型(模拟山羊),,,,对于大量任务的游戏,例如MMO精确物理不重要
Unity有两种不同的物理引擎:3D物理的Nvidia的PhysX和2D物理的开源项目Box2D
物理引擎通常是按固定值前进的假设下运行的。每个迭代都成为时间步长,物理引擎只是用非常特定的时间值来处理每个时间不长,与上一帧花费的时间无关。称为Fixed Update Timestep,默认20ms,每秒50次更新。
固定更新在物理引擎执行更新前进行处理
如果有足够的时间,固定更新将调用所有激活的mono定义的FixedUpdate,处理协程的waitForFixedUpdate,在这个过程中调用的方法不能保证执行的顺序。完成这些任务,物理引擎才开始处理当前的时间步长。如果自上次固定更新以来经过的时间太少(<20ms,,说白了就是帧率太高了,就跳过fixedupdate),则跳过当前的固定更新。高帧率下,渲染更新比物理引擎更新次数更多( 会跳过该次固定更新和物理引擎更新 )。物理引擎会根据两个状态进行可见位置的插值。FixedUpdate用于放置任何期望独立于帧率的游戏行为,例如AI计算。
**FixedUpdate:**调用 FixedUpdate 的频度常常超过 Update。如果帧率很低,可以每帧调用该函数多次;如果帧率很高,可能在帧之间完全不调用该函数。在 FixedUpdate 之后将立即进行所有物理计算和更新。在 FixedUpdate 内应用运动计算时,无需将值乘以 Time.deltaTime。这是因为 FixedUpdate 的调用基于可靠的计时器(独立于帧率)。
假设我们在Project Settings中设置固定时间步长为0.01秒/10毫秒,即每秒进行100次固定更新和物理引擎更新
当我们的游戏以60fps运行时,每帧大约0.01666秒/16.67毫秒,由于我们设置的固定时间步长为0.01秒/10毫秒,可以知道此时进行一次固定更新和物理引擎更新大概10毫秒,即此时每帧画面都调用一次固定更新和物理引擎更新
当我们的游戏以30fps运行时,每帧大约0.03333秒/33.33毫秒,由于我们设置的固定时间步长0.01秒/10毫秒,可以知道此时,进行一次固定更新和物理引擎更新大概10毫秒,即此时每帧画面要调用三次的固定更新和物理引擎更新
当我们的游戏以20fps运行时,每帧大约0.05秒/50毫秒,由于我们设置的固定时间步长0.01秒/10毫秒,可以知道此时,进行一次固定更新和物理引擎更新大概10毫秒,即此时每帧画面要调用五次的固定更新和物理引擎更新
当帧率继续降低时,每一帧需要的固定更新和物理引擎更新次数将会越来越多
因此,我们需要最大允许的时间步长Maximum Allowed Timestep,一般设置为0.3333秒/333.33毫秒,即每帧渲染超过333.33毫秒之后不再进行固定更新和物理引擎更新,来为其他的处理省下时间
动态碰撞器意味着gameobject包含collider和rigidbody,会对外部的里与其他rigidbody的碰撞做出反应。如果一个动态碰撞器碰撞一个无rigidbody的静态碰撞器,静态碰撞器没有相应,动态碰撞器有响应。相当于石头扔到无限质量的物体上。
Discrete离散(对于高速移动有丢失碰撞的风险)、Continuous连续(尽在给定碰撞器与静态碰撞器启用连续碰撞检测,,如果与动态碰撞器的碰撞还是离散碰撞检测)、ContinuousDynamic连续动态(与动态静态碰撞器都是连续碰撞检测)。后两个从时间步长的其实和结束位置插入碰撞器,检查这个时间段中是否有任何碰撞。
Concave凹至少有一个大于180的内角,开启Convex将生成类似左侧的Convex凸碰撞体,,,上限255个顶点。凹面网格碰撞不能时动态碰撞器,只能用作静态碰撞器或触发体积isTrigger。
矩阵定义允许哪些对象与哪些其他对象发生碰撞。
如果物体的速度在短时间内没有 超过某个阈值,那么物理引擎将假设物体在经历新的碰撞或施加新的力之前不需要再次移动,将进入休眠状态。直到被外力或碰撞事件唤醒。
射线从一个点投射到另一个点。
对象投射。Physics.OverlapSphere,检查空间中固定点的优先距离内获得目标列表。Physics.ShpereCast在空间中向前投射整个对象(宽激光束)
**如果不希望碰撞体与其他物体发生物理碰撞的情况下移动,应该添加Rigidbody称为动态碰撞器并开启Kinematic标志,来防止对象收到对象间碰撞的外部脉冲做出反应,类似静态碰撞器,但仍可以通过Transform组件或施加到Rigidbody的力来移动。(Kinematic对象不会对撞机的其他物体产生反应,在运动时会推开其他动态碰撞器,常应用于Player玩家物体)Is Kinematic 运动学:**如果我们只是需要让物理系统进行碰撞检测,不需要使用物理系统控制游戏对象,而是在脚本中使用 Transform 控制物体,此时我们可以勾选刚体组件中的 Is Kinematic 属性。
OnCollider…()回调体哦国内了一个Collision对象作为回调参数,其包含了详细的碰撞信息,OnTrigger…()没有
增加频率,使其更容易通过离散碰撞检测捕获碰撞。
减小频率,CPU有更多时间完成其他任务,但是物体的移动速度降低了,难以通过离散碰撞检测。
如果处理物理计算的时间经常超过允许的最大时间步长,将导致一些看起来很奇怪的物理行为,由于物理引擎需要在完全处理其整个时间配额之前,尽早退出时间步长的计算,因此刚体似乎会减速或突然停止。出现这种情况,很明显需要从其他地方优化物理行为,但是最起码这个阈值可以防止游戏在物理处理过程的峰值中完全卡住。。。默认0.333,超过该值,帧率显著下降。
射线投射方法好用但消耗大。特别是CapsuleCast和SphereCast方法。避免在Update中调用。如果在场景使用持续的西安、射线或区域碰撞(例如激光,火焰,光束武器等)并且保持相对静止,简单的触发体积也很好。不可避免使用的话,最好用layermask调整最小化处理量。
特殊物理碰撞器例如TerrainCollider、Cloth、WheelCollider等,如果不会近距离接触,就不要添加。
碰撞频率和活动物体的总累计时间更有可能以指数形式而不是线性形式增加。不要假定使翻倍。
修改Rigidbody的mass、drag等会重新唤醒对象。
对象离得太近,可能产生群岛效应,相互接触同时休眠,同时唤醒,会产生CPU峰值。
对于复杂的关节系统,可以增加迭代次数,有一些可以减少计算量。新建刚体可以Physics.defaultSolverIterations可以运行时修改迭代次数,但不会影响已经存在的刚体。可以在刚体构造后通过Rigidbody.solverIterations来修改Solver Iteration Count
VR用户最大的威胁是CPU,两眼都要渲染画面,并且AR要时刻解析对象的空间位置,还需要大量的Draw Call来渲染,并且遮挡剔除可能难以设置因为用户会下意识查看周围,,,,如果不使用注视点渲染lod等将毫无意义。
单通道将画面合成一个双倍宽度的texture每只眼镜显示一半,,,,能够降低Draw Call,显著降低CPU在主线程的工作,减少了GPU纹理需要转换的次数,,,当然CPU还是把对象从两个角度渲染了两次。缺点:OpenGl ES3.0以上才能用。
后处理需要针对每只眼镜执行一次,单通道尽管可以减少Draw Call但是不能同时对两个图像同时进行后处理。需要确保后处理的着色器效果,保证渲染到输出纹理的对应那一半上。
抗锯齿提高保真度,减少像素干,提高陈进度,但是会消耗大量的CPU。,,,但是应该尽早启用这个特性。
应该仔细考虑对远处的物体应用背面剔除。要保证摄像机附近的素材是完全封闭的形状
音频数据不再代表特定频道的音频数据,而是包含某些音频谐波数据,运行时合并在一起,创建更可信的音频体验。这会产生CPU活动。
用户可能视角穿模,,,添加碰撞体不让用户离墙面太近。但可能会导致VR中定向障碍,因为摄像机不会与用户移动一致。我往前走,但是摄像机卡住在画面中没走。。。。因此最好就设置安全的缓冲区,不要离这种地方太近。
可能多次旋转后导致不准,最好使用四元数
Ctrl + D赋值
Ctrl + Shift + N新建空GameObject
Ctrl + Shift + A快速打开Add Component
Shift + F(双击F)跟踪选中物体,,帮助追踪掉出场景之外的物体
Alt + 左键在Scene让相机环绕转动
Ctrl + 左键移动物体可以让物体移动时对齐到网格
按住V键移动物体,强制对齐顶点到其他对象。
按下右键,WASD控制摄像头飞行,QE起飞和降落
按住Alt键单击Hierarchy窗口的箭头,全部展开
Shift + 空格 快速让当前窗口充满整个屏幕
Ctrl + Shift + P 在play模式下快速暂停
Ctrl + Alt + P 单步执行
Edit-Project Settings-Script Execution Order,可以指定哪些脚本有限执行
使用Editor Log查看构建成功的项目资源,,,查看那个文件大。
输入公式计算,例如4*128
Inspector窗口有Debug Mode,打开所有的私有字段也会看见。甚至可以显示ObjectID,可以查看根序列化系统相关的东西
在搜索框输入t:查找特定类型的文件
如果使用Asset Bundle和内建的标签系统,l:也可以搜索标签捆绑的对象
右键选择物体,选择Select Dependencies可以查看此资源以来的所有对象,,如果尝试进行资源清理,这很有用
输入t:基于组件进行过滤。。。
此功能不区分大小写,并且派生组件也会显示
从Game窗口看不到Scene窗口相机,但使用热键移动和对齐相机会容易很多。
**编辑器允许将所选对象对齐到相同位置。并通过GameObject-Align with view或Ctrl + Shift + F将选中的物体直接放到当前的相机view位置。**例如选中相机,就对齐到当前view,,,选中物体,物体跑到当前view的位置
GameObject-Align View to Selected将Scene窗口相机与所选对象对齐。可以检查对象指向的方向是否有误。
右上角Layer可以直接过滤对象。
Play模式下也可以保存预制体,,,如果原始对象来源于预制体,在Play模式中更新了,可以直接拖拽play中的预制体到原始预制体上,强制覆盖。。。。不会弹出对话框,,别覆盖错
Frame Skip按钮Pause右边,一阵一阵迭代,每一次都会调用FixedUpdate和Update,与通常可能不一样。
Play模式开始时按下了Pause,将在第一帧暂停
可以修改新脚本、着色器和Compute Shader文件的不同模板
Debug.Log()可以添加富文本、粗体、斜体、
Debug.Log(“