Unity与C#
C#
基础
C#类型转换:https://www.cnblogs.com/AMzz/p/13563292.html
GC回收机制:https://www.cnblogs.com/AMzz/p/13565638.html
system.gc.collect
值类型与引用类型
值类型
数值类型,结构体,bool型,用户定义的结构体,枚举,可空类型。
引用类型
数组,用户定义的类、接口、委托,object,字符串,null类型,类,string
值类型转引用类型,称为装箱;引用类型转值类型,称为拆箱
区别
- 速度:值类型存取速度快,引用类型存取速度慢。
- 用途:值类型表示实际数据,引用类型表示指向存储在内存堆中的数据的指针或引用。
- 来源:值类型继承自System.ValueType,引用类型继承自System.Object
- 位置:值类型的数据存储在内存的栈中,引用类型的数据存储在内存的堆中,而内存单元中只存放堆中对象的地址。
- 类型:值类型的变量直接存放实际的数据,而引用类型的变量存放的则是数据的地址,即对象的引用。
常用数据结构
数组
Array
-
特点
- 数组存储在连续内存上,元素都是相同类型或类型的衍生类型
-
缺点
- 声明时必须指定长度
- []里指定长度 or {}里直接设置所有值
- 声明时必须指定长度
-
常规操作
int []arr=new int [5] int [,]arrs=new int [2,3]; arr[0]=1;
ArrayList
-
特点
- 可以存储不同类型元素,因为ArrayList会把元素当作Object处理
- 长度按存储数据动态变化,不必声明时指定长度
-
缺点
- 类型不安全,因为不同类型都当作Object处理,可能发生类型不匹配
- 频繁读写会产生额外开销,因为插入值类型需装箱,索引取值需拆箱
-
常规操作
ArrayList al=new ArrayList(); al.Add(233); al[0]=244; al.RemoveAt(0);
List
- List
可看作ArrayList的泛型等效类,但最大不同在于:声明时需为其声明元素的对象类型 - 声明时无需指定初始长度,长度动态变化
- 优点
- 确保类型安全
- 取消拆箱与装箱操作,以及因为引入泛型无需运行时类型检查,因此高性能
- 融合了Array可以快速访问,ArrayList长度可变优点
链表
LinkedList
-
链表适合元素数量不固定且需要经常增删结点的情况
-
常规操作
LinkedList<string>sentence=new LinkedList<string>("hello"); sentence.AddFirst("hh"); sentence.AddLast("nono"); LinkedListNode<string>node1=sentence.First; node1=sentence.Find("hh"); sentence.AddBefore(node1,"ok"); sentence.AddAfter(node1,"find"); sentence.RemoveFirst(); sentence.RemoveLast();
字典
Dictionary<key,value>
ref与out
ref 和 out 都是以引用的方式将变量传入函数
ref :传入时必须初始化,函数内能直接使用,在函数定义以及调用函数时要显式使用 ref 关键字
out:传入时不必初始化,但函数返回前必须在函数为其赋值,在函数需要返回多个值时使用,在函数定义以及调用函数时要显式使用 out 关键字
进阶
委托
C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。
委托(Delegate)特别用于实现事件和回调方法。所有的委托(Delegate)都派生自 System.Delegate 类。
反射
Lua与C#
Lua和C#是如何实现通信的?
交互过程:
C# call Lua :由C#文件调用Lua解析器底层dll库,由dll文件执行相应的Lua文件;C# -> dl l-> Lua
Lua call C# :首先生成C#源文件所对应的Wrap文件,warp文件把字段方法注册到lua解释器中,就可以通过Warp文件调用C#文件了。 Lua ->Warp->C#
交互原理:
主要通过虚拟栈实现。
C# call Lua:C#将数据放入栈顶,Lua去栈顶获取数据,在lua中做处理,然后把结果放回栈顶,再C#从栈顶取出数据,完成交互.
Lua call C#:巴拉巴拉巴拉巴拉
Unity
Unity3D引擎本身是用C++实现的,C#只是用户所使用的脚本语言
系统
碰撞
1.碰撞器和触发器
碰撞器是触发器的载体,而触发器只是碰撞器身上的一个属性。
- 当Is Trigger=false时,碰撞器根据物理引擎引发碰撞,产生碰撞的效果,可以调用OnCollisionEnter/Stay/Exit函数;
- 当Is Trigger=true时,碰撞器被物理引擎所忽略,没有碰撞效果,可以调用OnTriggerEnter/Stay/Exit函数
如果既要检测到物体的接触,又不想让碰撞检测影响物体移动,或要检测一个物件是否经过空间中的某个区域这时就可以用到触发器
2.发生碰撞的必要条件?
- 两个物体都必须带有碰撞器(Collider)
- 其中一个物体还必须带有Rigidbody刚体,而且必须是运动的物体
摄像机
面板
-
Clear Flags 清除标记
- Skybox:把颜色缓冲设置为天空盒,并完全清空深度缓冲
- Solid:和天空盒一样,只是把颜色缓冲设置为纯色
- Depth only:这个选项会保留颜色缓冲,但会清空深度缓冲
- Don’t Clear:不清除任何缓冲
-
Culling Mask剔除遮罩
- 用于来设定是否剔除处于某一层的对象。Unity场景中的每一个对象,都被分配了一个层,默认为“default”层。
- 在一个场景里设置了多个小场景与摄像机,可以通过设置这个使其画面中不会出现别的场景与摄像机
-
Projection
- Orthographic正交投影:长方体,平行光
- Perspective透视投影:视锥体,近大远小
-
target texture
- 可以实现把摄像机的画面投影到target texture上的render texture
- 然后将该贴图可以放在UIImage上或者plane上
- 可以实现小地图,后视镜,UI上的3D模型
RenderTexture
类似于一个帧缓冲
UI上实现模型
- 使用一个单独 Camera 将模型渲染到一张 RenderTexture,
- 然后再将这张 RenderTexture 贴到 UGUI 的 RawImage 组件上,
- 对于这个负责渲染模型的单独 Camera,将其 Clear Flags 设为 Solid Color,然后调整其 Backgroud 的 Alpha 值为 0。
UI系统
基本知识
NGUI 和 UGUI
ngui是unity一个ui插件
ugui是unity 4.6 开始官方推出的ui系统
区别
- UGUI的Canvas 有世界坐标和屏幕坐标。
- UGUI的Image可以使用material。
- UGUI通过Mask来裁剪,而NGUI通过Panel的Clip。
- NGUI的渲染前后顺序是通过Widget的Depth,而UGUI渲染顺序根据Hierarchy的顺序,越下面渲染在顶层。
- UGUI 不需要绑定Colliders,UI可以自动拦截事件。
- UGUI的Anchor是相对父对象,没有提供高级选项,感觉UGUI的Anchor操作起来比NGUI更方便。
- UGUI没有Atlas(NGUI的图集)一说,使用Sprite Packer。
- UGUI的Navgation在Scene中能可视化。
- UGUI的事件需要实现事件系统的接口,但写起来也算简单。
优缺点
- NGUI还保留着图集,需要进行图集的维护。而UGUI没有图集的概念,可以充分利用资源,避免重复资源。
- UGUI出现了锚点的概念,更方便屏幕自适应。
- NGUI支持图文混排,UGUI暂未发现支持此功能。
- UGUI没有 UIWrap 来循环 scrollview 内容。
- UGUI暂时没有Tween组件。
图集
原理
UV是光栅化的时候每个像素会依据算出的UV位置取贴图的颜色填充到RenderBuff。
UV是0-1是做了归一化。显卡渲染就像画家画画,如果要换一种颜色的话(换一张贴图)需要停下来做切换这样很慢。
为了减少这个过程减少DC提升效率,就会对图集打包,对Mesh做合并。把几张图合并成一张,然后UV归一化,把几个Mesh合并成一个Mesh,原来Mesh的UV就映射到新的合并后的贴图上。就像画家有了一只可以一笔画出多种颜色的笔,就变成了神笔马良
创建方法(UGUI)
- 准备好要打包的素材,将素材设置为Sprite(2D and UI)类型
- 右键菜单 create “Sprite Atlas”
- 在新建出来的Sprite Atlas的inspector面板中,在Objects for Packing添加要打包的素材
- 点击Pack Preview,打包(要先在Edit->Project Settings->Editor->SpritePacker设置为Enable)
加载图集(UGUI)
- SpriteAtlas类在UnityEngine.U2D的命名空间内
- 使用Resources.Load来获取SpriteAtlas对象
- 使用SpriteAtlas.GetSprite来获取Sprite对象
自动打包设置
Editor->Project Settings 下面有sprite packer的模式,启用它表示是否将小图自动打成图集
- Disabled表示不启用它
- Enabled For Builds 表示只有打包的时候才会启用它
- Always Enabled 表示永远启用它。
注意你的图片不能放在Resources文件夹下面,Resources文件夹下的资源将不会被打入图集
NGUI与UGUI关于图集区别
- NGUI是必须先打出图集然后才能开始做界面。这一点很烦,因为始终都要去考虑你的UI图集。比如图集会不会超1024 ,图集该如何来规划等等。
- UGUI的原理则是,让开发者彻底模糊图集的概念,让开发者不要去关心自己的图集。
AssetBundle
AssetBundle是将资源使用Unity提供的一种用于存储资源的压缩格式打包后的集合,它可以存储任何一种Unity可以识别的资源,如模型,纹理图,音频,场景等资源。
一般情况下AssetBundle的具体开发流程如下:
(1)创建Asset bundle,开发者在unity编辑器中通过脚本将所需要的资源打包成AssetBundle文件。
(2)上传服务器。开发者将打包好的AssetBundle文件上传至服务器中。使得游戏客户端能够获取当前的资源,进行游戏的更新。
(3)下载AssetBundle,首先将其下载到本地设备中,然后再通过AsstBudle的加载模块将资源加到游戏之中。
(4)加载,通过Unity提供的API可以加载资源里面包含的模型、纹理图、音频、动画、场景等来更新游戏客户端。
(5)卸载AssetBundle,卸载之后可以节省内存资源,并且要保证资源的正常更新。
打包
创建AssetBundle常用静态方法
流程
-
先在Inspector面板设置指定资源的AssetBundle属性,如prefab下面可设置:目录,名字与后缀名
-
创建一个文件夹命名Editor,这个文件夹是不会进行打包的特定编辑器扩展文件夹;然后我们创建一个编辑器扩展类,写如何进行打包
public class CreateAssetbundles { [MenuItem("AssetsBundle/Build AssetBundles")] static void BuildAllAssetBundles()//进行打包 { string dir = "AssetBundles"; //判断该目录是否存在 if (Directory.Exists(dir) == false) { Directory.CreateDirectory(dir);//在工程下创建AssetBundles目录 } //参数一为打包到哪个路径,参数二压缩选项 参数三 平台的目标 BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.None,BuildTarget.StandaloneWindows64); } }
-
回到Unity里面点击我们刚刚扩展出来的打包按钮
-
打包完成,可以在工程的目录下可以找到AssetBundles目录
压缩类型
Unity3D引擎为我们提供了三种压缩策略来处理AssetBundle的压缩,即:
- LZMA格式
- 默认压缩格式,BuildAssetBundleOptions.None
- 使用LZMA格式压缩的AssetBundle的包体积最小,但是相应的会增加解压缩时的时间。
- LZ4格式
- BuildAssetBundleOptions.ChunkBasedCompression
- 经过压缩后的AssetBundle包体的体积较大(该算法基于chunk)。但是,使用LZ4格式的好处在于解压缩的时间相对要短
- 不压缩
- BuildAssetBundleOptions.UncompressedAssetBundle
- 包大,加载快
加载
普通
- 从内存加载使用LoadFromMemoryAsync
- 从本地文件加载可以使用LoadFromFile
- 从服务器上Web上加载可以使用UnityWbRequest
using UnityEngine;
using System.IO;
using System.Collections;
using UnityEngine.Networking;
public class LoadFromFileExample : MonoBehaviour {
IEnumerator Start () {
string path = "AssetBundles/scene/model.ab";
//1.异步加载
AssetBundleCreateRequest request = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path));
yield return request;
AssetBundle ab = request.assetBundle;
//2.同步加载
AssetBundle ab = AssetBundle.LoadFromFile(path);
//3.使用UnityWbRequest 服务器加载使用http本地加载使用file
//string uri = @"file:///C:\Users\Administrator\Desktop\AssetBundleProject\AssetBundles\model.ab";
string uri = @"http://localhost/AssetBundles\model.ab";
UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(uri);
yield return request.Send();
AssetBundle ab = DownloadHandlerAssetBundle.GetContent(request);
//使用里面的资源
Object[] obj = ab.LoadAllAssets<GameObject>();//加载出来放入数组中
// 创建出来
foreach (Object o in obj)
{
Instantiate(o);
}
}
}
要在运行时加载AssetBundle对象主要可以分为两大类途径:
- 先获取WWW对象,再通过WWW.assetBundle获取AssetBundle对象
- 无需缓存
- 方法
- public WWW(string url)
- 构造WWW对象的过程中会加载Bundle文件并返回一个WWW对象,完成后会在内存中创建较大的WebStream
- public static WWW LoadFromCacheOrDownload(string url, int version, uint crc = 0)
- 调用该方法同样会加载Bundle文件同时返回一个WWW对象,和上一个直接调用WWW的构造函数的区别在于该方法会将解压形式的Bundle内容存入磁盘中作为缓存(如果该Bundle已在缓存中,则省去这一步)
- public WWW(string url)
- 直接获取AssetBundle
- 需要缓存
- 方法
- LoadAsset:从资源包中加载指定的资源
- LoadAllAsset:加载当前资源包中所有的资源
- LoadAssetAsync:从资源包中异步加载资源
光照系统
光照分类
-
局部光照(Local illumination)
- 光在模型表面的照射效果
-
全局光照(Global illumination)也称GI
- 全局光照里面又有Realtime GI(实时全局光照),默认情况下Unity的光源都是实时的,代表这些灯源会把光线照射到场景并每帧更新,
-
全局光照参考的条件太复杂,而且占据大量的CPU资源;局部光照只考虑模型表面的照射效果,所以更加灵活,简易
-
对于静态物体来说,大多使用光照贴图来模拟间接光的照明效果,然后加上直接光源的动态照明效果;
-
对于运动物体来说,则仅用直接光源的动态照明效果,或者使用光照探针来模拟间接光的照明效果。
光源面板
-
type光源类型
- Direct Light平行光
- Point Light点光源
- Spot Light聚光灯
- Area 区域光
- 仅在烘焙光照贴图时有效
-
mode灯光照明模式
- Realtime:Unity默认选择Realtime,灯光会通过烘焙GI系统处理直接光源与间接光源。
- 烘焙物体和物体之间的关系信息,只要物体之间的关系不变(也就是所有的静态物体都不移动位置),就不需要重新烘焙,
- 从而使得我们可以在场景中随意运用动态光源(dynamic lights)
- Baked:只会被场景内的静态对象拿去GI计算,在Baked GI模式下不会照亮非静态物体
- Mixed:场景内的静态对象会被烘焙GI拿去做计算,同时仍会继续运算即时光源到非静态对象上。
- Realtime:Unity默认选择Realtime,灯光会通过烘焙GI系统处理直接光源与间接光源。
-
Shadow Type:阴影贴图的类型,用阴影贴图模拟的阴影效果
- No Shadows:无阴影贴图
- Hard Shadows:硬阴影贴图
- Soft Shadows:光滑阴影边缘(也就是阴影模糊效果)
LightMap
Realtime GI(Global Illumination),实时全局光照
烘焙:把灯光信息写到贴图里 比如影子 AO 这样就可以不用实时灯光 是节省硬件资源的一种较好的方法
烘焙LightMap分为动态物体和静态物体,
- 静态物体的MeshRender上会有个lightmapIndex存放采用第几组lightmap,还有个lightmapScaleOffset存放uv偏移,通过这两个数据就能显示正确。
- 设置成可烘焙的
- 从windows的render打开lightsetting,设置完成后点击Generate Lighting按钮烘焙光照贴图
- 动态物体走的是GI,通过环境光,LightProb等这些算出三维光照数据,然后计算动态物体的球谐光照。对于静态物体来说就会烘焙成 lightmap,一般有几组三张贴图(color,dir以及shadow)。
导航系统
Unity内置导航系统Navigation,共分为四大部分,基础寻路原理是A*
将游戏中复杂的结构组织关系简化为带有一定信息的网格,在这些网格的基础上通过一系列的计算来实现自动寻路
旧版本导航
- NavMesh:用来描述一个可行走区域的数据结构,这个数据是需要我们手动设置生成(baked),或者动态生成(代码控制)。
- Max Slope:斜坡的坡度。
- Ste Height:台阶高度。
- Drop Height:允许最大的下落距离。
- Jump Distance:允许最大的跳跃距离。即允许跨过的分离网格的距离
- Nav Agent:用来帮助游戏中的角色移动到指定目标的组件,它使用的是NavMesh数据,并且知道如何规划最优路径,避开障碍物等。
- Off-Mesh Link:用于手动指定路线来生成分离的网格连接。例如,游戏中让行进对象上下爬梯子到达另一块网格的情景就是利用生成分离的网格连接来实现的。
- Navmesh Obstacle:这个组件是用来给需要导航的世界设置障碍的,如图中所示,当障碍物静止在地面上时,会在导航区域内扣除一个洞。
public class MoveDestination : MonoBehaviour {
public Transform goal;
void Start () {
NavMeshAgent agent = GetComponent<NavMeshAgent>();
agent.destination = goal.position;
}
旧的NavMesh是在游戏开发时就把NavMesh烘培出来,便不能满足游戏进行中游戏地图的随机生成,亦或者是游戏过程中游戏地图会出现多种变化情况的需求;新的navmesh系统带给我们的新的惊喜——在运行时烘焙navmesh
将需要参与实时烘培的地面添加NavMeshSourceTag组件即可
新版本Navigation
- NavMesh Surface:对标NaMesh系统,使用这个组件后会更方便,直接在需要进行导航的场景的父节点挂载这个组件就可以完成数据的bake,不再需要指定手动指定,而且会更方便管理导航的路径。
- 有了该组件后可以不需要设置物体Navigition static的静态属性,以前的静态属性只适用于editor模式,不适用于实时bake的特性
- Collect Object: 定义哪些物体用来bake生成NavMesh,里面通过选择volumn,再调整size大小即可只烘焙玩家周围的navmesh
- NavMesh Modifier:是用来配合NavMesh Surface使用的,在子层级上添加该组件后,直接修改这个组件会影响子层级下的所有场景物体,比如可以指定某类型的角色在该区域是不可以移动的。
- NavMeshModifierVolume:同样也是用来配合NavMesh Surface使用的,区别于NavMesh Modifier,该组件会创建一个体积盒,类似于碰撞盒,该体积盒包含的区域都会受到该组件参数的影响。比如放置一个该组件的体积盒,并设置为不可移动类型,在bake后,该组件体积盒所在区域都变为不可移动区域。
- NavMeshLink:对标的是Off-MeshLinks,添加该组件的场景物体会使接触到的两个不连通的NavMesh之间形成通路。
NavMeshSurface组件的BuildNavMesh()方法
脚本相关
.Net与Mono的关系?
mono是.net的一个开源跨平台工具,就类似java虚拟机,java本身不是跨平台语言,但运行在虚拟机上就能够实现了跨平台。.net只能在windows下运行,mono可以实现跨平台跑,可以运行于linux,Unix,Mac OS等。
协同程序(协程)
进程,线程,协程
- 进程:
- 进程是系统进行资源分配和调度的基本单位,是程序的实体
- 不同进程使用的不同的内存空间
- 线程:
- 线程是程序执行流的最小单位,是CPU调度和分派的基本单位
- 一个进程可以有一个或多个线程
- 同一个进程中的不同线程共享程序的内存空间
- 同一个进程中的不同线程使用不同的栈
- 协程:
- 比线程更加轻量级的存在,由程序控制(而不是被OS内核管理
- 协程在子程序内部是可中断的,且会保存退出时的状态,可以适时回来接着执行
- 一个线程中可以有多个协程
Unity中的协程
- 通过 MonoBehaviour.StartCoroutine 来启动协程(有3个重载
- StartCoroutine函数是立刻返回的(非阻塞
- 通过 MonoBehaviour.StopCoroutine 或者将 GameObject SetActive(false)掉 来停止协程
- 将 MonoBehaviour Disable 不能停止协程
协程返回的几种方式
- yield return 0 程序下一帧从这里执行
- yield return null 同1
- yield return new WaitForSeconds(N) 程序N秒后从这里执行
- yield new WaitForEndOfFrame() 在所有的渲染以及GUI程序执行完成后从当前位置继续执行
- yield new WaitForFixedUpdate() 所有脚本中的FixedUpdate()函数都被执行后从当前位置继续执行
- yield return WWW 等待一个网络请求完成后从当前位置继续执行
- yield return StartCoroutine(xxx) 等待一个xxx的协程执行完成后从当前位置继续执行
相当于把执行权交给启动的协程,等启动的协程执行结束后再将执行权交给原来的协程 - yield break 直接从当前位置跳出函数体
IEnumerator,IEnumerable
IEnumerator和IEnumerable 是接口:
public interface IEnumerator
{
object Current { get; }
// 如果是返回 false,就是结束迭代器块
bool MoveNext();
void Reset();
}
// 可枚举的接口 IEnumerable, 返回枚举器接口
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
如果说某个类实现了IEnumerable可枚举的,那么我们说这个类就是可迭代对象(即可枚举对象,可以返回枚举器)。
foreach循环就是利用实现了IEnumerable类的该接口
yield
yield关键字是一个语法糖,背后其实生成了一个新的IEnumerator。
原理:
https://www.cnblogs.com/blueberryzzz/p/8678700.html#undefined
//好jb难,放弃治疗
1.可以在方法,属性 和 索引器中使用 yield 来实现迭代器。 使用yield,函数返回类型必须是 IEnumberable
- try 和 catch 语句里面不能出现yield return
3.不能在匿名方法中用yield。
参考:https://blog.csdn.net/u012138730/article/details/81148086
定制特性Attribute
特性都派生自System.Attribute,多个特性可应用于同一目标元素,可用多个方括号分开,也可一个方括号内用逗号隔开多个特性
DllImport特性
应用于方法
在dll位非托管代码时使用,告诉运行时该方法的定义位于指定的dll的非托管代码中
- 若外部DLL是用托管语言即C#编译而来,则一般只需要将目标DLL放在Assets目录下的Plugins目录即可在游戏脚本中调用(直接using外部DLL的命名空间,创建类即可访问方法)
- 若目标DLL是使用非托管代码,如C/C++生成的,
Serializable特性
应用于类型,不会被派生类继承
用来告诉格式化程序一个类型的实例的字段可以进行序列化和反序列化操作(必要操作),使用方法
[Serializable]
public class xxClass{}
与之相对应的NonSerialized,标记类型不能被序列化,并且特性可以被派生类继承
Unity特性
Unity特性类分别定义在两个命名空间里,UnityEngine与UnityEditor
AddComponentMenu
位于UnityEngine,允许在Component菜单上放置一个脚本选项,单击该选项即可向目标GameObject添加该脚本
[AddComponentMenu("脚本类型名/脚本功能")]
public class xxclass:MonoBehaviour{}
MenuItem
位于UnityEditor,需将脚本放于Assets/Editor文件夹下;
允许添加菜单项到主菜单和检视面板上下文菜单,会将所有静态方法变成菜单命令
SerializeField
应用于私有变量,使其能在Inspector面板上显示,但仅作用于其下一行
[SerializeField]
private float life = 10;
与之相对的[HideInInspector]是隐藏一个变量,一般用于公有变量
Header
插入一个标签,用于将Inspector面板上的变量分类
[Header("基本属性")]
[SerializeField]
private float life = 10;
public float attackValue = 5;
public float moveSpeed = 2;
自定义特性类
AttributeUsage里的AttributeTargets是一个枚举,可以标明该特性适用范围,如下面是指定类可使用
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class MyAttribute : System.Attribute
{
string name;
public MyAttribute(string name)
{
this.name = name;
}
}
[MyAttribute("TestAttribute")]
public class TestAttribute : MonoBehaviour
{
public TestAttribute()
{
//用反射检测是否与MyAttribute特性类的实例已经进行关联
if(this.GetType().IsDefined(typeof(MyAttribute),false))
{
Debug.Log("yes");
}
}
}
脚本生命周期
Unity3d脚本从唤醒到销毁有着一套比较完整的生命周期,按顺序执行
- Awake:用于在游戏开始之前初始化变量或游戏状态。在脚本整个生命周期内它仅被调用一次.
- OnEnable:在同一周期中可以反复地发生。(每当脚本挂着的Object或该脚本变成active时执行一次)
- Start:仅在
- Update
- FixedUpdate:固定时间间隔执行,可更改间隔;比较适用于物理引擎的计算,因为是跟每帧渲染有关
- LateUpdate
- OnGUI
- OnDisable:当对象变为不可用或非激活状态时此函数被调用。(每当脚本挂着的Object或该脚本变成active时执行一次)
- OnDestroy:当MonoBehaviour将被销毁时,这个函数被调用
脚本enable=false时,碰撞事件还会判断
awake,start,onenable区别
- 调用时机
- onenable在同一周期中可以反复地发生。(每当脚本挂着的Object或该脚本变成active时执行一次)
- awake当脚本挂着的gameObject是active,脚本不是active时,也会执行一次;而start只有脚本是active时才会执行
- 调用次数
- awake与start只能在生命周期中被调用一次,即初始化时被调用,之后即使是在被重新激活之后也不会再次被调用。
- 初始化
- awake函数在对象初始化之后立刻就会调用,awake比start提前调用,适合用来初始化一些引用
- 在start里初始化可能被其他自定义的函数抢先(别的脚本创建了该物品,然后调用了该自定义函数,而此时若该脚本一些引用初始化放在start,自定义函数里涉及引用的使用都将报错,指向空)
多线程
Unity可以支持用户自己开发使用多线程,比如用户做网络模块的时候,可以开一个线程来做网络消息的读取等。
Unity框架里面是两个线程
- main thread:负责业务逻辑线程,跑Mono代码,物理引擎代码等
- render thread:负责处理绘制相关,将绘制相关数据提交给GPU。
所以unity内部是多线程的,Unity的很多API与场景组件相关API必须要在main thread里面调用,不能在其他线程调用。
序列化与持久化
Unity中与序列化相关的
- 属性监视板Inspector中的数值:通过被观察对象反序列化属性数值得到的
- 预制体prefab:是游戏对象或组件经过序列化得到的文件
- 实例化Instance:首先将传入参数original所引用的对象进行序列化操作,然后创建出一个新对象(各个字段都是默认值),在将被序列化的对象反序列化,将对应的字段值赋值给新对象
- 存储场景与加载场景
- 重载编辑器代码
序列化与反序列化底层
序列化
- 获取对象成员
- 获取与1对应的值
- 将程序集标识与类型的完整名称写入流中
- 将1与2的信息写入流
反序列化
- 获取程序集标识与类型完整名称,从而获得对象类型
- 为1的对象分配内存空间(未调用构造函数)
- 获取对象信息
- 获取对象字段所对应数值的信息
- 根据字段与对应值为第2步分配的对象进行初始化
存档方法
-
PlayerPrefs
- 原理:采用键值对的方式对数据进行存储---保存在本地文件(不同操作系统存储路径不同)中,然后程序可以根据这个名称取出上次保存的数值)
- 优点:上手简单,存储方便,不用考虑内部实现,适合做小游戏的数据存档
- 缺点:只支持基本数据类型,无法存储一个类,数组,集合,字典等。需要实现一个框架来自动赋值
-
c#二进制序列化
- 除了静态类型和抽象类型以及类必须标记为[Serializable]的,其他的都可以被序列化:类,数组,集合,字典,类及其子类等
- 优点:有加密效果,因为看不懂
- 缺点:
- 在升级版本后,新增一个字段也只是采用系统默认值,而不是我在类中直接赋的值
- 在升级版本后,如果删除了之前的一个字段,则无法正确解析(反序列化)
-
XML序列化
- 优点:
- 序列化出来的数据直观,可以序列化类和类中的对象
- 升级版本后,如果新增了字段,则自动采用你在类中赋给该变量的值
- 升级版本后,如果删除了之前的字段,则自动忽略之前的字段,而不会像c#序列化一样报错,
- 缺点:
- 不能序列化字典,二维数组以上的数据
- 比Json更占空间,且引入的dll也更大
- 优点:
-
Json
- 优点:
- 简单轻量
- 可以满足你要序列化的几乎任何类型数据(除了float必须用double来存)
- .如果要升级版本,可以任意删除之前的字段而不会出现不能解析的情况;可以新增字段且采用你在类中直接赋的值(不用像c#序列化那样手动赋值了)。
- 缺点:
- 相对PlayerPrefs来说,引入了一个50kb左右的dll
- 不能序列化float类型
- 不支持更改字段名。如果更改了,就相当于是两个操作,即删除之前的并新增加一个(这一点很重要)。
- 优点:
性能优化
分析工具
Status
//todo
Profiler分析器
//todo
2D图片压缩
Unity对纹理的处理是智能的:不论你放入的是PNG,PSD还是TGA,它们都会被自动转换成Unity自己的Texture2D格式
- 高清晰无压缩:RGBA32
- 中清晰中压缩:RGBA16 + Dithering
- 低清晰高压缩:RGB(ETC1)+Alpha
- ETC1不带A通道,需要二次方大小
- 在原RGBA32的原图中,提取RGB生成第一张ETC1,再提取A通道,填充另一张ETC1的R通道;游戏运行时,Shader将两张ETC1图片进行混合。
- PVRTC4:必须是二次方正方形;也就是说,长宽在二次方的同时,还必须要相等
为什么图片要有2的N次的设置
图片的纹理像素在Unity3D中需要遵循2的N次方,由图形学决定的,只识别2的N次方。非2的N次方的图片会转化为2的N次方图片(500 * 500 —》512 * 512),是因为转化过程比较慢,由运行程序转换十分耗时,所以Unity3D提前将资源转化为符合标准的图片。
渲染系统
DrawCall
- 在unity中,每次CPU准备数据并通知GPU的过程称之为一个DrawCall。
- Unity绘制一个场景的时候,把场景里面的物体分成几个批次提交给GPU绘制就是几个DrawCall。
- 具体过程:设置颜色->绘图方式->顶点坐标->绘制->结束。所以在绘制过程中,如果能在一次DrawCall完成所有绘制就会大大提高运行效率,进而达到优化的目的。
每次绘制时,CPU都需要调用drawcall而每个drawcall都需要很多准备工作,检测渲染状态、提交渲染数据、提交渲染状态;
当DrawCall过多,CPU就会很多额外开销用于准备工作,CPU本身负载,而这时GPU可能闲置了
降低DrawCall,CPU可以把绘制的数据一次提交给GPU,比多次提交性能要好,同时GPU绘制的时候一次处理的越多,GPU的吞吐量就越多,就越能发挥GPU的性能。
查看draw call数量和batch数量
很多时候我们需要Profile数据来查看我们渲染的场景的Draw call数量和Batch数量。Unity提供了Stats Pane、Profiler和Frame Debugger三个很方便的工具来帮助我们获得Batch相关数据。
切换到Game视图,点击Stats会弹出Stats pane:
通过Stats pane我们可以获得两个数据:
1.Batches – 目前绘制场景可视区域的Batch数量。
2.Saved by batching – 通过合批渲染我们减少的Batch(Draw call)数量。
Drawcall 和 Batch
Drawcall指图形API调用次数
Batch指用于画图的数据包的个数
有多少个Batch就要多少次Drawcall画出来,所以Batch数就是Drawcall数
合批方式
Unity合批的方式有静态合批,动态合批,与GPU Instancing合批,三种方式可以合并DrawCall。
合批合的是Mesh,我们能在屏幕上看到的一切都基于小小的mesh。我们目标是: 把尽可能多的mesh放在一起绘制
合批最重要的前提:材质必须相同
静态合批
- 使用方式
- PlayerSettings中开启static batching,然后对需要静态合批物体的Static打钩即可
- 解释
- 静态合批是将静态(不移动)GameObjects组合成大网格,然后进行绘制
- 被合批的模型必须是静态的物体,它们是不能被移动旋转和缩放的
- 静态批处理:游戏导出时,在player setting中勾选static batching,导出包的时候就进行批处理,导出来的包就会比较大。 在游戏场景中勾选场景的static选项,在加载场景时,会进行一次静态批处理合并,导出来的包不大,但加载时会使内存变大。
- 场景中有4个物体,如果都勾选静态选项,在进行静态批处理的时候,引擎会判断这4个物体是否共用同一渲染材质。如果共用同一渲染材质,则会将这4个物体视为可批处理的对象,引擎会基于单个渲染对象的大小拷贝出3个,共变为4个Mesh,这时4个Mesh会存在一个indexbuffer中,会让资源占用内存变大4倍,在渲染的时候,是将这个更大的mesh传递给GPU进行渲染操作的。
- 如果CPU的运行速度较慢,则GPU会出现等待CPU的情况,此时游戏主要受CPU的限制。 CPU在游戏中的主要作用是设置渲染状态和调用DC,如果每个物体的材质和贴图都不一样,CPU主要工作就是设置物体的渲染状态,调用DC也会更多,此时渲染状态的改变更消耗性能,游戏运行会变慢。所以,对于大量的不需要改变位置的物体,都会采用静态批处理的方式解决渲染状态的瓶颈。
动态合批
- 解释
- 动态合批是将一些足够小的网格,在CPU上转换它们的顶点,将许多相似的顶点组合在一起,并一次性绘制它们。
- 共用着相同的材质物体,Unity会自动进行批处理。
- 预设体的实例会自动地使用相同的网络模型和材质。
- 条件
- 900个顶点以下的模型。
- 如果我们使用了顶点坐标,法线,UV,那么就只能最多300个顶点。
- 如果我们使用了UV0,UV1,和切线,又更少了,只能最多150个顶点。
- 如果两个模型缩放大小不同,不能被合批的,即模型之间的缩放必须一致。
- 合并网格的材质球的实例必须相同。即材质球属性不能被区分对待,材质球对象实例必须是同一个。
- 如果他们有lightmap的数据,必须是相同的才有机会合批。
- 多个pass的Shader是绝对不会被合批。
- 延迟渲染是无法被合批。
GPU Instancing合批
- 优先级
静态合批>Instancing>动态合批,使用了高优先级的合批方式,低优先级的将不进行 - 特征
只能渲染相同的meshes,但是允许每个实例有不同的参数(颜色,scale..) - 与动态和静态合批的区别
- GPU Instancing 没有动态合批那样对网格数量的限制,也没有静态网格那样需要这么大的内存,它很好的弥补了这两者的缺陷
- 与动态和静态合批不同的是,GPU Instancing 并不通过对网格的合并操作来减少Drawcall,GPU Instancing 的处理过程是只提交一个模型网格让GPU绘制很多个地方,这些不同地方绘制的网格可以对缩放大小,旋转角度和坐标有不一样的操作,材质球虽然相同但材质球属性可以各自有各自的区别。
遮挡剔除与LOD
LOD(Level of detail)多层次细节,是最常用的游戏优化技术。它按照模型的位置和重要程度决定物体渲染的资源分配,降低非重要物体的面数和细节度,从而获得高效率的渲染运算。缺点是增加了内存。采用空间换时间
简单来说就是根据物体在游戏画面中所占视图的百分比来调用不同复杂度的模型的
- 定义一个空对象,添加LODGroup组件
- 分别将刚刚准备好的三种不同精度的模型,拖拽到空对象的LODGroup组件的各个级别上
- 把LOD下的三个子物体进行”对齐“处理。(将其相对于父物体的坐标置为0)
光照贴图烘焙
LightMap:就是指在三维软件里实现打好光,然后渲染把场景各表面的光照输出到贴图上,最后又通过引擎贴到场景上,这样就使物体有了光照的感觉。
脚本系统
减少GC开销
基本原则就是尽可能减少垃圾和减少GC过程中的开销。
- 不要显式调用System.GC.Collect()。很多情况下它会触发主GC,从而增加主GC的频率,也增加了间歇性停顿的次数。
- 可以定时执行GC操作,在不好产生卡顿的时候进行,如异步加载游戏场景时
- 尽量减少临时对象的使用。临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,减少了主GC的机会。
- 重复使用的类型进行缓存,即用一个变量存储
- 尽量使用StringBuffer,而不用String来累加字符串。string是固定长的字符串,累加string对象时,不是在一个string对象中扩增,而是重新创建新的string对象;stringbuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。
- 能使用基本类型int,long,就不用Integer,Long对象。基本类型变量占用内存资源比相应对象占用少的多。
- 尽量少用静态对象变量。