Unity引擎介绍&高级特性
Unity引擎介绍&高级特性
Unity 3D 游戏引擎
Unity平台本身提供完善的软件解决方案,使用Unity引擎可用于创作、运营任何实时互动内容,支持平台包括移动端、PC主机增强现实AR设备和虚拟现实VR设备[16]。Unity3D游戏引擎适配平台广泛,可以很便利地将开发的产品移植到不同平台上,目前市面上的众多独立游戏,移动端游戏,数据可视化,建筑设计,动画制作都使用了Unity引擎,国内大部分中小型游戏公司大多使用其进行开发,许多大公司的产品,如《王者荣耀》、《原神》、《明日方舟》等,也使用Unity进行开发。Unity支持三种脚本编程语言:C#,Unity Script和Boo,其中C#是最常用的。
Unity编辑器和基本常用API类
Unity编辑器界面
Unity编辑器界面主要由菜单栏、工具栏,Scene、Game、Hierarchy、Inspector、Project窗口组成。
②工具栏,包含七个基本控件,每个控件与Editor的不同部分相关。
③Scene窗口是正在创建的世界的交互式视图。Scene视图可用于选择和定位景物、角色、摄像机、光源和所有其他类型的游戏对象。能够在Scene视图中选择、操作和修改对象。
④Game窗口从游戏中的摄像机渲染Game视图。该视图代表最终发布的游戏。需要使用一个或多个摄像机来控制玩家在玩游戏时实际看到的内容。
⑤Hierarchy窗口包含场景中的游戏对象。其中一些对象是资源文件的直接实例(如3D模型),其他则是预制件(prefab)的实例,这是构成游戏大部分内容的自定义对象。在场景中添加和删除对象时,这些对象也会在层级视图中相应显示和消失。
⑥Inspector窗口可以查看和编辑 Unity Editor 中几乎所有内容(包括物理游戏元素,如游戏对象、资源和材质)的属性和设置,以及Editor 内的设置和偏好设置。
⑦Project窗口可访问和管理属于项目的资源。
⑧Console 窗口显示 Unity 生成的错误、警告和其他消息。
基本常用API类
在Unity中常见API类有如下几种:Time类、Vector3结构体、Mathf类、资源、预制体(prefab)资源、GameObject类、Transform属性和方法
①Time类是Unity提供的时间类,用以记录和控制游戏项目中关于时间和时间缩放的相关操作。
②在Unity中,和向量有关的结构体有Vector2、Vector3、Vector4,对应不同维度的向量,其中 Vector3使用最广泛。
③Unity中封装了数学类Mathf,使用它可以轻松地解决复杂的数学公式,通常使用Clamp函数和Lerp函数来做范围约束和线性插值。
④Unity中的资源主要包含三类:模型、图片、音频。
⑤预制体资源是Unity开发过程中非常重要的一类特有资源,导入游戏内的资源在修改后就以prefab资源存储。Prefab还支持内嵌预制体,预制体变体,拆包,多级覆写等操作来对同一个游戏资源进行高效复用,可以理解为多态思想的一种实际应用。
⑥GameObject类是所有Unity场景中的基类,它继承于Object类。在Hierarchy窗口下的所有预制体和游戏对象在脚本中均以GameObject类型进行调用。
⑦Transform属性是多有游戏对象都有的一个组件,其通过position、rotation、scale确定了一个游戏对象的位置、大小和方向,通过与世界坐标和本地坐标联用,可以很方便的对游戏对象的物理位置信息进行操作,其API内包含了Translate,Find等方法用于对游戏对象进行移动、旋转、缩放、查找。
核心功能
① 物理引擎 主要包含刚体控制,刚体碰撞触发,角色控制器,鼠标事件,物理射线检测。
②动画系统 主要包含动画的录制,Avatar谷歌,动画状态机和动画重定向,动画混合树、动画遮罩与IK、状态机脚本的应用。
③粒子系统 可定制需要的粒子效果,如持续时间,大小,速度,颜色,重力影响,最大数量等。
④音频和视频 管理背景音乐和特效音乐,支持3D音效和常见音频视频格式,如MP3、OGG、WAV、AIFF、MOV、MP4、MPG等。
⑤导航网格寻路 建立场景后进行路径烘焙实现可通行区域,给角色添加Nav Mesh Agent组件后挂载控制脚本,完成路径探索功能,其背后原理为A*算法。
⑥UI系统 Unity的UI系统支持UGUI和NGUI,包含按钮、文本框、滑动条、复选框等多种常见交互模块,UGUI因其简单便捷被广泛使用,通过Canvs可以很方便地实现层级排序,结合锚点系统能够自适应不同分辨率的设备。
⑦数据存储 Unity可以通过自带的PlayerPrefs实现用户数据保存,同时其也支持使用TextAsset存储文本、Json、CSV、XML等数据格式进行本地保存。
C# 及其在Unity中的高级特性
泛型
C#语言从2.0版本开始引入泛型,其核心思想就是将算法从数据结构中抽象出来,使得预定义的操作能够作用于不同的类型,从而提高了程序的效率、通用性和类型安全性,进而简化整个编程模型。
泛型类
public class Test<T>
{
public void Print(T t)
{
Debug.Log(t);
}
}
.....
Test<int> test_1 = new Test<int>();
test_1.Print(5); //输出5
Test<string> test_2 = new Test<string>();
test_2.Print(“unity”); //输出unity
如上示例,使用T指定了Test类的一种特定数据类型,可以用于类的方法的参数指定,而此时T自适应的根据构造函数初始化时指定的数据类型进行参数定义,传int类型和string类型都可以实现相应的参数输出,重用了同一个方法。 在C#中,泛型类支持多个类型参数,每个参数只带一个抽象数据类型,即二元、多元泛型类,如下:
public class Test<L, R>
{
public void Print(L l,R r)
{
Debug.Log(l.ToString() + r.ToString());
}
}
泛型类的标识由名称和类型参数共同组成,因此类型参数区分不同的类型,如果在程序中定义普通类Pair、一元泛型类Pair和二元泛型类Pair<L,R>,它们分 别表示不同的类型,不会引起编译错误,泛型类的类型参数也能够区分不同的方法成员,但对于二元泛型类需要注意,如果两个类型参数相同在进行方法重载时会产生歧义。
泛型约束
在默认情况下,泛型中的类型参数可以被替换为任意类型,但这并不符合大部分应用场景,C#支持在泛型定义中通过where关键字来对类型参数进行限制。
在泛型类的定义中,类型限制是在where关键字后依次写上类型参数、冒号以及限制方法,可以使用 class Test<L,R> where L:struct where R:class 来对多个参数进行同时约束,其中struct关键字表示值类型,而class关键字表示引用类型。
泛型有五大约束:
①struct,值类型的约束,参数类型只可为值类型。
②class,引用类型的约束,参数类型只可为引用类型。
③new,无参构造函数的约束,只有new约束才能在泛型类内进行类型参数T的对象实例化。
class Test<T>where T: Animal//,new()
{
public Tt = new T;//此时会报错,变量类型“T”没有new约束
}
如果不在泛型类内new传进来的T在外界如果要访问泛型T的对象的成员必须在外界实例化,注意传进来的泛型T和泛型类本身是两个概念。
④T:类名,基类约束,传进来的T必须是该类自己或者其派生类。
⑤T:接口名,接口约束,类型参数T只能传入接口或者接口的派生接口。
泛型方法
方法名后加<>,使用和方法类相同。
public static void Swap<T>(T[] a, int i, int j)//泛型方法
{
T temp = a[i];
a[i] = a[j];a[j] = temp;
}
int[] a = { 1,2,3,4 };
Util.Swap<int>(a,0,2);//调用的是泛型方法
委托与事件
委托
委托是一种特殊的引用类型,它将方法也作为特殊的对象封装起来,从而将方法作为变量或参数进行传递。使用委托主要包含四步:
①定义委托类型。
②声明方法或功能。
③构建委托对象。
④传入方法的实参,委托执行。
通过委托调用当前类的静态方法,只要写出方法名即可。
Delegate1 fun1= new Delegate1 (Sub);
如果是调用外部类型的静态方法,那么应写出方法所属的类型。
Delegate1 fun1 = new Delegate1(Test.Div);
如果调用的是非静态方法,那么还需要指出方法所属的对象名;
Test test = new Test();
Delegate1 fun1 = new Delegate1(test.Div);
委托的本质是传入方法名作为参数,所以可以省略new,直接写委托对象 = 方法名。
通过对委托对象的+或-可以实现委托链,这样当存在多个方法时,不需要全部写出来,可以全部放进委托对象,通过多播委托调用一次将所有方法都执行。
//+=多播委托链:向被委托的对象dl:注册/添加多个方法
dl += Sub;
dl += Mul;
//-=多播委托链:向被委托的对象dl:移除/取消多个方法
d1-= Mul;
d1-= Sub;
除最后方法调用外,委托变量的使用和一般对象变量有本质的区别。同样可以将一组委托放在一个数组中,实际就是封装了多个委托。 需要说明的是:
①C#的委托机制将方法视为特殊的对象,并允许将其作为其他方法的参数或返回值进行传递。
②可以利用泛型来约束委托挂载的方法的参数和返回类型传入方法的实参,委托执行。
③非void委托挂载的方法带返回值,如果委托挂载了多个带返回值的方法,委托执行完毕后返回的是最后挂载的方法返回值。
④系统内置委托(Action不带返回值,Func可以多加一元泛型指定返回类型),最多支持16个泛型参数,不用手动定义委托名Action,直接构建委托对象并注册方法即可。
delegate void Action();无参,无返回值
delegate void Action<T> (T1arg
delegate void Action<T1,T2> (T1 arg1,T2 arg2);
delegate void Action<T1,T2,T3> (T1 arg1,T2 arg2,T3 arg3);
delegate void Action<T1,T2,T3,T4>(T1 arg1, T2 arg2,T3 arg3,T4 arg4);
⑤系统内置委托(特殊的委托Action不带返回值),最多支持16个泛型参数,不用手动定义委托名Action,直接构建委托对象并注册方法即可。
事件
委托的发布和订阅问题可以通过事件解决,C#提供了专门的事件以实现可靠的订阅发布,其做法是在发布的委托定义中加上event关键字,如果其他类型再使用该委托时,就只能将其放在复合操作符“+=”或“-=”的左侧。
public event LightEvent OnColorChange;//委托发布
简单来说,事件要求委托的执行必须在委托定义的类内(可以= 可以执行),在其他类里只能用+= -= 不能 = 不能执行(= 相当于覆盖之前所有注册的方法,不允许订阅的这样做)。
事件处理的步骤如下:
①定义委托类型。
②在订阅者类中定义委托处理方法。
③订阅者对象将其事件处理方法合并到发布者对象的委托成员上,发布者对象在触发托操作时自动调用订阅者对象的委托处理方法。
集合
集合ArrayList
ArrayList类是一种特殊的数组。通过添加和删除元素,就可以动态改变数组的长度,其支持自动改变大小的功能,可以灵活地插入、删除、访问元素,但跟一般的数组相比速度慢一些且不是类型安全的,主要方法如下。
public virtual int Add(object value); //将对象添加到ArrayList的结尾处。
public virtual void Insert(int index,object value);// //将元素插入ArrayList的指定索引处。
public virtual void RemoveAt(int index);// 移除ArrayList的指定索引处元素。
public virtual void Clear(); //从ArrayList中移除所有元素。
public virtual void Sort(); //对ArrayList或它的一部分中的元素进行排序。
public virtual void Reverse(); //将ArrayList或它的一部分中元素的顺序反转。
public virtual int IndexOf(Object value,int startIndex,int count); //查找指定的 Ob ject并返回ArrayList 中包含指定的索引开始并包含指定的元素数的元 素范围 内第一匹配的从零。
public virtual bool Contains(Object item); //确定某元素是否在ArrayList中开始的索引。
列表List
List和ArrayList的区别主要体现在两方面:1)List添加元素时需要对元素进行严格的检验,而ArrayList可以添加任何类型的元素2)List无需强制类型转换,也就不存在装箱拆箱,因为指定了List装的类型,使用泛型T,通过List指定装什么类型,API所有增删改查的方法参数数据类型都是T。
以一个实例来说明List泛型集合避免了隐式的装箱、拆箱,提高性能。
ArrayList list=new ArrayList();
list.Add(20); //装箱,list存放的是object类型元素,须将值类型转化为引用类 型。
int i=(int)list[0]; //拆箱,list[0]的类型是object,要赋值就得把引用类型转化为 值类型。
如果换成泛型编程,就不会有装箱和拆箱的性能损失。
List<int> list=new List<int>();
list.Add(20); //因为指定了用int来实例化,因此不必装箱。
int i=list[0]; //同样地,访问时也不需要拆箱
进一步的,List泛型集合是类型安全的,如果使用Debug.Log输出一个包含了string类型和int类型的集合,当使用foreach int访问ArrayList输出时,会发生异常报错,集合中不是所有元素都可以转换成int,虽然可以通过var类型来解决数据获取,但有时在开发中更希望在定义一个list时就指定其类型,保证了其类型一致,并降低装箱拆箱性能损失。
Hastable
根据键的哈希代码进行组织的键/值对的集合,不存在索引,不能排序,key不可以重复,唯一标识,value可以重复,其常见方法如下。
Add,将带有指定键和值的元素添加到Hashtable中。
Remove,从Hashtable中移除带有指定键的元素。
Clear,从Hashtable中移除所有元素。
Contains ,确定Hashtable是否包含特定键。
//创建哈希表类型对象ht
//非泛型集合;key和value:都是object类型,存在着装箱和拆箱,数据不安全// 以键值对的形式存放数据,不存在索引,不能排序
Hashtable ht = new Hashtable() ;
ht.Add(1,"周一");
ht.Add(2,"周二");
/ / Console.WriteLine(ht.Count);
字典
字典和Hastable的关系同List和ArrayList的关系类似,字典也是一类泛型集合,Dictionary只能存入定义时指定的类型,不像Hashtable会 把类型转换成object,存取起来比前者方便,效率更高, 因为不需要转换类型,因此不会出Hashtable里的转换 类型错误而报出程序异常。
Dictionary<int,string) ht = new Dictionary<int,string>0);//泛型集合,只能传入int类型的key ,传入string类型的value,数据类型安全,不存在装箱和拆箱
ht.Add(1,"zs");
栈和队列
栈Stack有两类:普通栈,存的是Object对象;泛型栈,使用约束栈中的数据类型。
Stack<T> stack = new Stack<T>();
Stack常用方法:
Push,获取Stack中包含的元素数。
Pop,移除并返回位于Stack顶部对象。
Peek,返回位于Stack顶部的对象但不将其移除。
队列Queue同样有两类:普通队列和泛型队列。
Queue<T> queue= new Queue <T>();
Enqueue,将对象添加到Queue的结尾处。 Dequeue,移除并返回位于Queue开始处的对象。 Peek,返回位于Queue开始处的对象但不将其移除。
反射
反射指程序可以访问、检测和修改它本身状态或行为的一种能力。程序集包含模块,模块包含类型,类型包含成员,反射提供了封装程序集、模块和类型的对象。可以使用反射动态的创建类型的实例,将类型绑定到现有对象,可以调用类型的方法或访问其字段和属性。
生命周期函数
在Unity中,所有被挂载在游戏对象上的脚本均派生自Monobehaviour类,其声明了游戏运行过程中的各类回调函数,如生命周期,获取组件,游戏对象操作等,其中最主要的也是大部分Unity脚本都有的方法是生命周期函数,主要有如下几种:
Awake,初始化时调用,Start方法之前,时序最优先。
OnEnable,脚本对象启用时调用,通常和OnDisable配合使用。
Start,启用脚本时的第一帧调用,仅一次。
Update,游戏运行时每一帧调用一次,每一帧时间根据机器性能和运行状态决定,有快有慢。
FixedUpdate,固定时间调用,默认设置下位0.02s,通常用于物理系统。
LateUpdate,在Update后执行,每帧调用一次,通常由于相机跟随。
OnDisable,脚本对象禁用时调用,可用于监听移除。
OnDestroy,对象存在的最后一帧调用,物体销毁或场景关闭时触发。
OnMouse相关函数,实现鼠标交互事件,包括鼠标进入,移除,拖拽等,是事件工具的底层核心。
协程
在游戏中的有关时间延迟效果的功能,如人物攻击间隔,固定时间生成物体等,需要利用到异步函数来进行时间控制。异步函数是在一个方法执行时调用另一个方法,而被调用的方法或者其中的某些语句并不立刻执行,而是过一段时间后才执行。在Unity中,实现异步函数有两种方法:Invoke和协程。
Invoke方法
Invoke是Unity3D的一种委托机制,把需要延时执行的方法托管给Invoke并通过计时器指定延时时间即可实现在游戏中的异步操作。
Invoke(string methodName, float time);//methodName:方法名,time:延时时间
InvokeRepeating(string methodName, float time, float repeatRate);//repeatRate:重 复执行间隔
协同程序Coroutine
在Unity中通常情况并不使用多线程,游戏大多是单线程,尽量避免在多个协程同时进行渲染工作,这会对设备造成较大负担,但可以通过协同程序来达到同样的效果,协同程序即在主程序运行时同时开启另一段逻辑处理,来协同当前程序的执行。使用协程可以完成框架中的异步加载和非Mono脚本托管生命周期函数等功能。
通常使用一个IEnumerator迭代器来封装一个Coroutine,可以通过以下方法开启/结束协程,遇到yield return会挂起,直到条件满足才会被唤醒继续执行后面的代码,故可以通过yield return指定延时的时长,也可以返回另一个协同程序。
IEnumerator Test
{
yield return new WaitForSeconds(1f);//1秒
Debug. Log(Time.time) ;
}
//方法内调用
IEnumerator enum = Test();
StartCoroutine(enum); //开启协程
StopCoroutine(enum);//关闭协程
需要注意的是,协程本身不是线程,协程通过反编译,它本质上还是在主线程上的优化手段,并不属于真正的多线程,二者的主要区别在于:
①一个具有多线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。
②一个具有多个协同程序的程序在任何时刻只能运行一个协同程序,并且正在运行的协同程序只会在其显示地挂起时, 它的执行才会暂停。
Unity独立游戏开发