Unity 基础 目前状态 在学习过 Unity 入门后,你已经掌握的内容
Unity 引擎的工作原理
能够熟练使用 Unity 引擎提供的各个重要组件
能够熟练使用 Unity 引擎提供的 API
主要学习内容 知识点
Unity 中必备的 3D 数学知识
Unity 中的核心系统和组件以及 APl
实践小项目
窥探如何制作商业游戏———配置文件
主要学习方式 理论+习题+实践
理论:语法操作相关知识
习题:基于知识点的针对性习题
实践:基于知识点的小项目实践
学习建议 重视基础知识点
多思考多练习
==切忌浮躁==
Unity 基础当中都是实用又重要的基础知识,必须都要掌握
3D 数学基础 Mathf 知识点
UnityEngine.Mathf - Unity 脚本 API
知识点一 Mathf 和 Math Math 是 C#中封装好的用于数学计算的工具类 —— 位于 System 命名空间中
Mathf 是 Unity 中封装好的用于数学计算的工具结构体 —— 位于 UnityEngine 命名空间中
他们都是提供来用于进行数学相关计算的
知识点二 他们的区别 Mathf 和 Math 中的相关方法几乎一样
Math 是 C#自带的工具类,主要就提供一些数学相关计算方法
Mathf 是 Unity 专门封装的,不仅包含 Math 中的方法,还多了一些适用于游戏开发的方法
所以我们在进行 Unity 游戏开发时,使用 Mathf 中的方法用于数学计算即可。
知识点三 Mathf 中的常用方法——一般计算一次 1.π - PI 1 2 public const float PI = (float )Math.PI;
2.取绝对值 - Abs 有不同重载,支持多种数值类型
1 2 3 4 public static int Abs (int value ){ return Math.Abs(value ); }
3.向上取整 - Ceil,CeilToInt 注意返回值
1 2 3 4 5 6 7 8 9 10 11 12 print(Mathf.Ceil(1.3f )); print(Mathf.CeilToInt(1.3f )); public static float Ceil (float f ){ return (float )Math.Ceiling(f); } public static int CeilToInt (float f ){ return (int )Math.Ceiling(f); }
4.向下取整 - Floor,FloorToInt 注意返回值
1 2 3 4 5 6 7 8 9 10 11 12 print(Mathf.Floor(1.6f )); print(Mathf.FloorToInt(1.6f )); public static float Floor (float f ){ return (float )Math.Floor(f); } public static int FloorToInt (float f ){ return (int )Math.Floor(f); }
5.钳制函数 - Clamp,Clamp01 传递一个值,和一个区间;如果这个值不在这个区间,返回边界值,否则返回本身。
对于 int 和 float 有两个重载。
Clamp01:只传递一个值,边界值为 0 和 1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 print(Mathf.Clamp(10 , 11 , 20 )); print(Mathf.Clamp(13 , 11 , 20 )); print(Mathf.Clamp(20 , 11 , 20 )); print(Mathf.Clamp01(-1 )); print(Mathf.Clamp01(0.2f )); print(Mathf.Clamp01(2 )); public static int Clamp (int value , int min, int max ){ if (value < min) { value = min; } else if (value > max) { value = max; } return value ; } public static float Clamp01 (float value ){ if (value < 0f ) { return 0f ; } if (value > 1f ) { return 1f ; } return value ; }
6.获取最大值 - Max 多个数值是使用变长参数实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 print(Mathf.Max(1 , 2 , 3 , 4 )); print(Mathf.Max(1 , 2 )); public static int Max (int a, int b ){ return (a > b) ? a : b; } public static int Max (params int [] values ){ int num = values.Length; if (num == 0 ) { return 0 ; } int num2 = values[0 ]; for (int i = 1 ; i < num; i++) { if (values[i] > num2) { num2 = values[i]; } } return num2; }
7.获取最小值 - Min 多个数值是使用变长参数实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 print(Mathf.Min(1 , 2 , 3 , 4 , 545 , 6 , 1123 , 123 )); print(Mathf.Min(1.1f , 0.4f )); public static int Min (int a, int b ){ return (a < b) ? a : b; } public static int Min (params int [] values ){ int num = values.Length; if (num == 0 ) { return 0 ; } int num2 = values[0 ]; for (int i = 1 ; i < num; i++) { if (values[i] < num2) { num2 = values[i]; } } return num2; }
8.一个数的 n 次幂 - Pow 1 2 3 4 5 6 7 print(Mathf.Pow(4 , 2 )); print(Mathf.Pow(2 , 3 )); public static float Pow (float f, float p ){ return (float )Math.Pow(f, p); }
9.四舍五入 - Round,RoundToInt 1 2 3 4 5 6 7 8 9 10 11 12 print(Mathf.RoundToInt(1.2f )); print(Mathf.Round(1.5f )); public static float Round (float f ){ return (float )Math.Round(f); } public static int RoundToInt (float f ){ return (int )Math.Round(f); }
10.返回一个数的平方根 - Sqrt 1 2 3 4 5 6 7 print(Mathf.Sqrt(2f )); print(Mathf.Sqrt(3f )); public static float Sqrt (float f ){ return (float )Math.Sqrt(f); }
11.判断一个数是否是 2 的 n 次方 - IsPowerOfTwo 1 2 3 4 5 6 print(Mathf.IsPowerOfTwo(8 )); print(Mathf.IsPowerOfTwo(1 )); print(Mathf.IsPowerOfTwo(3 )); print(Mathf.IsPowerOfTwo(2 )); public static extern bool IsPowerOfTwo (int value ) ;
12.判断正负数,返回它的符号 - Sign 1 2 3 4 5 6 7 8 print(Mathf.Sign(-1 )); print(Mathf.Sign(2 )); print(Mathf.Sign(0 )); public static float Sign (float f ){ return (f >= 0f ) ? 1f : (-1f ); }
知识点四 Mathf 中的常用方法 Lerp,一般使用其不停计算 插值运算 - Lerp Lerp 函数公式:
1 2 result = start + (end - start) * t result = Mathf.Lerp(start, end, t);
t 为插值系数,取值范围为 0~1
插值运算用法一 每帧改变 start 的值——变化速度先快后慢,位置无限接近,但是不会得到 end 位置。
因为 start 一直在变大,end - start 的值一直在变小。
1 start = Mathf.Lerp(start, 10 , Time.dletatime);
插值运算用法二 每帧改变 t 的值——变化速度匀速,位置每帧接近,当 t >= 1 时,得到结果。
因为 start 不变,end - start 的值由 time 决定,当 time 为 1 时,到达 end。
1 2 time += Time.deltaTime; result = Mathf.Lerp(start, 10 , time);
练习题 使用线性插值实现一个方块跟随另一个方块移动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Lesson1_p : MonoBehaviour { public Transform targetObj; public Transform followObj; public float moveSpeed = 2 ; Vector3 tempPos; float time = 0 ; void Start () { tempPos = followObj.position; } void Update () { Vector3 newPos = followObj.position; newPos.x = Mathf.Lerp(followObj.position.x, targetObj.position.x, Time.deltaTime); newPos.y = Mathf.Lerp(followObj.position.y, targetObj.position.y, Time.deltaTime); newPos.z = Mathf.Lerp(followObj.position.z, targetObj.position.z, Time.deltaTime); followObj.position = newPos; targetObj.Translate(Vector3.forward * Time.deltaTime * moveSpeed); } }
三角函数 知识点 知识点一 弧度、角度相互转化 角度和弧度都是度量角的单位 角度:1°
弧度:1 radian
圆一周的角度:360°
圆一周的弧度:2 派(Π) radian
角度和弧度的转换关系 1 rad = 180°
1 rad = (180 / 派)° => 1 rad = 180/ 3.14 ≈ 57.3°
1°= (派 / 180) rad => 1°= 3.14 / 180 ≈ 0.01745 rad
由此可以得出 弧度 * 57.3 = 对应角度
角度 * 0.01745 = 对应弧度
Unity 当中的角度弧度转换 使用 Mathf.Deg2Rad 和 Mathf.Rad2Deg
1 2 3 4 5 6 7 8 9 float rad = 1 ;float anger = rad * Mathf.Rad2Deg;print(anger); anger = 1 ; rad = anger * Mathf.Deg2Rad; print(rad);
知识点二 三角函数 正弦函数 Sin:对边比斜边,A / C
余弦函数 Cos:邻边比斜边,B / C
正切函数 Tan:对边比邻边,A / B
常用特殊度数正弦余弦值
0
30
45
60
90
180
270
360
Sin
0
1 / 2
根号 2 / 2
根号 3 / 2
1
0
-1
0
Cos
1
根号 3 / 2
根号 2 / 2
1 / 2
0
-1
0
1
Tan
0
根号 3 / 3
1
根号 3
0
0
Unity 当中的三角函数 传入一个弧度,返回对应的函数值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static float Sin (float f ){ return (float )Math.Sin(f); } public static float Cos (float f ){ return (float )Math.Cos(f); } public static float Tan (float f ){ return (float )Math.Tan(f); } print(Mathf.Sin(30 * Mathf.Deg2Rad)); print(Mathf.Cos(30 * Mathf.Deg2Rad) * 2 ); print(Mathf.Tan(30 * Mathf.Deg2Rad) * 3 );
知识点三 反三角函数 反三角函数
反三角函数是初等函数之一
包括反正弦函数、反余弦函数等
作用:通过反三角函数计算正弦值或余弦值对应的==弧度值==
Unity 当中的反三角函数 使用 Asin,Acos,Atan,传入一个正、余弦值,正切值,返回对应的弧度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static float Asin (float f ){ return (float )Math.Asin(f); } public static float Acos (float f ){ return (float )Math.Acos(f); } public static float Atan (float f ){ return (float )Math.Atan(f); } print(Mathf.Asin(0.5f ) * Mathf.Rad2Deg); print(Mathf.Acos(0.8660254f ) * Mathf.Rad2Deg); print(Mathf.Atan(0.5773503f ) * Mathf.Rad2Deg);
总结
三角函数———Mathf.Sin(弧度)、Mathf.Cos(弧度)
角度和弧度———Mathf.Rad2Deg、Mathf.Deg2Rad
三角函数曲线——Sin 和 Cos 函数曲线对于我们的意义
反三角函数———Mathf.Asin(正弦值)、Mathf.Acos(余弦值)
练习题 实现一个物体按曲线移动(正弦或者余弦曲线)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Lesson2_p : MonoBehaviour { public Transform runObj; public float moveSpeedDeg; Vector3 newPos; void Start () { newPos = runObj.position; moveSpeedDeg = 1 ; } void Update () { newPos.x += moveSpeedDeg * Mathf.Deg2Rad; newPos.y = Mathf.Sin(newPos.x); runObj.position = newPos; } }
坐标系 知识点 知识点一 世界坐标系 原点:世界的中心点
轴向:世界坐标系的三个轴向是固定的
Unity 当中获取世界坐标相关 目前学习的和世界坐标系相关的,修改他们 会是相对世界坐标系的变化。
1 2 3 4 this .transform.position;this .transform.rotation;this .transform.eulerAngles;this .transform.lossyScale;
知识点二 物体坐标系 原点:物体的中心点(建模时决定)
轴向:
物体右方为 x 轴正方向
物体上方为 y 轴正方向
物体前方为 z 轴正方向
Unity 当中获取本地坐标 相对父对象的物体坐标系的位置的本地坐标,相对坐标。
修改他们,会是相对父对象物体坐标系的变化。
加了 local 的就是本地坐标
1 2 3 4 this .transform.localPosition;this .transform.localEulerAngles;this .transform.localRotation;this .transform.localScale;
知识点三 屏幕坐标系 原点:屏幕左下角
轴向:
最大宽高:
Screen.width
Screen.height
Unity 当中获取屏幕坐标 通过鼠标位置获取屏幕坐标
1 2 3 Input.mousePosition; Screen.width; Screen.height;
知识点四 视口坐标系 原点:屏幕左下角
轴向:
特点:
和屏幕坐标类似,只不过是将坐标单位化,变成了比例。
Unity 当中的视口坐标 就是摄像机上的 ViewPort Rect;
x,y,width,height 都是 0~1。
坐标转换相关
UnityEngine.Camera - Unity 脚本 API
世界转本地
1 2 3 this .transform.InverseTransformDirectionthis .transform.InverseTransformPointthis .transform.InverseTransformVector
本地转世界
1 2 3 this .transform.TransformDirectionthis .transform.TransformPointthis .transform.TransformVector
世界转屏幕
1 Camera.main.WorldToScreenPoint
屏幕转世界
1 Camera.main.ScreenToWorldPoint
世界转视口
1 Camera.main.WorldToViewportPoint
视口转世界
1 Camera.main.ViewportToWorldPoint
视口转屏幕
1 Camera.main.ViewportToScreenPoint
屏幕转视口
1 Camera.main.ScreenToViewportPoint
向量模长和单位向量 知识点 知识点一 向量 标量:有数值大小,没有方向
向量:有数值大小,有方向的矢量(一维、二维、三维)
注意:向量在空间中有无数条可以随意移动
Unity 当中的向量 Unity 当中,使用三维、二维向量,即 Vector3 和 Vector2
Vector2、Vector3 即可以代表一个点,也可以代表一个方向。
1 2 print(this .transform.position); print(this .transform.forward);
知识点二 两点决定一向量 A(点)向量:(x1,y1)
B(点)向量:(x2,y2)
则向量 AB 为 B-A,即(x2 - x1,y2 - y1);
向量 BA 为 A - B,即(x1 - x2,y1 - y2)。
1 2 3 4 5 6 7 8 9 Vector3 A = new Vector3(1 , 0 , 0 ); Vector3 B = new Vector3(2 , 0 , 0 ); Vector3 AB = B - A; Vector3 BA = A - B; print(A); print(B); print(AB); print(BA);
对于我们的意义:要求 B 物体在 A 物体什么方向,我们就可以使用 B 物体的 position - A 物体的 position,得到一个 AB 向量,即一个方向。
知识点三 零向量和负向量 零向量(0,0,0)
零向量是唯一一个大小为 0 的向量
负向量
(x,y,z)的负向量为(-x,-y,-z)
负向量和原向量大小相等
负向量和原向量方向相反
1 2 3 print(Vector3.zero); print(Vector3.forward); print(-Vector3.forward);
知识点四 向量的模长 向量的模长就是向量的长度
向量是由两个点算出,所以向量的模长就是两个点的距离
模长公式:A 向量(x,y,z),则模长 = 根号(x^2 + y ^ 2 + z ^ 2)。
Vector3 中提供了获取向量模长的成员属性:magnitude
1 2 3 4 5 6 7 print(AB.magnitude); Vector3 C = new Vector3(5 , 6 , 7 ); print(C.magnitude); print(Vector3.Distance(A, B)); public float magnitude => (float )Math.Sqrt(x * x + y * y + z * z);
知识点五 单位向量 模长为 1 的向量为单位向量
任意一个向量经过归 ─ 化就是单位向量
只需要方向,不想让模长影响计算结果时使用单位向量
归一化公式:
A 向量(x,y,z),模长 = 根号(x^2 + y ^ 2 + z ^ 2)
单位向量 = ( x / 模长,y / 模长,z / 模长)
Vector3 中提供了获取单位向量的成员属性:normalized
1 2 print(AB.normalized); print(AB / AB.magnitude);
总结
Vector3 这边变量,可以表示一个点,也可以表示一个向量,具体表示什么,是根据我们的具体需求和逻辑决定。
如何在 Unity 里面,终点减起点就可以得到向量,点 C 也可以代表向量,代表的就是 OC 向量,O 是坐标系原点。
得到了向量,就可以利用 Vector3 中提供的成员属性得到模长和单位向量。
模长相当于可以得到两点之间的距离 ,单位向量主要是用来进行移动计算的,它不会影响我们想要的移动效果。
练习题
Unity 中判断两点之间举例有几种方式?
使用 Vector3.Distance(Vector3, Vector3)方法
计算 AB 向量或者 BA 向量的模
计算向量(3,4,5)的模长(手写)
根号(3 ^ 2 + 4 ^ 2 + 5 ^ 2) = 根号(9 + 16 + 25) = 根号 50。
计算向量(3,-4)的单位向量(手写)
模长 = 根号(3 ^ 2 + (-4) ^ 2) = 5
单位向量:(3 / 5, -4 / 5)
向量加减乘除 知识点 知识点一 向量加法 向量 A(x1,y1,z1),向量 B(x2,y2,z2)
A + B = (x1 + x2, y1 + y2, z1 + z2)
几何意义:
位置 + 位置:两个位置相加,一般没有任何意义。
向量 + 向量:两个向量相加得到一个新的向量
位置 + 向量:位置加向量,得到一个新的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 this .transform.position += new Vector3(1 , 2 , 3 );this .transform.Translate(Vector3.forward * 5 );public void Translate (Vector3 translation, [DefaultValue("Space.Self" )] Space relativeTo){ if (relativeTo == Space.World) { position += translation; } else { position += TransformDirection(translation); } }
知识点二 向量减法 向量 A(x1,y1,z1),向量 B(x2,y2,z2)
A + B = (x1 - x2, y1 - y2, z1 - z2)
几何意义:
位置 - 位置:两个位置相减,得到一个新的向量
向量 - 向量:两个向量相减得到一个新的向量
向量相减,头连头,尾指尾
A - B = B 头指 A 头
位置 - 向量:位置减向量,得到一个新的位置
向量 - 位置:一般情况下,没啥意义
1 2 this .transform.position -= new Vector3(1 , 2 , 3 );this .transform.Translate(-Vector3.forward * 5 );
知识点三 向量乘除标量 向量只会和标量进行乘除法运算
向量 A(x,y,z)
标量 a
A * a = (x * a, y * a, z * a)
A / a = (x / a, y / a,z / a)
几何意义:
向量 * or / 标量 = 向量
向量 * or / 正数,方向不变,放大缩小模长
向量 * or / 负数,方向相反,放大缩小模长
向量 * 0,得到零向量
注意:全局缩放lossyScale
不可修改,需要修改缩放,只能修改 localScale
1 2 this .transform.localScale *= 2 ;this .transform.localScale /= 2 ;
总结
向量加法——主要用于位置平移和向量计算
向量减法——主要用于位置平移和向量计算
向量乘除法——主要用于模长放大缩小
练习题 用向量相关知识,实现摄像机跟随(摄像机不设置为对象子物体)
摄像机一直在物体的后方 4 米,向上偏 7 米的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Lesson5 : MonoBehaviour { public Transform targetTransfrom; private Vector3 lastPos; void Start () { Vector3 pos = targetTransfrom.position - targetTransfrom.forward * 4 + targetTransfrom.up * 7 ; Camera.main.transform.position = pos; Camera.main.transform.LookAt(targetTransfrom); lastPos = targetTransfrom.position; } private void LateUpdate () { if (lastPos != targetTransfrom.position) { Vector3 pos = targetTransfrom.position - targetTransfrom.forward * 4 + targetTransfrom.up * 7 ; Camera.main.transform.position = pos; Camera.main.transform.LookAt(targetTransfrom); lastPos = targetTransfrom.position; } } }
向量点乘 知识点 点乘计算公式 向量 A (Xa,Ya,Za)
向量 B(Xb,Yb,Zb)
A · B = Xa * Xb + Ya * Yb + Za * Zb
向量 · 向量 = 标量
点乘几何意义 点乘可以得到:一个向量在自己向量上投影的长度。
点乘结果 > 0 两个向量夹角为锐角
;
点乘结果 = 0 两个向量夹角为直角
;
点乘结果 < 0 两个向量夹角为钝角
;
我们可以用这个规律判断敌方的大致方位。
补充知识 调试画线 使用 Debug.DrawLine 和 Debug.DrawDRay,可以画出辅助线和辅助射线。
1 2 3 4 5 public static void DrawLine (Vector3 start, Vector3 end, Color color ) ;public static void DrawLine (Vector3 start, Vector3 end, Color color, float duration ) ;public static void DrawRay (Vector3 start, Vector3 dir, Color color ) ;public static void DrawRay (Vector3 start, Vector3 dir, Color color, float duration ) ;
知识点一 通过点乘判断对象方位 首先,获取自己的向量即 this.transform.position,对方的向量 targetTransform.position。
求得 AB 向量,使用Vector3.Dot
,计算当前的正朝向即 this.transform.forward 和 AB 向量的点乘结果。
如果大于 0,为锐角,即在前方;小于 0,为钝角,即在后方。
注意,这里求方向需要使用的是当前的面朝向向量,和 AB 向量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Debug.DrawRay(this .transform.position, this .transform.forward, Color.red); Debug.DrawRay(this .transform.position, targetTransform.position - transform.position, Color.red); float dotRet = Vector3.Dot(transform.forward, targetTransform.position - transform.position);if (dotRet >= 0f ){ print("target 在我前面" ); } else { print("target 在我后面" ); } public static float Dot (Vector3 lhs, Vector3 rhs ){ return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z; }
知识点二 通过点乘推导公式算出夹角 公式推导
Cosβ = 直角边 / 单位向量模长
直角边 = Cosβ * 单位向量模长
直角边 = 单位向量 A · 单位向量 B
Cosβ * 单位向量 B 模长 = 单位向量 A · 单位向量 B
Cosβ = 单位向量 A · 单位向量 B
推出结果:β = ArcCos(单位向量 A · 单位向量 B)。
Unity 当中使用公式算出夹角 我们可以使用上述公式计算出夹角,也可以直接使用Vector3.Angle
直接求出两向量的夹角。
两者的结果是一致的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 dotRet = Vector3.Dot(transform.forward, (targetTransform.position - transform.position).normalized); print("计算角度为:" + Mathf.Acos(dotRet) * Mathf.Rad2Deg); print("api计算角度为:" + Vector3.Angle(transform.forward, targetTransform.position - transform.position)); public static float Angle (Vector3 from , Vector3 to ){ float num = (float )Math.Sqrt(from .sqrMagnitude * to.sqrMagnitude); if (num < 1E-15 f) { return 0f ; } float num2 = Mathf.Clamp(Dot(from , to) / num, -1f , 1f ); return (float )Math.Acos(num2) * 57.29578f ; }
总结 向量点乘对于我们的意义
判断对象的大致方位
计算两个向量之间的夹角
练习题 当一个物体 B 在物体 A 前方 45 度角范围内,并且离 A 只有 5 米距离时,在控制台打印“发现入侵者”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Lesson6_p : MonoBehaviour { public Transform targetTransform; void Update () { #region DrawRay Debug.DrawRay(this .transform.position, this .transform.forward, Color.red); Debug.DrawRay(this .transform.position, targetTransform.position - this .transform.position, Color.red); #endregion #region 使用公式一步步算 Vector3 AB = targetTransform.position - this .transform.position; float distance = Mathf.Sqrt(AB.x * AB.x + AB.y * AB.y + AB.z * AB.z); float abModLen = Mathf.Sqrt(AB.x * AB.x + AB.y * AB.y + AB.z * AB.z); AB = new Vector3(AB.x / abModLen, AB.y / abModLen, AB.z / abModLen); float dotRet = this .transform.forward.x * AB.x + this .transform.forward.y * AB.y + this .transform.forward.z * AB.z; float angle = Mathf.Acos(dotRet) * Mathf.Rad2Deg; if (distance < 5 && angle >= 0f && angle <= 45f ) { print($"计算方式1--发现入侵者,距离:{distance} ,角度:{angle} " ); } #endregion #region 使用API计算Cosβ dotRet = Vector3.Dot(this .transform.forward, (targetTransform.position - this .transform.position).normalized); angle = Mathf.Acos(dotRet) * Mathf.Rad2Deg; if (distance < 5 && angle >= 0f && angle <= 45f ) { print($"计算方式2--发现入侵者,距离:{distance} ,角度:{angle} " ); } #endregion #region 使用API直接计算夹角 angle = Vector3.Angle(this .transform.forward, (targetTransform.position - this .transform.position)); if (distance < 5 && angle >= 0f && angle <= 45f ) { print($"计算方式3--发现入侵者,距离:{distance} ,角度:{angle} " ); } #endregion } }
向量叉乘 知识点 知识点一 叉乘计算 向量 x 向量 = 向量
向量 A (Xa,Ya,Za)
向量 B(Xb,Yb,Zb)
Ax B =(X,Y,Z)
X = Ya * Zb - Za * Yb
Y = Za * Xb - Xa * Zb
Z = Xa * Yb - Ya * Xb
Unity 当中向量叉乘计算 使用 APIVector3.Cross
1 print(Vector3.Cross(a.position, b.position));
叉乘的几何意义 A x B 得到的向量,叫做法向量
同时垂直 A 和 B
A x B 向量垂直于 A 和 B 组成的平面
A x B = -(B x A)
假设向量 A 和 B 都在 XZ 平面上
向量 A 叉乘 向量 B
y 大于 0 证明 B 在 A 右侧
y 小于 0 证明 B 在 A 左侧
总结 向量叉乘对于我们的意义
得到一个平面的法向量
得到两个向量之间的左右位置关系
练习题
判断一个物体 B 位置再另一个物体 A 的位置的左上,左下,右上,右下哪个方位。
当一个物体 B 在物体 A 左前方 20 度角或右前方 30 度范围内,并且离 A 只有 5 米距离时,在控制台打印”发现入侵者”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Lesson7_p : MonoBehaviour { public Transform a; public Transform b; private Vector3 c; void Update () { c = Vector3.Cross(a.position, b.position); float angle = Vector3.Angle(a.forward, b.position - a.position); if (c.y <= 0 && angle >= 0f && angle < 90f ) { print("b 在 a的左上,角度:" + angle); if (angle < 20f ) { print("发现入侵者" ); } } else if (c.y <= 0 && angle >= 90f && angle <= 180f ) { print("b 在 a的左下,角度:" + angle); } if (c.y > 0 && angle >= 0f && angle < 90f ) { print("b 在 a的右上,角度:" + angle); if (angle < 30f ) { print("发现入侵者" ); } } else if (c.y > 0 && angle >= 90f && angle <= 180f ) { print("b 在 a的右下,角度:" + angle); } } }
唐老狮答案 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 using System.Collections;using System.Collections.Generic;using UnityEngine;public class FindEnemy2 : MonoBehaviour { public Transform A; public Transform B; private float dotResult; private Vector3 crossResult; void Update () { dotResult = Vector3.Dot(A.forward, B.position - A.position); crossResult = Vector3.Cross(A.forward, B.position - A.position); if (dotResult >= 0 ) { if (crossResult.y >= 0 ) { print("右前" ); } else { print("左前" ); } } else { if (crossResult.y >= 0 ) { print("右后" ); } else { print("左后" ); } } if ( Vector3.Distance(A.position, B.position) <= 5 ) { if ( crossResult.y >= 0 && Vector3.Angle(A.forward, B.position - A.position) <= 30 || crossResult.y < 0 && Vector3.Angle(A.forward, B.position - A.position) <= 20 ) { print("发现入侵者" ); } } } }
向量插值运算 知识点 知识点一 线性插值 对两个点进行插值运算
公式:result = start + (end - start) * t
1.先快后慢 每帧改变 start 位置 位置无限接近 但不会得到 end 位置 1 a.position = Vector3.Lerp(a.position, target.position, Time.deltaTime);
2.匀速 每帧改变时间 当 t>=1 时 得到结果 注意:如果目标物体位置改变了,则需要重置参数。
1 2 3 4 5 6 7 8 9 b.position = Vector3.Lerp(startPos, nowTarget, time); time += Time.deltaTime; if (target.position != nowTarget){ nowTarget = target.position; startPos = b.position; time = 0 ; }
知识点二 球性插值 线性和球形插值的区别 线性:直接平移,直线轨迹
球性:旋转并移动,弧形轨迹
在 Unity 当中,使用Vector3.Slerp
,进行球性插值计算。
1 c.position = Vector3.Slerp(c.position, target.position, Time.deltaTime);
总结
线性插值——用于跟随移动,摄像机跟随。
球形插值——用于曲线运动,模拟太阳运动弧线。
练习题 第一题 用线性插值相关知识,实现摄像机跟随(摄像机不设置为对象子物体)
摄像机一直在物体的后方 4 米,向上偏 7 米的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Lesson8_P : MonoBehaviour { public Transform target; private float time; private Vector3 nowTargetPos; private Vector3 startPos; void Start () { nowTargetPos = target.position - target.forward * 4 + target.up * 7 ; startPos = Camera.main.transform.position; } private void LateUpdate () { Camera.main.transform.LookAt(target); Camera.main.transform.position = Vector3.Lerp(startPos, nowTargetPos, time); time += Time.deltaTime; if (nowTargetPos != (target.position - target.forward * 4 + target.up * 7 )) { time = 0 ; startPos = Camera.main.transform.position; nowTargetPos = target.position - target.forward * 4 + target.up * 7 ; } } }
第二题 通过球性插值模拟太阳的升降变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 using System.Collections;using System.Collections.Generic;using UnityEngine;public class SunRiseAndDown : MonoBehaviour { public Transform leftPos; public Transform rightPos; public Transform sun; private bool moveLeft; private Vector3 startPos; private float time; void Start () { moveLeft = true ; startPos = sun.position; time = 0 ; } void Update () { float leftDistance = Vector3.Distance(sun.position, leftPos.position) - 1E-1 f; float rightDistance = Vector3.Distance(sun.position, rightPos.position) - 1E-1 f; if (leftDistance < 1E-1 f || rightDistance < 1E-1 f) { moveLeft = !moveLeft; startPos = sun.position; time = 0 ; } time += Time.deltaTime / 2 ; if (moveLeft) { sun.position = Vector3.Slerp(startPos, leftPos.position + Vector3.up * 0.1f , time * 0.3f ); } else { sun.position = Vector3.Slerp(startPos, rightPos.position + Vector3.up * 0.1f , time * 0.3f ); } } }
为何使用四元数 知识点 欧拉角 由三个角度(x,y,z)组成
在特定坐标系下用于描述物体的旋转量
空间中的任意旋转都可以分解成绕
三个互相垂直轴的三个旋转角组成的序列
欧拉角旋转约定 heading-pitch-bank:是一种最常用的旋转序列约定
Y-X-Z 约定
heading:物体绕自身的对象坐标系的 Y 轴,旋转的角度
pitch:物体绕自身的对象坐标系的 X 轴,旋转的角度
bank:物体绕自身的对象坐标系的 Z 轴,旋转的角度
Unity 中的欧拉角 lnspector 窗口中调节的 Rotation 就是欧拉角;
this.transform.eulerAngles
得到的就是欧拉角角度。
欧拉角的优缺点 优点:
直观、易理解;存储空间小(三个数表示);
可以进行从一个方向到另一个方向旋转大于 180 度的角度
缺点:
万向节死锁 当某个特定轴达到某个特殊值时,绕一个轴旋转可能会覆盖住另一个轴的旋转,从而失去一维自由度。
Unity 中 X 轴达到 90 度时,会产生万向节死锁,此时旋转 y 轴和 z 轴,都只会绕着 z 轴旋转。
总结 因为欧拉角存在一些缺点
同一旋转的表示不唯一
万向节死锁
而四元数旋转不存在万向节死锁问题,因此在计算机中我们往往使用四元数来表示三维空间中的旋转信息。
四元数是什么 知识点 四元数的概念 四元数是简单的超复数,由实数加上三个虚数单位组成,主要用于在三维空间中表示旋转。
四元数原理包含大量数学相关知识,较为复杂比如 ∶ 复数、四维空间等等;
因此此处我们只对其基本构成和基本公式进行讲解,如想深入了解数学原理请从数学层面去查找资料了解它。
四元数构成 一个四元数包含一个标量和一个 3D 向量
[w,v],w 为标量,v 为 3D 向量
[w,(x,y,z)]
对于给定的任意一个四元数:表示 3D 空间中的一个旋转量
轴-角对 在 3D 空间中,任意旋转都可以表示绕着某个轴旋转一个旋转角得到
注意:该轴并不是空间中的 x,y,z 轴而是任意一个轴
对于给定旋转,假设为绕着 n 轴,旋转 β 度,n 轴为(x,y,z)
那么可以构成四元数为
四元数 Q=[cos(β/2),sin(β / 2) n]
四元数 Q= [cos(β / 2),sin(β / 2) x,sin(β / 2) y,sin( p / 2) z]
四元数 Q 则表示绕着轴 n,旋转 β 度的旋转量
Unity 当中的四元数 Quaternion
是 Unity 中表示四元数的结构体
Unity 中的四元数初始化方法 轴角对公式初始化
四元数 Q= [cos(β / 2),sin(β / 2) x,sin(β / 2) y,sin( p / 2) z]
1 Quaternion q = new Quaternion(sin(β/2 )x, sin(β/2 )y, sin(β/2 )z,cos(β/2 ));
轴角对方法初始化
四元数 Q = Quaternion.AngleAxis(角度,轴);
1 Quaternion q = Quaternion.AngleAxis(60 , Vector3.right);
知识点一 四元数 Quaternion 初始化了一个四元数,表示绕 x 轴旋转 60 度。
1 2 3 4 Quaternion q = new Quaternion(Mathf.Sin(30 * Mathf.Deg2Rad) * 1 , 0 , 0 , Mathf.Cos(30 * Mathf.Deg2Rad)); GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube); obj.transform.rotation = q;
轴角对初始化方法
Quaternion-AngleAxis - Unity 脚本 API
1 Quaternion q2 = Quaternion.AngleAxis(60 , Vector3.right);
知识点二 四元数和欧拉角转换 四元数和欧拉角转换
欧拉角转四元数
四元数转欧拉角
1 2 Quaternion q; q.eulerAngles
1 2 3 4 print(Quaternion.Euler(q.eulerAngles)); print(q.eulerAngles);
知识点三 四元数弥补的欧拉角缺点 欧拉角缺点:
同一旋转的表示不唯一
万向节死锁
必备知识点:四元数相乘代表旋转四元数
同一旋转的表示不唯一 由于欧拉角的性质,90 度的旋转和 450 的旋转时一致的,表示不唯一。
而四元数的旋转,只会在[0,180],[0,-180]之间
万向节死锁 我们想让物体绕 y 轴转,但是由于万向节死锁,物体会绕着 z 轴转。
1 2 3 4 5 6 7 this .transform.eulerAngles = Vector3.right * 90 ;Vector3 e = this .transform.eulerAngles; e += Vector3.up; this .transform.rotation = Quaternion.Euler(e);
使用四元数解决
四元数相乘,表示一个旋转。
注意:这里的旋转是相对于本地坐标系的
1 this .transform.rotation *= Quaternion.AngleAxis(1 , Vector3.up);
总结
四元数构成——[cos(β / 2),sin(β / 2) x,sin(β / 2) y,sin( p / 2) z]
Unity 中的四元数——Quaternion
四元数弥补了欧拉角的缺点—同一旋转的表示不唯一、万向节死锁
注意:我们一般不会直接通过四元数的 w,x,y,z 进行修改
四元数常用方法 知识点
UnityEngine.Quaternion - Unity 脚本 API
知识点一 单位四元数 单位四元数表示没有旋转量(角位移)
当角度为 0 或者 360 度时
对于给定轴都会得到单位四元数
按给定公式理解,Q = [cos(β / 2), sin(β / 2), sin(β / 2), sin(β / 2)]
[1,(0,0,0)] 和 [-1,(0,0,0)] 都是单位四元数表示没有旋转量。
Unity 当中,使用Quaternion.identity
1 2 print(Quaternion.identity); testObj.rotation = Quaternion.identity;
何时使用:初始化对象时,可以使用单位四元数
1 Instantiate(testObj, Vector3.zero, Quaternion.identity);
知识点二 插值运算
Quaternion-Lerp - Unity 脚本 API
Quaternion-Slerp - Unity 脚本 API
四元数中同样提供如同 Vector3 的插值运算
Lerp 和 Slerp
在四元数中 Lerp 和 Slerp 只有一些细微差别
由于算法不同
Slerp 的效果会好一些
Lerp 的效果相比 Slerp 更快,但是如果旋转范围较大效果较差。
所以建议使用 Slerp 进行插值运算
1 2 3 4 a.rotation = Quaternion.Slerp(a.rotation, targetTransform.rotation, Time.deltaTime); time += Time.deltaTime; b.rotation = Quaternion.Slerp(start, targetTransform.rotation, time);
知识点三 LookRotation,向量指向转四元数 1 Quaternino.LookRotation(面朝向量);
LookRoataion 方法可以将传入的面朝向量转换为对应的四元数角度信息
举例:当人物面朝向想要改变时,只需要把目标面朝向传入该函数,便可以得到目标四元数角度信息。
之后将人物四元数角度信息改为得到的信息即可达到转向
练习题 第一题 利用四元数的 LookRotation 方法,实现 LookAt 的效果
1 a.rotation = Quaternion.LookRotation(b.position - a.position);
第二题 将之前摄像机移动的练习题中的 LookAt 换成 LookRotation 实现并且通过 Slerp 来缓慢看向玩家
1 2 3 Quaternion tempQuat = Quaternion.LookRotation(player.position - Camera.main.transform.position); Camera.main.transform.rotation = Quaternion.Slerp(Camera.main.transform.rotation, tempQuat, Time.deltaTime);
四元数计算 知识点 知识点一 四元数相乘
Quaternion-operator * - Unity 脚本 API
q3 = q1 * q2
两个四元数相乘得到一个新的四元数,代表两个旋转量的叠加,相当于旋转。
注意:旋转相对的坐标系是物体==自身坐标系==
1 2 3 Quaternion q = Quaternion.AngleAxis(60 , Vector3.up); this .transform.rotation *= q;this .transform.rotation *= q;
知识点二 四元数乘向量 v2 = q1 * v1
四元数乘向量返回一个新向量,可以将指定向量旋转对应四元数的旋转量,相当于旋转向量。
注意:四元数乘向量,必须四元数在前,向量在后,因为 Quaternion 没有实现向量在前的方法
1 2 3 4 5 6 Vector3 forward = Vector3.forward; print(forward); forward = Quaternion.AngleAxis(45 , Vector3.up) * forward; print(forward); forward = Quaternion.AngleAxis(45 , Vector3.up) * forward; print(forward);
使用场景:飞机发射子弹,已知飞机朝向,只需要一个四元数乘上朝向向量,就可以得到不同方向,就可以在不同方向发射子弹。
练习题 第一题 用目前所学知识,模拟飞机发射不同类型子弹的方法
单发,双发,扇形,环形
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 using System.Collections;using System.Collections.Generic;using UnityEngine;enum E_Bullet_Type{ Single, Double, Fan, Annular } public class AirPlane : MonoBehaviour { public GameObject bulletPrefab; E_Bullet_Type bulletType = E_Bullet_Type.Single; void Update () { if (Input.GetKeyDown(KeyCode.Alpha1)) { bulletType = E_Bullet_Type.Single; } if (Input.GetKeyDown(KeyCode.Alpha2)) { bulletType = E_Bullet_Type.Double; } if (Input.GetKeyDown(KeyCode.Alpha3)) { bulletType = E_Bullet_Type.Fan; } if (Input.GetKeyDown(KeyCode.Alpha4)) { bulletType = E_Bullet_Type.Annular; } if (Input.GetMouseButtonDown(0 )) { Fire(); } } void Fire () { switch (bulletType) { case E_Bullet_Type.Single: Vector3 pos1 = this .transform.position + this .transform.forward; Instantiate(bulletPrefab, pos1, this .transform.rotation); break ; case E_Bullet_Type.Double: Vector3 leftPos = this .transform.position + this .transform.forward + Vector3.left; Vector3 rightPos = this .transform.position + this .transform.forward + Vector3.right; Instantiate(bulletPrefab, leftPos, this .transform.rotation); Instantiate(bulletPrefab, rightPos, this .transform.rotation); break ; case E_Bullet_Type.Fan: for (float angle = -90 ; angle <= 90 ; angle += 30 ) { GameObject bullet = Instantiate(bulletPrefab); bullet.transform.position = this .transform.position; bullet.transform.rotation = this .transform.rotation; bullet.transform.forward = Quaternion.AngleAxis(angle, this .transform.up) * this .transform.forward; } break ; case E_Bullet_Type.Annular: for (float angle = -180 ; angle <= 180 ; angle += 30 ) { GameObject bullet = Instantiate(bulletPrefab); bullet.transform.position = this .transform.position; bullet.transform.rotation = this .transform.rotation; bullet.transform.forward = Quaternion.AngleAxis(angle, this .transform.up) * this .transform.forward; } break ; default : break ; } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Bullet : MonoBehaviour { public float moveSpeed; void Start () { Destroy(this .gameObject, 3 ); } void Update () { this .transform.Translate(Vector3.forward * Time.deltaTime * moveSpeed); } }
第二题 用所学 3D 数学知识实现摄像机跟随效果
摄像机在人物斜后方,通过角度控制倾斜率
通过鼠标滚轮可以控制摄像机距离人物的距离(有最大最小限制)
摄像机看向人物头顶上方一个位置(可调节)
Vector3.Lerp 实现相机跟随人物
Quaternino.Slerp 实现摄像机朝向过渡效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 using System.Collections;using System.Collections.Generic;using UnityEngine;public class CameraMove : MonoBehaviour { public Transform target; public float maxDistance = 10 ; public float minDistance = 3 ; public float batter = 45 ; public float headOffset = 1 ; public float distance = 5 ; public float rotateSpeed = 1 ; public float lookAtSpeed = 1 ; public float moveSpeed = 1 ; private Vector3 nowPos; private Vector3 nowDir; private void LateUpdate () { distance += Input.GetAxis("Mouse ScrollWheel" ) * rotateSpeed; distance = Mathf.Clamp(distance, minDistance, maxDistance); nowPos = target.position + target.up * headOffset; nowDir = Quaternion.AngleAxis(batter, target.right) * -target.forward; nowPos = nowPos + nowDir * distance; this .transform.position = Vector3.Lerp(this .transform.position, nowPos, Time.deltaTime * moveSpeed); Debug.DrawLine(this .transform.position, target.position + target.up * headOffset); this .transform.rotation = Quaternion.Slerp(this .transform.rotation, Quaternion.LookRotation(-nowDir), Time.deltaTime * lookAtSpeed); } }
Mono 中的重要功能 延迟函数 知识点
UnityEngine.MonoBehaviour - Unity 脚本 API
为了更好的性能,请使用携程
知识点一 什么是延迟函数 延迟函数顾名思义,就是会延时执行的函数
我们可以自己设定延时要执行的函数和具体延时的时间
是 MonoBehaviour 基类中实现好的方法
知识点二 延迟函数的使用
MonoBehaviour-Invoke - Unity 脚本 API
MonoBehaviour-InvokeRepeating - Unity 脚本 API
1.延迟函数 使用Invoke(funcNam, time)
传入一个函数名,函数将在 time 秒后执行。
1 2 3 4 5 6 7 8 9 10 11 12 Invoke("Func" , 5 ); void Fun (){ print("Func call" ); Fun(2 ); } void Fun (int i ){ print(i); }
注意:
延时函数第一个参数传入的是函数名字符串
延时函数没办法传入参数,只有包裹一层
就是在一个无参函数里面调用有参函数,然后 Invoke 调用这个无参函数。
函数名必须是该脚本上声明的函数
2.延迟重复执行函数 在 time
秒后调用 methodName
方法,然后每 repeatRate
秒调用一次。
1 2 InvokeRepeating("Func" , 5 , 2 ); public void InvokeRepeating (string methodName, float time, float repeatRate ) ;
3.取消延迟函数
MonoBehaviour-CancelInvoke - Unity 脚本 API
不带参数,取消该脚本上的所有延时函数执行。
1 2 CancelInvoke(); public void CancelInvoke () ;
带参数,指定函数取消执行
1 2 CancelInvoke("Fun" ); public void CancelInvoke (string methodName )
4.判断是否有延迟函数
MonoBehaviour-IsInvoking - Unity 脚本 API
1 2 3 4 5 6 7 8 9 10 if (IsInvoking() && IsInvoking("Fun" )){ print("yes" ); } else { print("no" ); } public bool IsInvoking () ;public bool IsInvoking (string methodName ) ;
知识点三 延迟函数受对象失活销毁影响 脚本依附对象失活或者脚本自己失活,延迟函数可以继续执行,不会受到影响。
脚本依附对象销毁或者脚本移除,延迟函数无法继续执行。
所以,通常在生命周期函数 OnEnable 当中开启延迟函数,在 OnDisable 当中关闭延迟函数。
1 2 3 4 5 6 7 8 9 private void OnEnable (){ } private void OnDisable (){ }
练习题 第一题 利用延时函数实现一个计秒器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Lesson13_P : MonoBehaviour { private float time = 0 ; private void OnEnable () { InvokeRepeating("TimeCounter" , 0 , 1 ); } private void OnDisable () { time = 0 ; CancelInvoke(); } void TimeCounter () { print($"Now is {time} seconds" ); ++time; } }
第二题 请用两种方式延时销毁一个指定对象
1 Destory(this .gameObject, 4 );
1 2 3 4 5 6 7 8 9 10 11 Invoke("DestroyObj" , 0 ); void DestroyObj (){ DestroyObj(10 ); } void DestroyObj (float time ){ Destroy(this .gameObject); }
#协同程序 知识点
知识点一 Unity 是否支持多线程? 首先要明确一点,Unity 是支持多线程的,只是新开线程无法访问 Unity 相关对象的内容。
下面的代码是可以在 Unity 运行的。
注意:
Unity 中的多线程,要记住关闭。因为只要 Unity 编辑器没有关闭,线程就不会结束。
Unity 当中使用多线程有使用限制,大部分 Unity 内容不可在多线程当中访问
虽然不能使用 Unity 当中的大部分内容,但是可以使用多线程来进行复杂计算,将计算结果放到公共成员当中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 using System.Collections;using System.Collections.Generic;using System.Threading;using UnityEngine;public class Lesson14 : MonoBehaviour { private Queue<int > que = new Queue<int >(); Thread t; void Start () { t = new Thread(Test); t.Start(); } void Update () { print(que.Dequeue()); } private void Test () { while (true ) { System.Random r = new System.Random(); que.Enqueue(r.Next(-10 , 10 )); } } private void OnDestroy () { t.Abort(); t = null ; } }
知识点二 协同程序是什么? 协同程序简称协程,它是“假”的多线程,它==不是==多线程。
它的主要作用:将代码分时执行,不卡主线程。
简单理解,是把可能会让主线程卡顿的耗时的逻辑分时分步执行。
协程函数每一次返回,只是将协程挂起,等待下一次执行。
主要使用场景
异步加载文件
异步下载文件
场景异步加载
批量创建时防止卡顿
知识点三 协同程序和线程的区别 新开一个线程是独立的一个管道,和主线程并行执行
新开一个协程是在原线程之上开启,进行逻辑分时分步执行
知识点四 协程的使用
继承 MonoBehavior 的类都可以开启协程函数。
第一步:声明协程函数 协程函数 2 个关键点
返回值为 IEnumerator 类型及其子类
函数中通过yield return 返回值;
进行返回
1 2 3 4 5 6 7 8 9 10 private IEnumerator MyCoroutine (int i, string str ){ print(i); yield return new WaitForSeconds (5f ) ; print(str); yield return new WaitForSeconds (2f ) ; print(233 ); }
第二步:开启协程函数 协程函数是不能够直接这样去执行的!!!!!!!
这样执行没有任何效果
常用开启方式
1 2 3 4 5 6 7 public Coroutine StartCoroutine (IEnumerator routine ) ;IEnumerator ie = MyCoroutine(2 , "22" ); StartCoroutine(ie); StartCoroutine(MyCoroutine(10 , "Panzi" ));
第三步:关闭协程 StartCoroutine
返回一个Coroutine
,就是协程对象。
使用StopCoroutine
指定协程对象进行关闭。
使用StopAllCoroutine
关闭所有协程。
不建议使用指定函数名进行关闭协程。
1 2 3 4 5 6 7 8 9 10 11 IEnumerator ie = MyCoroutine(2 , "22" ); Coroutine c1 = StartCoroutine(ie); Coroutine c2 = StartCoroutine(MyCoroutine(10 , "Panzi" )); StopAllCoroutines(); StopCoroutine(c1); StopCoroutine(c2);
知识点五 yield return 不同内容的含义 1.下一帧执行 注意:==yield return 数字会产生装箱拆箱==。
1 2 yield return 数字;yield return null ;
2.等待指定秒后执行 在 Update 和 LateUpdate 之间执行
1 yield return new WaitForSeconds (秒 ) ;
3.等待下一个固定物理帧更新时执行 在 FixedUpdate 和碰撞检测相关函数之后执行
1 yield return new WaitForFixedUpdate () ;
4.等待摄像机和 GUI 渲染完成后执行 在 LateUpdate 之后的渲染相关处理完毕后之后,主要会用来截图时会使用
1 yield return new WaitForEndOfFrame () ;
5.一些特殊类型的对象 比如异步加载相关函数返回的对象 之后讲解 异步加载资源 异步加载场景 网络加载(WWW)时再讲解
一般在 Update 和 LateUpdate 之间执行
6.跳出协程
知识点六 协程受对象和组件失活销毁的影响 协程开启后,组件和物体销毁,协程不执行
物体失活协程不执行,组件失活协程仍然执行
总结
Unity 支持多线程,只是新开线程无法访问主线程中 Unity 相关内容
一般主要用于进行复杂逻辑运算或者网络消息接收等等
注意:Unity 中的多线程一定记住关闭
协同程序不是多线程,它是将线程中逻辑进行分时执行,避免卡顿
继承 MonoBehavior 的类都可以使用协程
开启协程方法、关闭协程方法
yield return 返回的内容对于我们的意义
协程只有当组件单独失活时不受影响,其它情况协程会停止
练习题 第一题 利用协程制作一个计秒器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class YieldTimeCounter : MonoBehaviour { float time = 0 ; void Start () { Coroutine timeCounter = StartCoroutine(TimeCounter()); } IEnumerator TimeCounter () { while (true ) { print($"Now is {time} seconds" ); ++time; yield return new WaitForSeconds (1f ) ; } } }
第二题 请在场景中创建 100000 个随机位置的立方体,让其不会明显卡顿
本渣渣只能想到隔几秒创建 100 个,或者几帧创建 100 个,直到创建完 100000 个。
创建完成后的掉帧不可避免
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class CubeCreator : MonoBehaviour { void Update () { if (Input.GetMouseButtonDown(0 )) { Coroutine cubeCreate = StartCoroutine(CreateCube()); } } IEnumerator CreateCube () { for (int i = 0 ; i < 100000 ; i += 100 ) { for (int j = 0 ; j < 100 ; ++j) { GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); cube.transform.position = new Vector3(Random.Range(-100 , 100 ), Random.Range(-100 , 100 )); } yield return new WaitForSeconds (0.5f ) ; } } }
协同程序原理 知识点 知识点一 协程的本质 协程可以分成两部分:
协程函数本体
协程调度器
协程本体就是一个能够中间暂停返回的函数
协程调度器是 Unity 内部实现的,会在对应的时机帮助我们继续执行协程函数。
Unity 只实现了协程调度部分,协程的本体本质上就是一个 C#的迭代器方法。
知识点二 协程本体是迭代器方法的体现 1.协程函数本体 如果我们不通过开启协程方法执行协程,Unity 的协程调度器是不会帮助我们管理协程函数的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 IEnumerator Test () { print("call 1" ); yield return 1 ; print("call 2" ); yield return 2 ; print("call 3" ); yield return 1 ; print("call 4" ); yield return new TestClass (10 ) ; }
但是,由于其是一个迭代器函数。
我们可以通过迭代器的 MoveNext 函数执行分段的函数,MoveNext
的返回值就是是否还有下一个分段函数需要执行;
通过迭代器的Current
属性,可以获取到每一次yield return
返回的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 IEnumerator ie = Test(); while (ie.MoveNext()){ print(ie.Current); } 1 call 2 2 call 3 1 call 4 TestClass
2.协程调度器 继承 MonoBehavior 后开启协程,相当于是把一个协程函数(迭代器)放入 Unity 的协程调度器中帮助我们管理进行执行,具体的 yield return 后面的规则 也是 Unity 定义的一些规则。
总结 你可以简化理解迭代器函数,C#看到迭代器函数和 yield return 语法糖,就会把原本是一个的 函数 变成”几部分”。
我们可以通过迭代器,从上到下遍历这 “几部分”进行执行,就达到了将一个函数中的逻辑分时执行的目的。
而协程调度器就是利用迭代器函数返回的内容来进行之后的处理。
比如 Unity 中的协程调度器,根据 yield return 返回的内容决定了下一次在何时继续执行迭代器函数中的”下一部分”。
理论上来说 我们可以利用迭代器函数的特点自己实现协程调度器来取代 Unity 自带的调度器。
总结 协程的本质就是利用 C#的迭代器函数”分步执行”的特点;
加上,协程调度逻辑,实现的一套分时执行函数的规则。
练习题 请不使用 Unity 自带的协程协调器开启协程
通过迭代器函数实现每隔一秒执行函数中的一部分逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 using System.Collections;using System.Collections.Generic;using System.Threading;using UnityEngine;using System.Reflection;using System;using UnityEngine.PlayerLoop;public class YieldReturnTime { public IEnumerator ie; public float time; } public class CoroutineMgr : MonoBehaviour { private static CoroutineMgr _instance = new CoroutineMgr(); public static CoroutineMgr Instance => _instance; List<YieldReturnTime> timeList = new List<YieldReturnTime>(); private void Awake () { _instance = this ; } public void MyStartCoroutine (IEnumerator ie ) { if (ie.MoveNext()) { if (ie.Current is int ) { YieldReturnTime yrt = new YieldReturnTime(); yrt.time = Time.time + (int )ie.Current; yrt.ie = ie; timeList.Add(yrt); } } } private void Update () { for (int i = timeList.Count - 1 ; i >= 0 ; --i) { if (timeList[i].time <= Time.time) { if (timeList[i].ie.MoveNext()) { if (timeList[i].ie.Current is int ) { timeList[i].time = Time.time + (int )timeList[i].ie.Current; } } else { timeList.RemoveAt(i); } } } } }
Resources 资源动态加载 特殊文件夹 知识点 知识点一 工程路径获 注意:
该方式获取到的路径,一般情况下只在编辑模式下使用。我们不会在实际发布游戏后,还使用该路径;可以认为,游戏发布过后,该路径就不存在了 。
1 print(Application.dataPath);
知识点二 Resources 资源文件夹 路径获取:一般不获取,而是直接使用 Resources 相关 API 进行加载
如果硬要获取,可以用工程路径拼接
1 print(Application.dataPath + "/Resources" );
注意:需要我们自己将创建
作用:资源文件夹
需要通过 Resources 相关 API 动态加载的资源需要放在其中
该文件夹下所有文件都会被打包出去
打包时 Unity 会对其压缩加密
该文件夹打包后只读 只能通过 Resources 相关 API 加载
知识点三 StreamingAssets 流动资源文件夹 路径获取:
1 print(Application.streamingAssetsPath);
注意:需要我们自己创建
作用:流文件夹
打包出去不会被压缩加密,可以任由我们摆布
移动平台只读,PC 平台可读可写
可以放入一些需要自定义动态加载的初始资源
知识点四 persistentDataPath 持久数据文件夹 路径获取:
1 print(Application.persistentDataPath);
注意:不需要我们自己将创建
作用:固定数据文件夹
所有平台都可读可写
一般用于放置动态下载或者动态创建的文件,游戏中创建或者获取的文件都放在其中
知识点五 Plugins 插件文件夹 路径获取:一般不获取
注意:需要我们自己将创建
作用:插件文件夹,不同平台的插件相关文件放在其中,比如 IOS 和 Android 平台。
知识点六 Editor 编辑器文件夹 路径获取:一般不获取,如果硬要获取 可以用工程路径拼接。
1 print(Application.dataPath + "/Editor" );
注意:需要我们自己将创建 作用:编辑器文件夹
开发 Unity 编辑器时,编辑器相关脚本放在该文件夹中
该文件夹中内容不会被打包出去
知识点七 默认资源文件夹 Standard Assets 路径获取:一般不获取
注意:需要我们自己将创建
作用:默认资源文件夹
一般 Unity 自带资源都放在这个文件夹下,代码和资源优先被编译
Resources 资源同步加载 知识点 知识点一 Resources 资源动态加载的作用
通过代码动态加载 Resources 文件夹下指定路径资源
避免繁琐的拖曳操作
知识点二 常用资源类型
预设体对象——GameObject
音效文件——AudioClip
文本文件——TextAsset
图片文件——Texture
其它类型——需要什么用什么类型
注意:预设体对象加载需要实例化,其它资源加载一般直接用。
知识点三 资源同步加载 普通方法 在一个工程当中 Resources 文件夹,可以在多个目录下有多个,通过 API 加载时,它会自己去这些同名的 Resources 文件夹中去找资源,打包时 Resources 文件夹里的内容都会打包在一起。
1.预设体对象 想要创建在场景上,记住实例化
第一步:要去加载预设体的资源文件(本质上,就是加载配置数据在内存中)。
1 Object obj = Resources.Load("Cube" );
第二步:如果想要在场景上创建预设体,一定是加载配置文件过后,然后实例化。
2.音效资源 第一步:就是加载数据;
1 Object obj3 = Resources.Load("Music/BKMusic" );
第二步:使用数据,我们不需要实例化;音效切片,我们只需要把数据,赋值到正确的脚本上即可。
1 2 audioS.clip = obj3 as AudioClip; audioS.Play();
3.文本资源 文本资源支持的格式
1 2 3 4 5 6 .txt .xml .bytes .json .html .csv.....
1 TextAsset ta = Resources.Load("Txt/Test" ) as TextAsset;
文本内容
字节数据组
4.图片 1 tex = Resources.Load("Tex/TestJPG" ) as Texture;
5.其它类型,需要什么类型,就用什么类型就行 1 Resources.Load("fileName" ) as xxx;
6.问题:资源同名怎么办 Resources.Load
加载同名资源时,无法准确加载出你想要的内容。
可以使用另外的 API
6-1 加载指定类型的资源 1 2 3 tex = Resources.Load("Tex/TestJPG" , typeof (Texture)) as Texture; ta = Resources.Load("Tex/TestJPG" , typeof (TextAsset)) as TextAsset; print(ta.text);
6 - 2 加载指定名字的所有资源 1 2 3 4 5 6 7 8 9 10 11 12 Object[] objs = Resources.LoadAll("Tex/TestJPG" ); foreach (Object item in objs){ if (item is Texture) { } else if (item is TextAsset) { } }
知识点四 资源同步加载 泛型方法 使用泛型指定类型,省略了 as 步骤,一般应该都是使用泛型方法。
1 2 3 4 TextAsset ta2 = Resources.Load<TextAsset>("Tex/TestJPG" ); print(ta2.text); tex = Resources.Load<Texture>("Tex/TestJPG" );
总结 Resources 动态加载资源的方法,让拓展性更强,相对拖曳来说,它更加一劳永逸 ,更加方便。
重要知识点:
记住 API:Resources.Load
记住一些特定的格式
TextAssets:文本文件
AudioClip:音频切片
Texture:材质(图片)
Gameobject:预设体
预设体加载出来一定要实例化
练习题 请把之前四元数练习题中,发射散弹等相关逻辑改为动态加载资源并创建。
1 2 3 4 void Start (){ bulletPrefab = Resources.Load<GameObject>("Prefab/Bullet" ); }
关于重复加载同一个资源 多次加载同一个资源,Unity 会将这个资源保存到缓存池当中,不会多耗费内存。
但是,加载同一个资源,Unity 会去缓存池当中查找这个资源,会多耗费性能。
Resources 资源异步加载 知识点 知识点一 Resources 异步加载是什么? 上节课学习的同步加载中,如果我们加载过大的资源可能会造成程序卡顿;
卡顿的原因就是,从硬盘上把数据读取到内存中,是需要进行计算的。
越大的资源耗时越长,就会造成掉帧卡顿;
Resources 异步加载,就是内部新开一个线程进行资源加载,不会造成主线程卡顿。
注意:异步加载不是立刻就能获取到资源的,同步加载是可以立刻加载立刻使用的。
知识点二 Resources 异步加载方法
UnityEngine.Resources - Unity 脚本 API
注意:异步加载,不能马上得到加载的资源,至少要等一帧。
YieldInstruction 是 AsyncOperation 的父类,AsyncOperation 是 ResourceRequest 的父类。
继承链:ResourceRequest -> AsyncOperation -> YieldInstruction
1.通过异步加载中的完成事件监听,使用加载的资源 这句代码,可以理解为,Unity 在内部就会去开一个线程进行资源加载。
1 2 3 4 ResourceRequest rq = Resources.LoadAsync<Texture>("Tex/TestJPG" );
ResourceRequest
当中有一个事件completed
,在资源加载完成之后,会立刻执行此事件。
我们可以向这个事件当中添加函数,用来执行其他代码。
注意:事件的委托模板需要带一个AsyncOperation
类型的参数,是加载完成后的资源。
1 2 3 4 5 6 7 8 9 10 rq.completed += LoadOver; print(Time.frameCount); private void LoadOver ( AsyncOperation rq ){ print("加载结束" ); tex = (rq as ResourceRequest).asset as Texture; print(Time.frameCount); }
2.通过协程 使用加载的资源 通过协程,我们可以在加载资源的时候做一些其他事情,比如资源进度条的显示等等。
如果我们直接返回ResourceRequest
,由于其是一个YieldInstruction
的子类,所以,Unity 会进行特殊处理,只有当资源加载完成之后,才会继续执行协程后方的代码。
1 2 3 4 5 6 7 8 9 StartCoroutine(Load()); IEnumerator LoadOver () { ResourceRequest rq = Resources.LoadAsync<TextAsset>("TextAsset/words" ); yield return rq; ta = rq.asset as TextAsset; print(ta.text); }
如果不返回获取ResourceRequest
;
那么我们可以通过ResourceRequest.isDonw
判断资源是否加载完成;
通过ResourceRequest.progress
获取资源加载进度,这个进度不是特别准确。
UnityEngine.AsyncOperation - Unity 脚本 API
1 2 3 4 5 6 7 8 9 10 11 12 IEnumerator LoadOver () { ResourceRequest rq = Resources.LoadAsync<TextAsset>("TextAsset/words" ); while (!rq.isDone) { print(rq.progress); yield return null ; } ta = rq.asset as TextAsset; print(ta.text); }
总结 1.完成事件监听异步加载 好处:写法简单
坏处:只能在资源加载结束后,进行处理
类似“线性加载”
2.协程异步加载 好处:可以在协程中处理复杂逻辑,比如同时加载多个资源,比如进度条更新
坏处:写法稍麻烦
类似“并行加载”
注意: 理解为什么异步加载不能马上加载结束,为什么至少要等 1 帧
因为异步加载是类似于开了一个新线程,线程同步需要时间。
理解协程异步加载的原理
异步加载LoadAsync
返回值是YieldInstruction
的一个子类;
协程异步加载是通过 yield return 返回值,使得 Unity 知道这个协程是在加载资源,就会等待资源的加载。
资源加载过后,才会执行之后的代码。
练习题 请写一个简单的资源管理器,提供统一的方法给外部用于资源异步加载。外部可以传入委托用于当资源加载结束时使用资源。
使用两种方式实现,并添加了已加载的资源部重复加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 using System;using System.Collections;using System.Collections.Generic;using System.IO;using System.Runtime.InteropServices.WindowsRuntime;using System.Xml.Schema;using UnityEngine;using UnityEngine.Events;public class ResourceLoadMgr { private static ResourceLoadMgr _instance = new ResourceLoadMgr(); public static ResourceLoadMgr Instance => _instance; public Dictionary<string , object > loadDic = new Dictionary<string , object >(); public void AsyncLoadUseCompleted <T >(string path, UnityAction<T> action ) where T : class { if (loadDic.ContainsKey(path)) { action(loadDic[path] as T); return ; } ResourceRequest rq = Resources.LoadAsync(path); rq.completed += (value ) => { T t = (value as ResourceRequest).asset as T; action(t); if (!loadDic.ContainsKey(path)) { loadDic.Add(path, t); } }; } public void AsyncLoadUseIEnumerator <T >(string path, UnityAction<T> action ) where T : class { if (loadDic.ContainsKey(path)) { action(loadDic[path] as T); Debug.Log("Already load" ); return ; } Load(path, action); } IEnumerator Load <T >(string path, UnityAction<T> action ) where T : class { ResourceRequest rq = Resources.LoadAsync(path); yield return rq; action((rq.asset) as T); if (!loadDic.ContainsKey(path)) { loadDic.Add(path, (rq.asset) as T); } } }
Resources 资源卸载 知识点 知识点一 Resources 重复加载资源会浪费内存吗? 其实 Resources 加载一次资源过后,该资源就一直存放在内存中作为缓存。
第二次加载时发现缓存中存在该资源,会直接取出来进行使用。
所以,多次重复加载不会浪费内存。
但是会浪费性能(每次加载都会去查找取出,始终伴随一些性能消耗)。
知识点二 如何手动释放掉缓存中的资源 1.卸载指定资源 Resources.UnloadAsset
方法
1 2 3 4 5 6 7 8 9 10 Resources.UnloadAsset(asset); TextAsset tex = Resources.Load<TextAsset>("TextAsset/words" ); print(tex.text); Resources.UnloadAsset(tex); tex = null ; if (tex == null ){ print("unload" ); }
注意:该方法,不能释放 GameObject 对象,因为 GameObject 是用于实例化对象的。
它只能用于一些 不需要实例化的内容 比如 图片 和 音效 文本等等
一般情况下 我们很少单独使用它。
1 2 GameObject obj = Resources.Load<GameObject>("Cube" ); Resources.UnloadAsset(obj);
即使是没有实例化的,GameObject 对象也不能进行卸载。
2.卸载未使用的资源 注意:一般在过场景时和 GC 一起使用
使用Resources.UnloadUnusedAssets();
1 2 Resources.UnloadUnusedAssets(); GC.Collect();
Unity 性能监听面板
Profiler Module Editor(性能分析器模块编辑器) - Unity 手册
可以使用ctrl + 7
或者windows->panels->7 profiler
打开 Unity 性能监听面板,查看游戏运行时的各种情况。
在 memory 即内存当中,可以查看到加载的资源数和锁占内存。
场景异步加载 知识点 知识点一 回顾场景同步切换 1 SceneManager.LoadScene("Lesson19" );
场景同步切换的缺点 在切换场景时,Unity 会删除当前场景上所有对象,并且去加载下一个场景的相关信息。
如果当前场景,对象过多或者下一个场景对象过多。
这个过程会非常的耗时,会让玩家感受到卡顿,所以异步切换就是来解决该问题的。
知识点二 场景异步切换 场景异步加载和资源异步加载几乎一致,有两种方式。
1.通过事件回调函数 异步加载 1 AsyncOperation ao = SceneManager.LoadSceneAsync("Lesson20Test" );
当场景异步加载结束后,就会自动调用该事件函数。
我们如果希望在加载结束后,做一些事情,那么就可以在该函数中写处理逻辑。
1 2 3 4 5 6 ao.completed += (a) => { print("加载结束" ); }; ao.completed += LoadOver;
2.通过协程异步加载 需要注意的是,加载场景会把当前场景上,没有特别处理的对象都删除了。
所以,协程中的部分逻辑,可能是执行不了的。
解决思路:让处理场景加载的脚本依附的对象,过场景时不被移除。
1 2 3 DontDestroyOnLoad(this .gameObject); StartCoroutine(LoadScene("Lesson20Test" ));
协程的好处,是异步加载场景时,我可以在加载的同时,做一些别的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 IEnumerator LoadScene (string name ) { AsyncOperation ao = SceneManager.LoadSceneAsync(name); print("异步加载过程中 打印的信息" ); print("异步加载结束后 打印的信息" ); yield return ao; }
总结 场景异步加载和资源异步加载 一样,有两种方式
通过事件回调函数
协程异步加载
他们的优缺点表现和资源异步加载 也是一样的
事件回调函数
优点:写法简单,逻辑清晰
缺点:只能加载完场景做一些事情 不能再加载过程中处理逻辑
协程异步加载
优点:可以在加载过程中处理逻辑,比如进度条更新等
缺点:写法较为麻烦,要通过协程
练习题 请写一个简单的场景管理器,提供统一的方法给外部用于场景异步切换。外部可以传入委托用于当异步切换结束时执行某些逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.Events;using UnityEngine.SceneManagement;public class SceneLoadMgr : MonoBehaviour { private static SceneLoadMgr _instance = new SceneLoadMgr(); public static SceneLoadMgr Instance => _instance; private SceneLoadMgr () { } public void LoadSceneUseCompleted (string scene, UnityAction action = null ) { AsyncOperation ao = SceneManager.LoadSceneAsync(scene); ao.completed += (a) => { action(); }; } public void LoadSceneUseIEnumerator (string scene, UnityAction action = null ) { Load(scene, action); } IEnumerator Load (string scene, UnityAction action = null ) { AsyncOperation ao = SceneManager.LoadSceneAsync(scene); yield return ao; action(); } }
LineRenderer 知识点 知识点一 LineRenderer 是什么 LineRenderer 是 Unity 提供的一个用于画线的组件,使用它我们可以在场景中绘制线段。
一般可以用于:
绘制攻击范围
武器红外线
辅助功能
其它画线功能
知识点二 LineRender 参数相关 重要参数
Loop:是否终点起始自动相连
Positions:线段的点
线段宽度曲线调整
Color:颜色变化
Corner Vertices(角顶点,圆角):此属性指示在一条线中绘制角时使用了多少额外的顶点。 增加此值,使线角看起来更圆。
End Cap Vertices(终端顶点,圆角):终点圆角
Generate Lighting Data:生成光源数据,使得生成的线收光的影响
Use World Space:是否使用世界坐标系,如果勾选,则点的位置就是世界坐标,否则是相对于父物体的坐标。
Materials:线使用的材质球
知识点三 LineRender 代码相关
UnityEngine.LineRenderer - Unity 脚本 API
动态添加一个线段
注意:只要添加了一个 LineRenderer 组件,他们就有两个点,所以在使用的时候,要先将positionCount
置 0
1 2 3 GameObject line = new GameObject(); line.name = "Line" ; LineRenderer lineRenderer = line.AddComponent<LineRenderer>();
首尾相连
1 lineRenderer.loop = true ;
开始结束宽
1 2 lineRenderer.startWidth = 0.02f ; lineRenderer.endWidth = 0.02f ;
开始结束颜色
1 2 lineRenderer.startColor = Color.white; lineRenderer.endColor = Color.red;
设置材质
1 2 m = Resources.Load<Material>("M" ); lineRenderer.material = m;
设置点 注意:设置点要先设置点的个数!!
1 lineRenderer.positionCount = 4 ;
接着设置对应每个点的位置。
如果设置了个数,但是未设置具体点位置,那么这些点的位置将是(0,0,0)。
1 2 3 4 lineRenderer.SetPositions(new Vector3[] { new Vector3(0 ,0 ,0 ), new Vector3(0 ,0 ,5 ), new Vector3(5 ,0 ,5 )}); lineRenderer.SetPosition(3 , new Vector3(5 , 0 , 0 ));
是否使用世界坐标系,决定了,是否随对象移动而移动。
1 lineRenderer.useWorldSpace = false ;
让线段受光影响,会接受光数据,进行着色器计算。
1 lineRenderer.generateLightingData = true ;
练习题 第一题 请写一个方法,传入一个中心点,传入一个半径,用 LineRender 画一个圆出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void DrawRing (Vector3 center, int radius, int pointNum ){ GameObject obj = new GameObject("ring" ); obj.transform.position = center; LineRenderer line = obj.AddComponent<LineRenderer>(); line.useWorldSpace = false ; line.loop = true ; float angle = 360 / pointNum; line.positionCount = pointNum; for (int i = 0 ; i < pointNum; ++i) { line.SetPosition(i, center + Quaternion.AngleAxis(angle * i, Vector3.up) * Vector3.forward * radius); } }
第二题 请实现,在 Game 窗口长按鼠标用 LineRender 画出鼠标移动的轨迹
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void Update (){ if (Input.GetMouseButtonDown(0 )) { GameObject obj = new GameObject("line" ); obj.transform.SetParent(lineParent.transform); line = obj.AddComponent<LineRenderer>(); line.startWidth = 0.5f ; line.endWidth = 0.5f ; line.loop = false ; line.positionCount = 0 ; } if (Input.GetMouseButton(0 )) { pointPos = Input.mousePosition; pointPos.z = 10 ; line.positionCount++; line.SetPosition(line.positionCount - 1 , Camera.main.ScreenToWorldPoint(pointPos)); } if (Input.GetMouseButtonDown(1 )) { Transform trans = lineParent.transform; Destroy(trans.GetChild(trans.childCount - 1 ).gameObject); } }
Unity 核心系统-物理系统-检测相关 范围检测 知识点
UnityEngine.Physics - Unity 脚本 API
知识回顾 物理系统之碰撞检测 碰撞产生的必要条件
至少一个物体有刚体
两个物体都必须有碰撞器
碰撞和触发
碰撞会产生实际的物理效果
触发看起来不会产生碰撞,但是可以通过函数监听触发
碰撞检测主要用于实体物体之间产生物理效果时使用
知识点一 什么是范围检测 游戏中瞬时的攻击范围判断一般会使用范围检测
举例:
玩家在前方 5m 处释放一个地刺魔法,在此处范围内的对象将受到地刺伤害;
玩家攻击,在前方 1 米圆形范围内对象都受到伤害。
等等
类似这种并没有实体物体,只想要检测在指定某一范围,是否让敌方受到伤害时,便可以使用范围判断。
简而言之:在指定位置,进行范围判断,我们可以得到处于指定范围内的对象。
目的是对对象进行处理,比如受伤、减血等等。
知识点二 如何进行范围检测
Physics-OverlapBox - Unity 脚本 API
必备条件:想要被范围检测到的对象,必须具备碰撞器(Collider)
注意点:
范围检测相关 API,只有当执行该句代码时,进行一次范围检测,它是瞬时的。
范围检测相关 API,并不会真正产生一个碰撞器,只是碰撞判断计算而已。
范围检测 API
1.盒状范围检测
参数
意义
center
盒体的中心。
halfExtents
盒体各个维度大小的一半。
orientation
盒体的旋转。
layerMask
层遮罩 ,用于在投射射线时有选择地忽略碰撞体。
queryTriggerInteraction
指定该查询是否应该命中触发器。
参数一:立方体中心点
参数二:立方体三边大小的一半
参数三:立方体角度
参数四:检测指定层级(不填检测所有层)
参数五:
是否忽略触发器 UseGlobal-使用全局设置
Collide - 检测触发器
Ignore - 忽略触发器
不填使用 UseGlobal
返回值:在该范围内的触发器(得到了对象触发器就可以得到对象的所有信息)
1 2 3 4 5 6 7 8 9 print(LayerMask.NameToLayer("UI" )); Collider[] colliders = Physics.OverlapBox( Vector3.zero, Vector3.one, Quaternion.AngleAxis(45 , Vector3.up), 1 << LayerMask.NameToLayer("UI" ) | 1 << LayerMask.NameToLayer("Default" ), QueryTriggerInteraction.UseGlobal); for (int i = 0 ; i < colliders.Length; i++){ print(colliders[i].gameObject.name); }
重要知识点: 关于层级 通过名字得到层级编号 LayerMask.NameToLayer
,我们需要通过编号左移构建二进制数。
这样每一个编号的层级,都是对应位为 1 的 2 进制数;
我们通过,位运算,可以选择想要检测层级。
好处:一个 int 就可以表示所有想要检测的层级信息
层级编号是 0~31 刚好 32 位
是一个 int 数
每一个编号,代表的都是二进制的一位。
1 2 3 4 5 6 0 —— 1 << 0 ——0000 0000 0000 0000 0000 0000 0000 0001 = 1 1 —— 1 << 1 ——0000 0000 0000 0000 0000 0000 0000 0010 = 2 2 —— 1 << 2 ——0000 0000 0000 0000 0000 0000 0000 0100 = 4 3 —— 1 << 3 ——0000 0000 0000 0000 0000 0000 0000 1000 = 8 4 —— 1 << 4 ——0000 0000 0000 0000 0000 0000 0001 0000 = 16 5 —— 1 << 5 ——0000 0000 0000 0000 0000 0000 0010 0000 = 32
另一个 API 返回值:碰撞到的碰撞器数量 参数:传入一个数组进行存储Physics.OverlapBoxNonAlloc()
1 2 3 4 if (Physics.OverlapBoxNonAlloc(Vector3.zero, Vector3.one, colliders) != 0 ){ }
2.球形范围检测
Physics-OverlapSphere - Unity 脚本 API
参数一:中心点
参数二:球半径
参数三:检测指定层级(不填检测所有层)
参数四:是否忽略触发器 UseGlobal-使用全局设置 Collide - 检测触发器 Ignore - 忽略触发器 不填使用 UseGlobal
返回值:在该范围内的触发器(得到了对象触发器就可以得到对象的所有信息)
1 2 colliders = Physics.OverlapSphere(Vector3.zero, 5 , 1 << LayerMask.NameToLayer("Default" ));
另一个 API
3.胶囊范围检测
Physics-OverlapCapsule - Unity 脚本 API
参数一:半圆一中心点
参数二:半圆二中心点
参数三:半圆半径
参数四:检测指定层级(不填检测所有层)
参数五:是否忽略触发器 UseGlobal-使用全局设置 Collide - 检测触发器 Ignore - 忽略触发器 不填使用 UseGlobal
返回值:在该范围内的触发器(得到了对象触发器就可以得到对象的所有信息)
1 colliders = Physics.OverlapCapsule(Vector3.zero, Vector3.up, 1 , 1 << LayerMask.NameToLayer("UI" ), QueryTriggerInteraction.UseGlobal);
另一个 API
返回值:碰撞到的碰撞器数量
参数:传入一个数组进行存储
Physics.OverlapCapsuleNonAlloc
1 2 3 4 if ( Physics.OverlapCapsuleNonAlloc(Vector3.zero, Vector3.up, 1 , colliders ) != 0 ){ }
Layer(图层相关) 所有关于图层的使用,都是使用一个 Int32 的整数表示的,Layer 最多有 32 个。
总结 范围检测主要用于==瞬时==的碰撞范围检测。
主要掌握
Physics 类中的静态方法
球形、盒状、胶囊三种 API 的使用即可
练习题 世界坐标原点有一个立方体,键盘 WASD 键可以控制其前后移动和旋转。
请结合所学知识实现
按 J 键在立方体面朝向前方 1 米处进行立方体范围检测
按 K 键在立方体前面 5 米范围内进行胶囊范围检测
按 L 键以立方体脚下为原点,半径 10 米内进行球形范围检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 void Update () { this .transform.Translate(Input.GetAxis("Vertical" ) * Vector3.forward * moveSpeed * Time.deltaTime); this .transform.Rotate(Input.GetAxis("Horizontal" ) * Vector3.up * rotateSpeed * Time.deltaTime); Debug.DrawLine(this .transform.position, this .transform.position + this .transform.forward * 2 ); if (Input.GetKeyDown(KeyCode.J)) { Collider[] colliders = Physics.OverlapBox(this .transform.position + this .transform.forward * 0.5f , Vector3.one * 0.5f , this .transform.rotation, 1 << LayerMask.NameToLayer("Default" ) ); for (int i = 0 ; i < colliders.Length; i++) { print(colliders[i].gameObject.name); print("距离" + colliders[i].gameObject.name + Vector3.Distance(this .transform.position, colliders[i].transform.position).ToString() + "米" ); } } if (Input.GetKeyDown(KeyCode.K)) { Collider[] colliders = Physics.OverlapCapsule( this .transform.position, this .transform.position + this .transform.forward * 5 , 0.5f , 1 << LayerMask.NameToLayer("Default" ) ); for (int i = 0 ; i < colliders.Length; i++) { print(colliders[i].gameObject.name); print("距离" + colliders[i].gameObject.name + Vector3.Distance(this .transform.position, colliders[i].transform.position).ToString() + "米" ); } } if (Input.GetKeyDown(KeyCode.L)) { Collider[] colliders = Physics.OverlapSphere( this .transform.position, 10f , 1 << LayerMask.NameToLayer("Default" ) ); for (int i = 0 ; i < colliders.Length; i++) { print(colliders[i].gameObject.name); print("距离" + colliders[i].gameObject.name + Vector3.Distance(this .transform.position, colliders[i].transform.position).ToString() + "米" ); } } }
射线检测 知识点 知识点一 什么是射线检测 Unity 物理系统中,目前我们学习的物体相交判断:
碰撞检测——必备条件 1 刚体 2 碰撞器
范围检测——必备条件 碰撞器
如果想要做这样的碰撞检测呢?
鼠标选择场景上一物体
一些 FPS 射击游戏当中的射击(无弹道 - 不产生实际的子弹对象进行移动,而是射线检测)
等等需要判断一条线和物体的碰撞情况,射线检测就是来解决这些问题的。它可以在指定点发射一个指定方向的射线,判断该射线与哪些碰撞器相交,得到对应对象。
知识点二 射线对象 1.3D 世界中的射线 假设有一条,起点为坐标(1, 0, 0),方向为世界坐标 Z 轴正方向的射线
1 Ray r = new Ray(Vector3.right, Vector3.forward);
注意理解参数含义:
参数一:起点
参数二:方向(一定记住 ,不是两点决定射线方向,第二个参数直接 就代表方向向量)
目前只是声明了一个射线对象,对于我们来说,没有任何的用处。
Ray 中的成员
1 2 print(r.origin); print(r.direction);
2.摄像机发射出的射线 得到一条从屏幕位置作为起点,摄像机视口方向为方向的射线。
1 Ray r2 = Camera.main.ScreenPointToRay(Input.mousePosition);
注意: 单独的射线对于我们来说没有实际的意义,我们需要用它结合物理系统进行射线碰撞判断。
知识点三 碰撞检测函数 Physics 类中提供了很多进行射线检测的静态函数,他们有很多种重载类型,我们只需要掌握核心的几个函数,其它函数自然就明白什么意思了。
注意:射线检测也是瞬时的,执行代码时进行一次射线检测。
1.最原始的射线检测 准备一条射线
1 Ray r3 = new Ray(Vector3.zero, Vector3.forward);
进行射线检测,如果碰撞到对象,返回 true。
参数一:射线对象
参数二:检测的最大距离,超出这个距离不检测
参数三:检测指定层级(不填检测所有层)
参数四:是否忽略触发器 UseGlobal-使用全局设置 Collide - 检测触发器 Ignore - 忽略触发器 不填使用 UseGlobal
返回值:bool 当碰撞到对象时 返回 true 没有 返回 false
注意:这个 API 只能检测是否有碰撞到对象,不返回碰到的对象
1 2 3 4 5 6 7 if (Physics.Raycast(r3, 1000 , 1 << LayerMask.NameToLayer("Monster" ), QueryTriggerInteraction.UseGlobal)) { print("碰撞到了对象" ); }
还有一种重载,不用传入射线,直接传入起点和方向,也可以用于判断是否碰到了对象。
就是把,第一个参数射线变成了射线的两个点,一个起点一个方向。
1 2 3 4 5 6 7 8 9 if (Physics.Raycast(Vector3.zero, Vector3.forward, 1000 , 1 << LayerMask.NameToLayer("Monster" ), QueryTriggerInteraction.UseGlobal)) { print("碰撞到了对象2" ); }
2.获取相交的单个物体信息 物体信息类RaycastHit
参数一:射线
参数二:RaycastHit 是结构体,是值类型;Unity 会通过 out 关键字,在函数内部处理后 得到碰撞数据后返回到该参数中(就是传引用,并且 out 修饰,必须在函数内修改)。
参数三:距离
参数四:检测指定层级(不填检测所有层)
参数五:是否忽略触发器 UseGlobal-使用全局设置 Collide - 检测触发器 Ignore - 忽略触发器 不填使用 UseGlobal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if ( Physics.Raycast(r3, out hitInfo, 1000 , 1 <<LayerMask.NameToLayer("Monster" ), QueryTriggerInteraction.UseGlobal) ){ print("碰撞到了物体 得到了信息" ); print("碰撞到物体的名字" + hitInfo.collider.gameObject.name); print(hitInfo.point); print(hitInfo.normal); print(hitInfo.transform.position); print(hitInfo.distance); }
RaycastHit
该类对于我们的意义:
它不仅可以得到我们碰撞到的对象信息,还可以得到一些碰撞的点、距离、法线等等的信息。
碰撞器信息:hitInfo.collider.gameObject.name
碰撞到的点:hitInfo.point
得到碰撞到对象的位置:hitInfo.transform.position
得到碰撞到对象,离自己的距离:hitInfo.distance
注意:它是一个==结构体==
还有一种重载,不用传入射线直接传入起点和方向,也可以用于判断。
1 2 3 4 5 6 7 8 9 10 11 if ( Physics.Raycast( Vector3.zero, Vector3.forward, out hitInfo, 1000 , 1 << LayerMask.NameToLayer("Monster" ), QueryTriggerInteraction.UseGlobal)) { }
3.获取相交的多个物体 可以得到碰撞到的多个对象,如果没有就是容量为 0 的数组。
参数一:射线
参数二:距离
参数三:检测指定层级(不填检测所有层)
参数四:是否忽略触发器 UseGlobal-使用全局设置 Collide - 检测触发器 Ignore - 忽略触发器 不填使用 UseGlobal
返回 RaycastHit 数组
1 2 3 4 5 6 7 8 9 10 11 RaycastHit[] hits = Physics.RaycastAll( r3, 1000 , 1 << LayerMask.NameToLayer("Monster" ), QueryTriggerInteraction.UseGlobal ); for (int i = 0 ; i < hits.Length; i++){ print("碰到的所有物体 名字分别是" + hits[i].collider.gameObject.name); }
还有一种重载,不用传入射线,直接传入起点和方向,也可以用于判断;
之前的参数一射线,通过两个点传入。
1 2 3 4 5 6 7 8 9 RaycastHit[] hitInfos = Physics.RaycastAll(r3, 1000 , 1 << LayerMask.NameToLayer("Default" ), QueryTriggerInteraction.UseGlobal); for (int i = 0 ; i < hitInfos.Length; i++){ print(hitInfos[i].collider.gameObject.name); print(hitInfos[i].point); print(hitInfos[i].transform.position); print(hitInfos[i].normal); print(hitInfos[i].distance); }
还有一种函数,返回的碰撞的数量,通过传入数组得到数据。
注意:如果这个数组不赋值,将无法访问元素,因为这个函数是NonAlloc
的,即不分配内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 RaycastHit[] hitInfos = new RaycastHit[3 ]; if (Physics.RaycastNonAlloc(r3, hitInfos, 1000f , 1 << LayerMask.NameToLayer("Default" ), QueryTriggerInteraction.UseGlobal) > 0 ){ for (int i = 0 ; i < hitInfos.Length; i++) { print(hitInfos[i].collider.gameObject.name); print(hitInfos[i].point); print(hitInfos[i].transform.position); print(hitInfos[i].normal); print(hitInfos[i].distance); } }
知识点四 使用时注意的问题 注意: 距离、层级两个参数,都是数值类型。当我们传入参数时,一定要明确传入的参数代表的是距离还是层级。 举例 这样写是错误的,因为第二个参数,代表的是距离,不是层级。
1 2 3 4 5 6 if (Physics.Raycast(r3, 1 << LayerMask.NameToLayer("Monster" ))){ } public static bool Raycast (Ray ray, float maxDistance ) ;
应该这么写
1 2 3 4 5 if (Physics.Raycast(r3, 1000 , 1 << LayerMask.NameToLayer("Monster" ))){ } public static bool Raycast (Ray ray, float maxDistance, int layerMask ) ;
练习题 第一题 请用资料区给的资源,实现鼠标点击场景上一面墙,在点击的位置创建子弹特效和弹孔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Lesson23_P : MonoBehaviour { public GameObject bulletEff; public GameObject bulletHole; void Start () { bulletEff = Resources.Load<GameObject>("ArtRes/BulletEff/MagicArsenal/Effects/Prefabs/Projectiles/Fire/FireImpactSmall" ); bulletHole = Resources.Load<GameObject>("ArtRes/BulletEff/MagicArsenal/Effects/Prefabs/Projectiles/Fire/FireProjectileSmall" ); } void Update () { if (Input.GetMouseButtonDown(0 )) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hitInfo; if (Physics.Raycast(ray, out hitInfo, 1000 , 1 << LayerMask.NameToLayer("Walls" ), QueryTriggerInteraction.UseGlobal)) { GameObject obj = Instantiate(bulletEff); obj.transform.position = hitInfo.point; Destroy(obj, 2f ); GameObject hole = Instantiate(bulletHole); hole.transform.position = hitInfo.point + Vector3.back * 0.1f ; Destroy(hole, 5f ); } } } }
第二题 场景上有一个平面,有一个立方体,当鼠标点击选中立方体时,长按鼠标左键可以拖动立方体在平面上移动,点击鼠标右键取消选中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 using System.Collections;using System.Collections.Generic;using UnityEditor.PackageManager;using UnityEngine;public class Lesson23_P2 : MonoBehaviour { private GameObject selectedObj; void Update () { if (Input.GetMouseButtonDown(0 ) || Input.GetMouseButton(0 )) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hitInfo; if (selectedObj == null ) { if (Physics.Raycast(ray, out hitInfo, 1000 , 1 << LayerMask.NameToLayer("Default" ), QueryTriggerInteraction.UseGlobal)) { selectedObj = hitInfo.collider.gameObject; } } else { if (Physics.Raycast(ray, out hitInfo, 1000 , 1 << LayerMask.NameToLayer("Walls" ), QueryTriggerInteraction.UseGlobal)) { selectedObj.transform.position = hitInfo.point; } } } if (Input.GetMouseButtonDown(1 )) { selectedObj = null ; } } }
总结 学习的主要内容
3D 数学
Mathf
三角函数
Unity 当中的坐标系
Vector3 向量
Quaternion 四元数
Mono 当中的重要内容
延迟函数
协同程序
协同程序原理
Resources 资源加载
Unity 中的特殊文件夹
Resources 同步加载
Resources 异步加载
Resources 卸载资源
场景异步加载
画线功能:LineRenderer
核心系统
物理系统之范围检测
物理系统之射线检测
Unity 基础中知识点的重要性
向量和四元数——游戏中移动旋转都和它有关
协程——可以分时分步处理逻辑,避免卡顿
范围检测——动作游戏必备
射线检测——交互功能必备
资源场景的同步异步加载——所有功能必备
如何学好 Unity? 用所学知识点,独立的去模拟,你喜欢的游戏中的一些基础功能,从简单功能开始入手去练习。
随着你实现的功能越多知识点自然可以融会贯通。
强调 不要基础知识点都没有弄明白,就急于求成的去照着实践教学视频学习,实践视频是不会给讲知识点原理的。 要积少成多,慢慢积累,理论结合实践的提升自己的编程和逻辑能力。