Monobehaviour生命周期
一图流
碰撞体和触发器的区别
使用条件的区别
- 触发器使用时需要在碰撞体上勾选
isTrigger
,勾了之后将不会产生碰撞效果,碰撞体和触发器两者不能共存 - 触发器的主动方需要携带RigidBody和碰撞体,被动方只需要携带一个触发器即可;或主动方携带RigidBody和触发器,被动方携带碰撞体
- 碰撞体则需要主动方携带RigidBody,两者都需要有碰撞体
用途的区别
碰撞体能够模拟真实的物理碰撞;而触发器更适合做一个触发功能:比如主角走到了保存点,主角接触到地面的物品自动捡起
Unity中的GC
Unity中使用C#进行编程,内存部分分为栈区和堆区。
其中栈区的内存由操作系统负责分配和回收,堆区的内存需要手动申请和通过GC机制来回收。
堆区申请和回收的速度都比栈区要慢。在GC时,需要扫描整个堆区的内存,然后找出其中被标记为可回收的,最后执行回收操作
什么会导致GC
- 手动调用GC
- 系统会按照一定的频率自动GC
- 当申请堆空间但是当前空间不足的时候
申请堆空间后可能发生的事情是什么
- 内存空间足够,正常申请
- 内存空间不够,正常申请后失败,然后进行GC,最后成功申请
- 内存空间非常缺乏,进行GC后仍然失败,需要扩充堆空间
GC是比较耗时的,扩充堆空间也耗时,所以如果不注意优化的话,GC频繁,游戏性能下降
GC的步骤是什么
- 首先会扫描所有堆上的对象
- 然后根据对象的引用数量判断是否可以删除
- 标记可以删除的对象
- 删除标记的对象
也就是说,堆上的对象越多,GC的开销越大
GC频繁的害处是什么
首先频繁的GC代表多次的内存申请和回收,它和先进后出的栈不同,频繁的GC可能会导致内存碎片的产生,从而导致更频繁的GC
进行GC的时候最直接的表现就是掉帧
如何减少GC
把GC放在不为人知的地方
比方说可以在游戏的设置界面或者在加载界面手动进行GC,这样用户不易察觉到
减少GC
-
最基本的,装箱拆箱操作就会导致产生更多的内存垃圾,导致GC的发生,所以应该尽量避免装箱拆箱
-
string类型是引用类型,也就是说需要在堆上分配空间,那么使用频繁的使用
+
号进行字符串拼接,肯定会导致垃圾的产生。所以应该尽量使用StringBuilder
来完成这易操作 -
不要频繁的在Update函数中申请堆空间
-
协程也会产生内存垃圾,因为Unity会创建一个实体来管理协程
// 携程中使用不当也会导致GC // 装箱操作 yield return 0; // 每次调用都构建了一个新的堆区对象 yield return new WaitForSecond(1f);
-
LinQ表达式采用装箱方式实现,也会造成GC
一图流
协程与多线程
Unity中的性能优化
脚本方面
GetComponent
,Instantiate
,SetActive
,IO操作等耗时较长的接口应该在Loading的时候做- 用移除屏幕代替UI的
SetActive(false)
。因为SetActive
会去遍历当前物体及它的所有子物体,然后执行对应的OnEnable
和OnDisable
操作 - 减少使用
==
号对gameObject的使用 - 子物体过多的Gameobject应该减少对其进行Transform变换
- 使用StringBuilder代替string,减少gc
- 减少临时对象的创建,减少gc
- 减少装箱拆箱的操作,使用装箱拆箱操作的容器有:
ArrayList
和Hashtable
- 在不影响游戏体验的时候(例如Loading、设置界面)主动通过System.GC.Collect()调用垃圾回收
图像方面
降低DrawCall
-
批处理:将使用相同渲染状态的网格合并,一次性交给GPU进行渲染
静态批处理:适用于场景中不会运动的物体,材质相同的物体会被合并在一起
动态批处理:由Unity自动完成。我们做的工作就是辅助Unity自动合并更多的物体:在shader中减少使用顶点属性,或尽量减少模型的顶点数量
-
GPU Instancing:将同一材质,同一网格的物体合并,一次性交给GPU进行渲染。
使用了不同的颜色或者贴图的(uniform)都不属于同一材质;物体缩放不会影响Instancing
Unity中快速移动物体穿墙
https://www.zhihu.com/question/39177106
C#装箱拆箱
装箱拆箱是什么
理解层面来讲:值类型转换为引用类型叫做装箱;引用类型转化为值类型叫做拆箱
装箱拆箱的坏处
首先装箱和“拆箱”带来的内存分配和数据拷贝是耗时的,而且传参时产生的装箱拆箱会产生内存垃圾,导致内存占用,从而导致GC的发生
什么会导致装箱拆箱
使用非泛型的容器可能会,例如ArrayList和HashTable
LinQ表达式也是使用装箱实现的,所以也可能会
装箱拆箱执行了什么
从定义层面来讲:
- 装箱
- 首先需要在堆中分配内存(大小为值类型实例大小加上一个方法表指针和一个SyncBlockIndex)
- 然后将栈中的数据拷贝至堆
- 最后返回一个引用指向该内存空间
- 拆箱
- 获取到箱子中属于值类型字段的那个地址
- (将该地址中的数据拷贝到栈中的值类型中)——这一步其实不属于拆箱,但是是”拆箱“的必经之路
如何改变箱子中物体的值
public struct A
{
public int data;
public void Change(int num) { data = num; }
}
public interface IChange
{
void Change();
}
public struct B : IChange
{
public int data;
public void Change(int num) { data = num; }
}
// 尽管有new但是还是分配在栈上 不要被C++误导
A a = new A();
object o = a;
((A)o).Change(300);
// 结果还是0 无法修改a中的数据 因为这是拆箱之后产生的一份拷贝
Debug.Log(((A)o).data);
B b = new B();
object o = b;
((IChange)o).Change(100);
// 成功修改
Debug.Log(((B)o).data);
public class C
{
public int data;
public void Change(int num) { data = num; }
}
C c = new C();
// 不存在装箱的操作 只是单纯的类型转换
object o = c;
((C)o).Change(500);
// 成功修改
Debug.Log(((C)o).data);
C#中的struct和class
- struct是值类型,按值传递,内存分配在栈上;class是引用类型,按引用传递,内存分配在堆中
- struct不能有无参构造函数和析构函数,只能继承接口。调用struct中的有参构造函数需要使用到new
- struct创建时可以不适用new,但是要需要对它的成员(全部)进行赋值,然后才能使用。class的对象创建时必须使用new后才能使用
- struct中若包含了引用类型,那么该引用类型还是存放在堆上的。class中如果包含了值类型,那么值类型是存放在堆上的
- struct中默认的修饰符是public但是class中是private
- struct中不能使用abstract,virtual和protected修饰符