游戏引擎基础

2026-04-18157次浏览

0评论

0收藏

0点赞

分享

一、Unity界面介绍

Unity界面大致如图1所示。分别对各个子窗口或区域进行介绍:

  • ①:标题区域,从左到右分别标明了Unity版本、License类别、当前激活的Scene文件,项目工程名,当前平台类型,图形渲染方案。其中,建议手游项目把平台类型切换为Android,具体方法为File→Build Setting→选择Android→Switch Platform。虽然编辑器不会因此在运行时为你在PC上创建Android的虚拟机或模拟器,但是代码中所有平台相关的参数都会以Android为准,以及诸如图片压缩方式等都会以Android的配置为准(图片压缩方式分平台进行配置),可以更大程度上贴近真机运行。但这种方法不会更改图形渲染方案(DX11)。
  • ②:菜单栏,包括窗口设置、物体生成、帮助等等功能,也可支持编辑器类自定义的菜单增加。
  • ③:主要包括Unity账号管理,编辑器窗口布局设置、场景可视类型筛选等功能。
  • 场景编辑视图:包含上方工具栏,可以简便地移动、旋转编辑镜头,修改GameObject的位置、旋转角度、尺寸等transform参数。
  • 游戏运行视图:以MainCamera为主视角,代表游戏实际运行时的镜头显示。
  • Hierarchy:当前场景层级面板,可以查看当前实例化的所有GameObject及其结构。
  • Project:项目工程资源目录面板,可以浏览项目工程Assets文件夹下,所有的资源文件。
  • Inspector:属性面板,点选GameObject出现,显示当前点选的Gameobject的所有组件及其参数属性,并可以进行组件增删、参数调节等操作。

图1 Unity引擎界面简介

二、Unity工程特殊目录

当我们新建一个Unity工程时,会自动生成如图2所示的目录结构:

图2 Unity工程的目录结构

Unity有一批保留相关逻辑或API的特殊目录,比如:

  • Assets:目录内所有的文件都可以在编辑器Project界面内访问到,并且可以用API获取到这一级目录。
  • Library:Unity会把Asset下支持的资源导入成自身识别的格式,以及编译代码成为DLL文件,都放在Library文件夹中。
  • ProjectSettings:编辑器中设置的各种参数
  • Packages:存放Unity的各种功能增强插件包的地方。

平时我们主要使用的是Assets目录,在其之中没有自动创建的子文件夹,但是我们可以自行创建一系列带保留逻辑的特殊目录:

  • Resources:资源存放文件夹,可以使用Resources.Load()方法把指定路径和名字的资源加载到内存使用。在打包的时候,Unity默认会把该目录中的所有资源打入AssetBundle,任何Assets下名为Resources的目录都遵循该逻辑,不论层级。
  • Editor:Unity编辑器自定义扩展程序目录,不论层级。Editor目录下的所有文件不会被打包。由于编辑器程序一般会引用UnityEditor命名空间,若被打包,会报错找不到UnityEditor,所以编辑器扩展程序都应该放在Editor中,以免报错导致打包失败。
  • StreamingAssets:一般是Assets的下一级目录,唯一。该文件夹也会在打包时全部打进包中,但它是“原封不动”的拷贝,一般用于存放视频文件、或者已经提前打成AB的资源之类的东西。

 

三、资源的“身份证”:meta文件

在Assets目录下,所有的目录和文件,都会由Unity自动生成同名的以.meta为扩展的文件。meta文件主要包含以下内容:

  • guid:重点中的重点,是Unity对该资源或目录的唯一标识和全局引用/查找依据。
  • ImportSetting:对于Import类型的资源,比如Image,FBX,记录其ImportSetting面板所有的参数配置,这也是导致meta文件有diff的主要原因。
  • assetBundleName/assetBundleVariant:Unity内可以对资源手动分配AssetBundle归属,这两个字段负责记录该手动配置。
  • (2018.3版本前)timeCreated:记录资源最后一次被修改时的时间戳(而非资源创建时间),基本没用,而且时常变化,很容易造成meta文件冲突。使用2018.3以前的版本,需要小心处理这个字段的DIFF,慎防漏传错传。

图3 一个典型的meta文件内容

guid是在资源被新建时就确定的,由Library/assetDatabase负责管理,即使手动删除meta文件,Unity再次自动生成时,guid也不会变化,同时也保证了全局唯一性,不会出现重复的问题,所以当发现guid有diff,应当审慎处理,从资源被创建开始,guid不应该会有变化,除非是手动把Library目录删掉了。在timeCreated属性被移除后,其实meta冲突的可能性已经下降不少,以下列举部分本人遇到过的meta文件在多人协作下遇到过的两个多发问题:

  1. 同学A上传了新建的资源X,但是忘记同时上传资源X.meta,同学B/C/...在Update Svn后,各人本地Unity工程各自生成了自己的X.meta,B/C/...开始在各个地方引用资源X,此时同学A补充上传X.meta,同学B/C/...的X.meta在更新后guid发生变化,相关引用位置全部变成missing。同时可能衍生出guid冲突的问题。
  2. 同学A对资源X的Import Setting进行修改,但没有上传X.meta,此后Update Svn时因其他人上传过同一位置修改,导致冲突,同学A没有处理冲突,并把X.meta保持冲突状态上传。其他人Update后,因meta维持冲突,导致文件在Unity中识别失败。

guid冲突可以通过Unity的Console发现,导入资源后,但凡有冲突的,Unity会以warning的形式打印出来,如图4所示:

图4 Unity导入资源时关于guid冲突的warning

 

四、核心概念:GameObject/Prefab/Scene

1、GameObject

GameObject时Unity中的基本操作对象,类似于UE4中的Actor。如前面所述,任何可以出现在Hierarche里的东西都是GameObject,包括灯光、镜头、3D物体、2D的UI、粒子系统等。GameObject都包含最基本的Transform组件,其中包含position、rotation和scale属性,对于2D GameObject,可能会以Rect Transform代替Transform。其余各种组件(Component)可以根据需要自行添加,包括Unity内置的各种组件,或者是使用者自己编写的,继承自Monobehaviour的C#脚本。

Monobehaviour类提供了多种API,可以通过gameObject和transform属性方便地调用,以获取当前GameObject的各项组件实时参数或状态。这里一个常见的问题是使用者新建了一个脚本,但是在编辑器中Add Component时,无法搜到它,导致无法添加到GameObject上。这种问题一般的原因是类没有继承Monobehaviour,可以着重检查以下。

2、Prefab

GameObject是实际在场景中作用的物体,而Prefab(预制体)则是GameObject的描述文件,或者说是母体/模板,我们可以利用Prefab,快速批量地、动态地生成GameObject。实际项目中一般都是先提前编辑好各种需要的GameObject,存成Prefab,运行时通过代码把Prefab加载到内存,并利用它实例化出GameObject。

需要说明的是,Prefab本身并不包含任何模型贴图等真正的素材文件,它只是一个虚拟的“参考”,它实质上是通过文本文件的形式,将GameObject的各项属性和引用等,记录下来。利用GameObject生成Prefab的方法很简单,直接从Hierarche拖动到Project里需要的目录就行,具体见图5。

图5 拖动GameObject生成Prefab

可以注意到,Hierarche中,不是由Prefab实例化而来的GameObject是灰色方块图标,否则就是浅蓝色方块图标,Cube生成对应Prefab后,其本身也变成了浅蓝色,表明是由Prefab-Cube实例化而来,此后对该Cube实例做的修改,都可以同步保存到Prefab-Cube中。

相反地,如果我们想在编辑器中直接依据某Prefab生成Gameobject,则反向拖动,由Project窗口拖动到Hierarchy里即可。

在2018.3之后,Unity的Prefab系统大改,增添了诸如Nested Prefab,Prefab Override,Prefab Variant,功能强大了不少,但操作复杂度也略有增加,由于这部分内容较多,这里暂不做具体介绍,读者可以自行了解。 

3、Scene

Scene也是可以实际存在于硬盘中的一种Unity内置的文件类型,后缀名为.scene。Scene是场景单元,一个场景单元可以保存其所挂载的所有GameObject状态及参数(不一定是通过Prefab生成的),一旦加载便以最后保存时的状态展示,场景可以通过Application.LoadLevel系列函数进行不同场景文件间的切换。场景切换会把隶属于当前场景的所有GameObject统一销毁,如果希望保留部分GameObject,应在切换前,使用DontDestroyOnLoad()方法,对其进行保护。DontDestryOnLoad会创建一个独立的Scene,其中的GameObject不会随着切场景被销毁,如图6所示。

图6 DontDestroyOnLoad创建独立的Scene

需要说明的是,切场景不一定需要依赖多个Scene文件和LoadLevel方法完成,本人曾经任职的项目组就是从头到尾单一Scene的架构,其中所有的切场景都是完全手动指定的GameObject销毁和创建。多Scene在场景管理上更加清晰和方便,但需要注意数据同步的问题,数据尽量不要依赖于GameObject存储,如果实在需要,应使用DontDestroyOnLoad。而单Scene则需要自行管理好当前场景中所有的物体,确保切场景时不存在错清漏清的情况。两种方案各有优点,可根据实际情况选取。

 

五、简单3D模型

模型与Renderer

简单3D模型,包括可以通过Unity直接创建的所有3D GameObject,如立方体、球体、椭圆体、平面等。3D模型主要使用Mesh进行渲染,简单3D模型生成后都会带有Mesh Filter和Mesh Renderer组件。前者指定了该物体的Mesh,值得一说的是,该Component的Mesh可以通过代码指定顶点数据和UV等参数后动态生成并赋值,而简单3D模型生成时都会自带Unity默认的对应模型Mesh。而后者则是使用Mesh Filter所指定的Mesh作为输入,并使用指定的Material(材质球)进行渲染。因此,Mesh Filter和Mesh Renderer都是成对出现的。Renderer对于需要需要显示在游戏里的GameObject来说是必不可少的,Unity对于场景GameObject的渲染处理逻辑是遍历场景中所有的GameObject并取含有Renderer的部分作渲染。对于3D模型是Mesh Renderer或者Skinned Mesh Renderer,对于2D的UI来说则是Canvas Renderer。除此以外还有其他类型的Renderer,这里不一一赘述。

 

图7 一个简单立方体的Components

Material与Shader

如果我们需要自定义一个模型的渲染方式,一般是使用Shader(着色器)。在Unity中,我们需要先在Project窗口右键,生成Shader文件,Unity的Shader使用CG语言,新建之后已经自带一些默认代码,这里对于Shader逻辑编写和CG语言不做具体描述,主要介绍下Shader索引路径:

 

图8 Shader索引路径

由于我们创建的Shader,需要配套地创建Metarial(材质球)才能使用,为材质球指定我们自定义的Shader时,就会用上这一路径:

 

图9 为材质球指定自定义的Shader

此后,再把该材质球绑定到Mesh Renderer中,就能让对应的3D模型按自定义Shader的逻辑进行绘制。

 

六、2D(UI)

在Unity4.6版本以前,没有一个官方的UI解决方案,最著名的第三方UI解决方案是NGUI。此后4.6版本,官方UI方案——UGUI出现,UGUI很多方面都参考了NGUI,但在此后很长一段时间的更新中,UGUI作为官方原生支持的UI方案,具有了更多的优势。由于笔者的项目经验仅限于UGUI,本文后续的UI介绍也以UGUI为主,望见谅。

Canvas

Canvas(画布)是UGUI所有功能性UI组件所必须的根节点。若我们直接在Hierarchy中创建一个UI GameObject(下面简称UI),但场景中未有含有Canvas组件的GameObject,则Unity会自动帮我们创建一层名为Canvas的GameObject,其中就含有包括Canvas组件,并自动地把我们要创建的UI组件挂载在Canvas下作为子节点。

一个自动创建的Canvas GameObject如图10所示:

图10 一个自动生成的Canvas GameObject

Canvas负责其子节点下所有带有Canvas Renderer的UI的渲染(UI创建的时候都默认带有Canvas Renderer),而不挂在Canvas下,或者不带Canvas Renderer的UI,都是无法被绘制的。

对于Canvas,其中有个参数是Render Mode,这是一个三选一的选项参数,其中三个参数分别解释如下:

  • Screen Space-Overlay:Canvas下所有的UI永远位于屏幕最前面,不需额外指定UI的摄像机。此时UI不由某一具体的摄像机负责绘制,哪怕没有任何一个摄像机,UI都能绘制。由于该模式下,UI的高度不可控,实际项目中很少使用,对于一些必须固定在最高层级的UI,可以使用。
  • Screen Space-Camera:这个模式需要指定摄像机,相机投射获取到的Canvas界面即是最终显示结果。一般而言,UI相机都需要和主相机有所区分。这种模式下相机和Canvas有一定的距离,中间可以插入例子特效、或者UI上显示模型等。这是游戏系统UI最常见的模式。
  • World Space:该模式下,Canvas及其所有子UI视为场景中的普通的3D物体,可以用于作为跟随模型的UI——比如血条之类的显示。

Graphic RayCaster

Canvas必备组件之一,用于UI的射线检测,当点击或其他输入事件发生时,会检查该Canvas上哪个UI被射线碰撞到。

Event System

与Canvas类似地,EventSystem也是不可或缺的。但与Canvas不同的是,Canvas可以存在多个,但EventSystem时全局唯一的。创建UI时,Unity也会检查并自动创建带EventSystem的GameObject。

图11 自动创建的Event System GameObjct

Event System主要负责对整个输入事件系统管理,并进行事件分发。

StandaloneInputModule

BaseInputModule是一个基类,StandaloneInputModule是Unity自带的对该基类的一个继承实现,负责接收用户输入,是整个Event System的输入源。用户也可以实现自己的InputModule。

事件监听者:UI功能组件(以按钮为例)

我们已经有了输入事件的生产者EventSystem,但为了开发与用户输入事件相关的逻辑,我们还需要有输入事件的监听者。这里以按钮的点击事件为例,以如下步骤可添加一个事件监听者:

  1. 创建Monobehaviour脚本,编写监听函数,要求公开级别为public
  2. 创建按钮UI
  3. 把脚本添加到按钮GameObject上
  4. 给Button组件的OnClick()属性添加监听,如图12所示。

图12 给Button OnClick事件添加监听者

 

七、动画系统

Unity的动画系统主要由三部分组成:

  • Animation:基本的动画片段,是动画表现的主体。
  • Animator Controller:动画状态机,每个状态即是一个Animation,Controller可以配置状态之间的转换逻辑
  • Animator:Component,挂载在GameObject上, 引用Animator Controller,可以通过脚本逻辑驱动Controller状态切换。

Animation

最基本的动画单元,可以用关键帧KEY动画,或者使用曲线来KEY,可以在任意有效持续时间内的帧上添加事件帧(白色长条),并绑定相关GameObject上的脚本方法:

 

图13 Animation的关键帧视图和曲线视图

Animator Controller:

Animator Controller的重点在于状态跳转的配置,状态跳转主要有两种类型:播放结束自动跳转/参数控制跳转。其中前者通过状态跳转配置中勾选Has Exit Time并填写Exit Time,并且保持Condition为空即可。动画片段播放持续到Exit Time后,将开始跳转。后者主要靠Condition中的条件完成跳转,这个要求Controller有相关的控制参数(Parameters),我们可以配置多种类型的参数,并且在跳转中设置相应的条件,然后通过脚本进行参数的取值控制或触发,以逻辑驱动状态跳转。当然我们也可以同时填写Condition和勾选Has Exit Time,这种情况下,跳转发生则需要同时满足两个条件:参数条件符合,播放长度达Exit Time。

另外,使用者可以配置动画跳转的融合,如图14右所示,使用者可以通过Transition Duration、Transition Offset配置融合参数,也可以通过下边的图形直接拖动配置,【需要注意的是,跳转源动画在融合区间结束时间点之后的部分A,以及跳转目标动画在融合区间开始时间点之前的部分B,是不会被播放的,意味着A和B内的事件帧是无法被触发的,真实案例】。所以安全起见,不建议把UI清理或交互阻塞等关键逻辑建立在动画事件帧上,因为你无法保证它一定被执行!

 

图14 Animator Controller及其状态跳转配置

Animator

Animator是挂载在GameObject上的Component,需要指定Animator Controller才能播放动画,若Animator在GameObject被激活/生成时即处于Enabled状态,则动画的默认状态(图16左,黄色状态框)会立即播放,若不希望Animator自动播放,应当先把Animator设置为Disabled,按需激活。另外Animator对象可以通过脚本获取,通过相关API设置Controller内对应参数,或者直接播放指定状态等,具体可自行参考Unity Script指引。

 

八、脚本开发

所有的Unity API可以参考: https://docs.unity3d.com/2018.4/Documentation/ScriptReference/  (注意选择Unity版本)

这里主要介绍以下GameObject全生命周期的Mono类方法,和序列化相关。

GameObject全生命周期中的Mono类方法:

  • Awake:最早执行,GameObject被激活后,对象的数据和状态初始化后,只触发一次。不激活不调用。
  • Start:脚本第一次调用Update之前调用,只触发一次。
  • OnEnable:GameObject由Disabled(未激活)变为Enabled状态时触发,可多次触发。
  • OnDisable:激活变为未激活时触发,可多次触发。
  • Update:每帧调用。
  • FixedUpdate:固定更新,默认情况下0.02秒一次,可以通过TimeManager进行配置。
  • LateUpdate:每帧调用,但顺序在所有Update中最后。
  • OnDestroy:该组件被销毁,或对应GameObject被销毁时调用,只有在其被激活过后才会触发。(由于OnDestroy不一定被调用,建议清理逻辑放在OnDisable里,而不是OnDestroy,真实案例)

序列化:

序列化是Unity对于脚本组件参数支持图形化编辑的一种解决方案。一般而言,脚本类中定义为public或带[SerializeField]的可序列化对象,会出现在Inspector里,开放给使用者直接赋值和编辑,相当于暴露参数。对于序列化的域,有如下要点:

  • public,或带[SerializeField]的自定义非抽象类/结构体
  • 非static,非const,非readonly
  • 可以被实例化的其他域类型
  • 泛型、字典、高维数组、委托等不能被序列化
  • 序列化对自定义类型的空引用和多态支持不好(空引用会自动构造对象,多态则是List<BaseClass>序列化派生类时只会序列化基类的信息。)
  • Prefab的大小会因应其挂载的脚本序列化的域数量变大而变大,哪怕增加的域是空引用。所以对于广泛使用的脚本类(比如所有UI或所有模型都需要挂的一个基类)而言,应节制地使用序列化,以免导致资源体积不必要地增大。

而对于字典这种无法被序列化的类型,我们也可以使用ISerializationCallbackReceiver这个接口实现间接的序列化,具体可以参考Unity官方给的例子:https://docs.unity3d.com/2018.4/Documentation/ScriptReference/ISerializationCallbackReceiver.html

其他常用的UnityAPI相关的类:

  • Component类,可以访问、增加同GameObject下的指定Component,比如gameObject.GetComponent<T>(), gameObject.AddComponent<T>()
  • GameObject类,实例化Prefab,删除、查找GameObject、Component等,比如GameObject.Instantiate()。
  • Transform类,访问GameObject的transform属性、或其父/子节点。
  • PlayerPrefs类,数据库与存储,可以本地持久化存储一些简单类型的值,比如:
    1 PlayerPrefs.SetFloat(“Score”,0.1f);
    2 PlayerPrefs.Save();
    3 float a = PlayerPrefs.GetFloat(“Score”);

评论 0

0/1000