《Unity 3D游戏开发(第2版)》学习笔记
游戏脚本
自定义定时器
- CustomYieldInstruction类并重写keepWaiting属性。
- 保持协程暂停返回true。为了让协程进行执行返回false。
- 调用重写的函数,在MonoBehaviour.Update之后和MonoBehaviour.LateUpdate之后的每个帧中- 此类需要Unity 5.3或更高版本
样例
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
public class Test: MonoBehaviour
{
IEnumerator Start()
{
// 10秒后结束
yield return new CustomWait(10f, 1f, delegate () {
Debug.LogFormat("每过一秒回调一次 : {0}", Time.time);
});
Debug.Log("十秒结束");
}
}
public class CustomWait : CustomYieldInstruction
{
public override bool keepWaiting
{
get
{
//此方法返回false表示协程结束
if (Time.time - m_StartTime >= m_Time)
{
return false;
}
else if(Time.time - m_LastTime >= m_Interval)
{
//更新上一次间隔时间
m_LastTime = Time.time;
m_CallBack();
}
return true;
}
}
private float m_StartTime;
private float m_LastTime;
private float m_Interval;
private float m_Time;
private UnityAction m_CallBack;
public CustomWait(float time, float interval, UnityAction callback)
{
//记录开始时间
m_StartTime = Time.time;
//记录上一次回调的时间
m_LastTime = Time.time;
//记录间隔的时间
m_Interval = interval;
//记录总时间
m_Time = time;
//回调
m_CallBack = callback;
}
}
多线程
- 2018之后开放了工作线程(多线程)
- 主线程和工作线程的数据需要使用Native来传递
- 工作线程只需要读数据,所以在现在代码中标注是只读
样例:
TransformAccessArray transformAccessArray = new TransformAccessArray(cubes);
//启动工作线程
MyJob job = new MyJob() { position = position };
JobHandle jobHandle = job.Schedule(transformAccessArray);
//等待工作线程结束
jobHandle.Complete();
//结束
transformAccessArray.Dispose();
position.Dispose();
struct MyJob : IJobParallelForTransform
{
//只读
[ReadOnly] public NativeArray<Vector3> position;
public void Execute(int index, TransformAccess transform)
{
//工作线程中设置坐标
transform.position = position[index];
}
}
代码编译
编译顺序如下
- Plugins根目录 -> Assembly-CSharp-firstpass.dll
- Plugins/Editor -> Assembly-CSharp-Editor-firstpass.dll
- 根目录 -> Assembly-CSharp.dll
- Editor -> Assembly-CSharp-Editor.dll
生命周期
UGUI
渲染顺序
三层,顺序如下
Camera,先绘制depth低的相机下的物体,depth高的相机会覆盖depth低的
1. RenderQueue 2500以下(含2500)
sorting layer + Order in Layer,越小越优先,后者为前者子项
RenderQueue, 越小越优先
RenderQueue 相等,【由近到远】排序优先
z轴,远到近
2. RenderQueue 2500以下
sorting layer + Order in Layer,越小越优先,后者为前者子项
RenderQueue, 越小越优先
RenderQueue 相等,【由远到近】排序优先
z轴,远到近
3. 当一个RenderQueue在0~2500 ,一个在2501之后,那么此时SortingOrder失效,谁的RenderQueue更大,谁在前面
事件系统
Graphic Raycaaster,绑定在canvas上,表示下面所有UI元素支持的事件。可以挂在子控件上,只对某个子控件生效
通过继承IPointerEnterHandler来重写各个监听的方法
UI事件管理
可以通过UGUIEventListener.Get(gameObject).OnClick = func
的方式,进行统一管理,这个样例是对点击事件进行管理。
图片和文本的点击事件
继承EventSystems.EventTrigger
来实现OnPointerClick()
点击方法。
实现方式:重写UGUIEventListener方法
public class UGUIEventListener : UnityEngine.EventSystems.EventTrigger
{
public override void OnPointerClick(UnityEngine.EventSystems.PointerEventData eventData)
{
}
static public UGUIEventListener Get(GameObject go)
{
}
}
事件传递
UnityAction: 类似c#中的委托,可以实现方法的传递
UnityEvent: 类似c++中的信号,负责管理UnityAction。提供接口:
- AddListener
- RemoveListener
- RemoveAllListerners
*RaycastTarget优化(屏幕射线功能)
重写OnDrawGizmos()方法,找到所有RaycastTarget勾选的UI,然后画出线框,然后取消UI中勾选就可以了
UI穿透
EventSystem.current.RaycastAll(PointerEventData, func) 可以找到所有能传递点击事件的对象
EventEvents.Execute(gameObject, PointerEventData, func) 可以将事件传递给需要的对象
自适应
Canvas Scaler组件
Canvas优化问题
如果一个Canvas中的元素过多,每次更新UI,都需要重新合并Mesh,会很卡
如果Canvas数量过多,DrawCall会卡,因为每个Canvas会单独占用一个DrawCall
和NGUI的对比
NGUI与UGUI的区别
- 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一说,使用Sprite Packer
- UGUI的Navigation在Scene中能可视化
- UGUI的事件需要实现事件系统的接口,但写起来也算简单
各自的优缺点
- NGUI还保留着图集,需要进行图集的维护。而UGUI没有图集的概念,可以充分利用资源,避免重复资源。
- UGUI出现了锚点的概念,更方便屏幕自适应。
- NGUI支持图文混排,UGUI暂未发现支持此功能。
- UGUI没有 UIWrap 来循环 scrollview 内容。
- UGUI暂时没有Tween组件。
2D游戏
Tile更新
tilemap.GetTile()
tilemap.SetTile()
2D物理
物理引擎基于PhysX
碰撞效果发生条件:主动碰撞的物体,需要有Collider2D和Rigidbody2D,被碰撞的物体只需要有Collider2D
刚体
- Dynamic: 动态刚体,完全模拟物理效果,碰到Collider2D会被挡住。效率最低,仅适合主角使用
- Kinematic: 运动学,只能和选中Dynamic复选框的刚体发生碰撞效果。效率比上面那个好
- Static: 静态,只能和Dynamic发生碰撞效果,和Kinematic只能发生碰撞事件(需要勾选Use Full Kinematic Contact复选框,效率最高)
碰撞监听
- OnCollisionEnter2D
- OnCollisionStay2D
- OnCollisionExit2D
只触发事件,不触发效果
Is Trigger
动画系统
2017新加Playables,也支持脚本控制动画
2018新概念:Constraint,可以让平级的游戏对象互相依赖
状态机相关监听
- OnStateEnter,进入状态
- OnStateUpdate,状态更新,每帧调用
- OnStateExit,离开状态
- OnStateMove,处理动画根节点的位移
- OnStateIK,处理IK(反向动力学)动画
剧情动画
TimeLine编辑器
持久化数据
json支持字典
Unity的JSON是不支持字典的,不过可以继承ISerializationCallbackReceiver借口,间接实现字典序列化。
实现原理:分别序列化两个List元素来保存key-value。在OnBeforeSerialize()和OnAfterDeserialize()进行序列化和反序列化赋值操作。
游戏存档
PlayerPrefs
Unity自带的存档方法,会在应用程序将切入后台时统一保存文件,也可以强制调用PlayerPrefs.Save()保存。支持存储接口:
- SetInt(String, Int)
- SetFloat(String, Float)
- SetString(String, String)
EditorPrefs
编辑器模式下,Unity提供的一组存档功能。接口和上面一样,且是及时保存的。
TextAssert
unity提供的一个文本对象,用来读取Resources目录下的MyText文本
本地文件读写
c#的File类,需要注意下路径问题,编辑模式和打包的路径有区别
存储类型对比
- json: 方便,但是数据多了之后,可读性差
- XML: 比json可读性好一点
- YAML: 数据格式要求没那么严格(比如少些括号或者逗号),预览性和编辑性都非常好。可以用#来注释部分内容
静态对象
Lightmap
两类物体:一类是可发生位移变化的,使用实时光照计算;另一类是不可发生位移变化的,采用预先烘焙Lightmap
光源设置
windows - Lighting - Setting,可以打开烘焙面板
windows - Lighting - Light Explore, 管理光源
windows - Occlusion Culling, 设置最小遮挡距离、最小遮挡空隙和最小阈值(遮挡的对象,需要选中Occluder Static和Occludee Static选项)
遮挡事件
- OnBecameInvisible
- OnBecameVisible
手动遮挡:Renderer.isVisible
自动寻路
unity使用的是A*寻路
windows - Navigation 打开寻路面板,可以控制寻路的各个参数
public void Update()
{
if(Input.GetMouseButton(0)) {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit[] hits = Phytsics.RaycastAll(ray);
foreach(var hit in hits) {
string name = hit.collider.gameObject.name;
if (name == "Plane") {
// 移动方块
navMeshAgent.SetDestination(hit.point);
}
}
}
}
连接不相通的两点: OffMeshLink组件
检查路径是否合法: NavMesh.CalcuiatePath()
支持动态阻挡:Nav Mesh Obstacle组件
音频
Audio Source组件
- Spatial Blend: 0表示2D音频,1表示3D,0-1表示之间的插值音频
- Priority: 优先级,同时播放的音频有上限,超过后悔关闭最高的音频,一般bgm设置0
- Volume: 音量
- Stereo pan: 左右声道占比
- Pitch: 播放速度
资源加载与优化
Prefab实例化
SetParent会继承世界坐标,所以新建的实例不一定在原点
- GameObject.Instantiate: 只创建新对象,会丢失Prefab的引用
- PrefabUtility.InstantiatePrefab: 只能在编辑模式使用
资源卸载
- GameObject.Destroy()
- GameObject.DestroyImmediate(): 编辑模式用
上面两个方法只会卸载掉对象,它身上引用的贴图和Mesh还在内存中,如果需要卸载这些无用的资源,需要调用EditorUtility.UnloadUnusedAssetsImmediate()方法
ps: Unity这么做的原因是为了防止频繁的加载和卸载资源导致的IO阻塞
版本管理
需要管理的只有Assets、ProjectSetting中的所有文件(包括.meta文件)
删除资源
Resources.UnLoadUnusedAssets() + isDone
m_Operation = Resources.UnLoadUnusedAssets();
// 强制卸载对象引用的资源
// Resources.UnLoadAssets(g1);
if(m_Operation.isDone) {
m_Operation = null;
Debug.Log("资源卸载完成");
}
AB包压缩格式
- LZMA压缩:默认压缩方式,优点是非常小,缺点是每次使用都要解压,可能会有卡顿,不建议在项目中使用
- BuildAssetBundleOptions.UncompressedAssetBundle 不压缩:缺点是构建的AB包很大,优点是加载速度很快,可以通过第三方算法压缩,然后在解压
- BuildAssetBundleOptions.ChunkBasedCompression LZ4压缩:上面两种的折中方案,综合了优缺点,建议在项目中使用
加载流程
磁盘中加载AssetBundle对象 -> 从AB对象中加载资源对象 -> 从资源读取对象并且实例化到Hierarchy视图,变成游戏对象
卸载流程就是上面这个流程相反: Destroy删除游戏对象 -> 卸载资源 -> 卸载AB包
游戏资源管理
特殊的文件夹:
- Assets:游戏资源的顶层文件夹。AssetDatabase方法可以访问到里面的任意资源
- Editor:编辑模式下的代码需要放在这里。打包之后,会自动剥离它
- Editor Default Resources:编辑模式的资源
- Gizmos:放一些工具类图标,使用Gizmos.DrawIcon()方法来显示他们
- Plugins:优先编译成DLL文件
- Resources:资源目录,自动构建到包内,可以使用Resources.Load()加载资源
- Standard Assets:导入Unity标准资源的package文件夹,这里的脚本执行顺序会设置得靠前
- StreamingAssets:AB包适合放在这里,这里的资源不会被压缩或改变