Unity 基础

目前状态

在学习过 Unity 入门后,你已经掌握的内容

  1. Unity 引擎的工作原理
  2. 能够熟练使用 Unity 引擎提供的各个重要组件
  3. 能够熟练使用 Unity 引擎提供的 API

主要学习内容

知识点

  1. Unity 中必备的 3D 数学知识
  2. Unity 中的核心系统和组件以及 APl
  3. 实践小项目
  4. 窥探如何制作商业游戏———配置文件

主要学习方式

理论+习题+实践

理论:语法操作相关知识

习题:基于知识点的针对性习题

实践:基于知识点的小项目实践

学习建议

重视基础知识点

多思考多练习

==切忌浮躁==

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
//     The well-known 3.14159265358979... value (Read Only).
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));        //2
print(Mathf.CeilToInt(1.3f)); //2

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));       //1
print(Mathf.FloorToInt(1.6f)); //1

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)); //11
print(Mathf.Clamp(13, 11, 20)); //13
print(Mathf.Clamp(20, 11, 20)); //20

print(Mathf.Clamp01(-1)); //0
print(Mathf.Clamp01(0.2f)); //0.2
print(Mathf.Clamp01(2)); //1

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));   //4
print(Mathf.Max(1, 2)); //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));    //1
print(Mathf.Min(1.1f, 0.4f)); //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)); //16
print(Mathf.Pow(2, 3)); //8

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));  //1
print(Mathf.Round(1.5f)); //2f

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));  //1.414214
print(Mathf.Sqrt(3f)); //1.732051

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));   //True
print(Mathf.IsPowerOfTwo(1)); //True
print(Mathf.IsPowerOfTwo(3)); //False
print(Mathf.IsPowerOfTwo(2)); //True

public static extern bool IsPowerOfTwo(int value);

12.判断正负数,返回它的符号 - Sign

1
2
3
4
5
6
7
8
print(Mathf.Sign(-1));  //-1
print(Mathf.Sign(2)); //1
print(Mathf.Sign(0)); //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;

// Start is called before the first frame update
void Start()
{
tempPos = followObj.position;
}

// Update is called once per frame
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;

//第二种使用方法,匀速移动,最后重合
//Vector3 newPos = followObj.position;
//time += Time.deltaTime;
//newPos.x = Mathf.Lerp(tempPos.x, targetObj.position.x, time);
//newPos.y = Mathf.Lerp(tempPos.y, targetObj.position.y, time);
//newPos.z = Mathf.Lerp(tempPos.z, targetObj.position.z, time);
//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); //57.29578

//角度转弧度
anger = 1;
rad = anger * Mathf.Deg2Rad;
print(rad); //0.01745329

知识点二 三角函数

正弦函数 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)); //0.5
print(Mathf.Cos(30 * Mathf.Deg2Rad) * 2); //1.732051
print(Mathf.Tan(30 * Mathf.Deg2Rad) * 3); //1.732051

知识点三 反三角函数

反三角函数

  1. 反三角函数是初等函数之一
  2. 包括反正弦函数、反余弦函数等

作用:通过反三角函数计算正弦值或余弦值对应的==弧度值==

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); //30
print(Mathf.Acos(0.8660254f) * Mathf.Rad2Deg); //30
print(Mathf.Atan(0.5773503f) * Mathf.Rad2Deg); //30

总结

  1. 三角函数———Mathf.Sin(弧度)、Mathf.Cos(弧度)
  2. 角度和弧度———Mathf.Rad2Deg、Mathf.Deg2Rad
  3. 三角函数曲线——Sin 和 Cos 函数曲线对于我们的意义
  4. 反三角函数———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;
// Start is called before the first frame update
void Start()
{
newPos = runObj.position;
moveSpeedDeg = 1;
}

// Update is called once per frame
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;

知识点三 屏幕坐标系

原点:屏幕左下角

轴向:

  • 向右为 x 轴正方向
  • 向上为 y 轴正方向

最大宽高:

  • Screen.width
  • Screen.height

Unity 当中获取屏幕坐标

通过鼠标位置获取屏幕坐标

1
2
3
Input.mousePosition;
Screen.width;
Screen.height;

知识点四 视口坐标系

原点:屏幕左下角

轴向:

  • 向右为 x 轴正方向
  • 向上为 y 轴正方向

特点:

  • 左下角为(0,0)
  • 右上角为(1,1)

和屏幕坐标类似,只不过是将坐标单位化,变成了比例。

Unity 当中的视口坐标

就是摄像机上的 ViewPort Rect;

x,y,width,height 都是 0~1。

坐标转换相关

UnityEngine.Camera - Unity 脚本 API

世界转本地

1
2
3
this.transform.InverseTransformDirection
this.transform.InverseTransformPoint
this.transform.InverseTransformVector

本地转世界

1
2
3
this.transform.TransformDirection
this.transform.TransformPoint
this.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); //1,0,0
print(B); //2,0,0
print(AB); //1,0,0
print(BA); //-1,0,0

对于我们的意义:要求 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);

总结

  1. Vector3 这边变量,可以表示一个点,也可以表示一个向量,具体表示什么,是根据我们的具体需求和逻辑决定。
  2. 如何在 Unity 里面,终点减起点就可以得到向量,点 C 也可以代表向量,代表的就是 OC 向量,O 是坐标系原点。
  3. 得到了向量,就可以利用 Vector3 中提供的成员属性得到模长和单位向量。
  4. 模长相当于可以得到两点之间的距离 ,单位向量主要是用来进行移动计算的,它不会影响我们想要的移动效果。

练习题

  1. Unity 中判断两点之间举例有几种方式?

    1. 使用 Vector3.Distance(Vector3, Vector3)方法
    2. 计算 AB 向量或者 BA 向量的模
  2. 计算向量(3,4,5)的模长(手写)

    根号(3 ^ 2 + 4 ^ 2 + 5 ^ 2) = 根号(9 + 16 + 25) = 根号 50。

  3. 计算向量(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;

总结

  1. 向量加法——主要用于位置平移和向量计算
  2. 向量减法——主要用于位置平移和向量计算
  3. 向量乘除法——主要用于模长放大缩小

练习题

用向量相关知识,实现摄像机跟随(摄像机不设置为对象子物体)

摄像机一直在物体的后方 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;

// Start is called before the first frame update
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);

//使用this.transform.forward和AB向量
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;
}

知识点二 通过点乘推导公式算出夹角

公式推导

  1. Cosβ = 直角边 / 单位向量模长

    直角边 = Cosβ * 单位向量模长

  2. 直角边 = 单位向量 A · 单位向量 B

    Cosβ * 单位向量 B 模长 = 单位向量 A · 单位向量 B

    Cosβ = 单位向量 A · 单位向量 B

推出结果:β = ArcCos(单位向量 A · 单位向量 B)。

image-20230308155552604

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);

//使用API
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-15f)
{
return 0f;
}

//公式
float num2 = Mathf.Clamp(Dot(from, to) / num, -1f, 1f);
return (float)Math.Acos(num2) * 57.29578f;
}

总结

向量点乘对于我们的意义

  1. 判断对象的大致方位
  2. 计算两个向量之间的夹角

练习题

当一个物体 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;

// Update is called once per frame
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 使用公式一步步算

//使用公式计算
//计算AB向量
Vector3 AB = targetTransform.position - this.transform.position;
float distance = Mathf.Sqrt(AB.x * AB.x + AB.y * AB.y + AB.z * AB.z);
//计算AB向量模长
float abModLen = Mathf.Sqrt(AB.x * AB.x + AB.y * AB.y + AB.z * AB.z);
//求AB向量的单位向量
AB = new Vector3(AB.x / abModLen, AB.y / abModLen, AB.z / abModLen);
//求两个单位向量点乘结果,即Cosβ
float dotRet = this.transform.forward.x * AB.x + this.transform.forward.y * AB.y + this.transform.forward.z * AB.z;
//根据Cosβ,使用Acos求得弧度,再乘转换值得到角度
float angle = Mathf.Acos(dotRet) * Mathf.Rad2Deg;
if (distance < 5 && angle >= 0f && angle <= 45f)
{
print($"计算方式1--发现入侵者,距离:{distance},角度:{angle}");
}
#endregion

#region 使用API计算Cosβ
//使用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直接计算夹角
//使用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 左侧

总结

向量叉乘对于我们的意义

  1. 得到一个平面的法向量
  2. 得到两个向量之间的左右位置关系

练习题

  1. 判断一个物体 B 位置再另一个物体 A 的位置的左上,左下,右上,右下哪个方位。
  2. 当一个物体 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;
// Update is called once per frame
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;

// Update is called once per frame
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);

总结

  1. 线性插值——用于跟随移动,摄像机跟随。
  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
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;

// Start is called before the first frame update
void Start()
{
nowTargetPos = target.position - target.forward * 4 + target.up * 7;
startPos = Camera.main.transform.position;
}

private void LateUpdate()
{
//第一种,先快后慢
//Camera.main.transform.position = Vector3.Lerp(Camera.main.transform.position, nowTargetPos, Time.deltaTime);
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;

// Start is called before the first frame update
void Start()
{
moveLeft = true;
startPos = sun.position;
time = 0;
}

// Update is called once per frame
void Update()
{
float leftDistance = Vector3.Distance(sun.position, leftPos.position) - 1E-1f;
float rightDistance = Vector3.Distance(sun.position, rightPos.position) - 1E-1f;
if (leftDistance < 1E-1f || rightDistance < 1E-1f)
{
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 度的角度

缺点:

  • 同一旋转的表示不唯一

    就是 x 轴转 90 度,等于 x 轴转 450 度

  • 万向节死锁

    简单理解就是,当两个旋转轴重合时,继续旋转时,发生了维度的丢失,导致旋转的错误旋转。

    欧拉角万向节死锁 - 知乎 (zhihu.com)

万向节死锁

当某个特定轴达到某个特殊值时,绕一个轴旋转可能会覆盖住另一个轴的旋转,从而失去一维自由度。

Unity 中 X 轴达到 90 度时,会产生万向节死锁,此时旋转 y 轴和 z 轴,都只会绕着 z 轴旋转。

总结

因为欧拉角存在一些缺点

  1. 同一旋转的表示不唯一
  2. 万向节死锁

而四元数旋转不存在万向节死锁问题,因此在计算机中我们往往使用四元数来表示三维空间中的旋转信息。

四元数是什么 知识点

四元数的概念

四元数是简单的超复数,由实数加上三个虚数单位组成,主要用于在三维空间中表示旋转。

四元数原理包含大量数学相关知识,较为复杂比如 ∶ 复数、四维空间等等;

因此此处我们只对其基本构成和基本公式进行讲解,如想深入了解数学原理请从数学层面去查找资料了解它。

四元数构成

一个四元数包含一个标量和一个 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
//四元数Q = [cos(β/2),  sin(β/2)x, sin(β/2)y, sin(β/2)z]
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
Quaternion.Euler(x,y,z)

四元数转欧拉角

1
2
Quaternion q;
q.eulerAngles
1
2
3
4
print(Quaternion.Euler(q.eulerAngles));
print(q.eulerAngles);
//(0.5, 0.0, 0.0, 0.9)
//(60.0, 0.0, 0.0)

知识点三 四元数弥补的欧拉角缺点

欧拉角缺点:

  1. 同一旋转的表示不唯一
  2. 万向节死锁

必备知识点:四元数相乘代表旋转四元数

同一旋转的表示不唯一

由于欧拉角的性质,90 度的旋转和 450 的旋转时一致的,表示不唯一。

而四元数的旋转,只会在[0,180],[0,-180]之间

万向节死锁

我们想让物体绕 y 轴转,但是由于万向节死锁,物体会绕着 z 轴转。

1
2
3
4
5
6
7
//start
this.transform.eulerAngles = Vector3.right * 90;

//update
Vector3 e = this.transform.eulerAngles;
e += Vector3.up;
this.transform.rotation = Quaternion.Euler(e);

使用四元数解决

四元数相乘,表示一个旋转。

注意:这里的旋转是相对于本地坐标系的

1
this.transform.rotation *= Quaternion.AngleAxis(1, Vector3.up);

总结

  1. 四元数构成——[cos(β / 2),sin(β / 2) x,sin(β / 2) y,sin( p / 2) z]
  2. Unity 中的四元数——Quaternion
  3. 四元数弥补了欧拉角的缺点—同一旋转的表示不唯一、万向节死锁

注意:我们一般不会直接通过四元数的 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); //(0.0, 0.0, 0.0, 1.0)
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); //0, 0, 0
forward = Quaternion.AngleAxis(45, Vector3.up) * forward;
print(forward); //0.7, 0, 0.7
forward = Quaternion.AngleAxis(45, Vector3.up) * forward;
print(forward); //1, 0, 0

使用场景:飞机发射子弹,已知飞机朝向,只需要一个四元数乘上朝向向量,就可以得到不同方向,就可以在不同方向发射子弹。

练习题

第一题

用目前所学知识,模拟飞机发射不同类型子弹的方法

单发,双发,扇形,环形

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;

// Update is called once per frame
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:
//由于四元数的旋转量唯一,只能是-180~180,所以正半圆是-90~90
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:
//由于四元数的旋转量唯一,只能是-180~180,所以环形是-180~180
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;
// Start is called before the first frame update
void Start()
{
Destroy(this.gameObject, 3);
}

// Update is called once per frame
void Update()
{
this.transform.Translate(Vector3.forward * Time.deltaTime * moveSpeed);
}
}

第二题

用所学 3D 数学知识实现摄像机跟随效果

  1. 摄像机在人物斜后方,通过角度控制倾斜率
  2. 通过鼠标滚轮可以控制摄像机距离人物的距离(有最大最小限制)
  3. 摄像机看向人物头顶上方一个位置(可调节)
  4. Vector3.Lerp 实现相机跟随人物
  5. 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;

//使用Lerp移动到指定位置
this.transform.position = Vector3.Lerp(this.transform.position, nowPos, Time.deltaTime * moveSpeed);
//辅助线
Debug.DrawLine(this.transform.position, target.position + target.up * headOffset);

//使用Slerp看向对象
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);
}

注意:

  1. 延时函数第一个参数传入的是函数名字符串
  2. 延时函数没办法传入参数,只有包裹一层
    1. 就是在一个无参函数里面调用有参函数,然后 Invoke 调用这个无参函数。
  3. 函数名必须是该脚本上声明的函数

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
{
// Start is called before the first frame update
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 运行的。

注意:

  1. Unity 中的多线程,要记住关闭。因为只要 Unity 编辑器没有关闭,线程就不会结束。
  2. Unity 当中使用多线程有使用限制,大部分 Unity 内容不可在多线程当中访问
  3. 虽然不能使用 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)
{
//线程当中不能使用Unity.Random
System.Random r = new System.Random();
que.Enqueue(r.Next(-10, 10));
}
}

private void OnDestroy()
{
//使用Abort方法关闭线程
t.Abort();
t = null;
}
}

知识点二 协同程序是什么?

协同程序简称协程,它是“假”的多线程,它==不是==多线程。

它的主要作用:将代码分时执行,不卡主线程。

简单理解,是把可能会让主线程卡顿的耗时的逻辑分时分步执行。

协程函数每一次返回,只是将协程挂起,等待下一次执行。

主要使用场景

  • 异步加载文件
  • 异步下载文件
  • 场景异步加载
  • 批量创建时防止卡顿

知识点三 协同程序和线程的区别

新开一个线程是独立的一个管道,和主线程并行执行

新开一个协程是在原线程之上开启,进行逻辑分时分步执行

知识点四 协程的使用

继承 MonoBehavior 的类都可以开启协程函数。

第一步:声明协程函数

协程函数 2 个关键点

  1. 返回值为 IEnumerator 类型及其子类
  2. 函数中通过yield return 返回值; 进行返回
1
2
3
4
5
6
7
8
9
10
//返回值为IEnumerator
private IEnumerator MyCoroutine(int i, string str)
{
print(i);
//返回一个等待5秒的类
yield return new WaitForSeconds(5f); //一个yield return就是一个分段
print(str);
yield return new WaitForSeconds(2f);
print(233);
}

第二步:开启协程函数

协程函数是不能够直接这样去执行的!!!!!!!

这样执行没有任何效果

1
MyCoroutine(1, "123");

常用开启方式

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.跳出协程

1
yield break;

知识点六 协程受对象和组件失活销毁的影响

协程开启后,组件和物体销毁,协程不执行

物体失活协程不执行,组件失活协程仍然执行

总结

  1. Unity 支持多线程,只是新开线程无法访问主线程中 Unity 相关内容

    一般主要用于进行复杂逻辑运算或者网络消息接收等等

    注意:Unity 中的多线程一定记住关闭

  2. 协同程序不是多线程,它是将线程中逻辑进行分时执行,避免卡顿

  3. 继承 MonoBehavior 的类都可以使用协程

  4. 开启协程方法、关闭协程方法

  5. yield return 返回的内容对于我们的意义

  6. 协程只有当组件单独失活时不受影响,其它情况协程会停止

练习题

第一题

利用协程制作一个计秒器

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;
// Start is called before the first frame update
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
{
// Update is called once per frame
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);
}
}
}

协同程序原理 知识点

知识点一 协程的本质

协程可以分成两部分:

  1. 协程函数本体
  2. 协程调度器

协程本体就是一个能够中间暂停返回的函数

协程调度器是 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);
}
//output
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");

注意:需要我们自己将创建

作用:资源文件夹

  1. 需要通过 Resources 相关 API 动态加载的资源需要放在其中
  2. 该文件夹下所有文件都会被打包出去
  3. 打包时 Unity 会对其压缩加密
  4. 该文件夹打包后只读 只能通过 Resources 相关 API 加载

知识点三 StreamingAssets 流动资源文件夹

路径获取:

1
print(Application.streamingAssetsPath);

注意:需要我们自己创建

作用:流文件夹

  1. 打包出去不会被压缩加密,可以任由我们摆布
  2. 移动平台只读,PC 平台可读可写
  3. 可以放入一些需要自定义动态加载的初始资源

知识点四 persistentDataPath 持久数据文件夹

路径获取:

1
print(Application.persistentDataPath);

注意:不需要我们自己将创建

作用:固定数据文件夹

  1. 所有平台都可读可写
  2. 一般用于放置动态下载或者动态创建的文件,游戏中创建或者获取的文件都放在其中

知识点五 Plugins 插件文件夹

路径获取:一般不获取

注意:需要我们自己将创建

作用:插件文件夹,不同平台的插件相关文件放在其中,比如 IOS 和 Android 平台。

知识点六 Editor 编辑器文件夹

路径获取:一般不获取,如果硬要获取 可以用工程路径拼接。

1
print(Application.dataPath + "/Editor");

注意:需要我们自己将创建
作用:编辑器文件夹

  1. 开发 Unity 编辑器时,编辑器相关脚本放在该文件夹中
  2. 该文件夹中内容不会被打包出去

知识点七 默认资源文件夹 Standard Assets

路径获取:一般不获取

注意:需要我们自己将创建

作用:默认资源文件夹

一般 Unity 自带资源都放在这个文件夹下,代码和资源优先被编译

Resources 资源同步加载 知识点

知识点一 Resources 资源动态加载的作用

  1. 通过代码动态加载 Resources 文件夹下指定路径资源
  2. 避免繁琐的拖曳操作

知识点二 常用资源类型

  1. 预设体对象——GameObject

  2. 音效文件——AudioClip

  3. 文本文件——TextAsset

  4. 图片文件——Texture

  5. 其它类型——需要什么用什么类型

注意:预设体对象加载需要实例化,其它资源加载一般直接用。

知识点三 资源同步加载 普通方法

在一个工程当中 Resources 文件夹,可以在多个目录下有多个,通过 API 加载时,它会自己去这些同名的 Resources 文件夹中去找资源,打包时 Resources 文件夹里的内容都会打包在一起。

1.预设体对象

想要创建在场景上,记住实例化

第一步:要去加载预设体的资源文件(本质上,就是加载配置数据在内存中)。

1
Object obj = Resources.Load("Cube");

第二步:如果想要在场景上创建预设体,一定是加载配置文件过后,然后实例化。

1
Instantiate(obj);

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 //xml配置文件
.bytes //二进制文件
.json //json配置文件
.html //网页文件
.csv.....
1
TextAsset ta = Resources.Load("Txt/Test") as TextAsset;

文本内容

1
print(ta.text);

字节数据组

1
print(ta.bytes);

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");
//这个,刚刚执行了异步加载的执行代码,资源还没有加载完毕,这样用是不对的。
//一定要等加载结束过后,才能使用。
//rq.asset ××××××××××××

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("加载结束");
//asset 是资源对象 加载完毕过后 就能够得到它
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");
//yield return rq;
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);
//Debug.Log("Already load");
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);
//Unity内部的 协程协调器 发现是异步加载类型的返回对象 那么就会等待
//等待异步加载结束后 才会继续执行 迭代器函数中后面的步骤
print("异步加载过程中 打印的信息");
//协程的好处 是异步加载场景时 我可以在加载的同时 做一些别的逻辑
//yield return ao;
//第二步
print("异步加载结束后 打印的信息");

//比如 我们可以在异步加载过程中 去更新进度条
//第一种 就是利用 场景异步加载 的进度 去更新 但是 不是特别准确 一般也不会直接用
//while(!ao.isDone)
//{
// print(ao.progress);
// yield return null;
//}

//离开循环后 就会认为场景加载结束
//可以把进度条顶满 然后 隐藏进度条

//第二种 就是根据你游戏的规则 自己定义 进度条变化的条件
yield return ao;
//场景加载结束 更新20%进度
//接着去加载场景中 的其它信息
//比如
//动态加载怪物
//这时 进度条 再更新20%
//动态加载 场景模型
//这时 就认为 加载结束了 进度条顶满
//隐藏进度条
}

总结

场景异步加载和资源异步加载 一样,有两种方式

  1. 通过事件回调函数
  2. 协程异步加载

他们的优缺点表现和资源异步加载 也是一样的

  1. 事件回调函数

    优点:写法简单,逻辑清晰

    缺点:只能加载完场景做一些事情 不能再加载过程中处理逻辑

  2. 协程异步加载

    优点:可以在加载过程中处理逻辑,比如进度条更新等

    缺点:写法较为麻烦,要通过协程

练习题

请写一个简单的场景管理器,提供统一的方法给外部用于场景异步切换。外部可以传入委托用于当异步切换结束时执行某些逻辑

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 提供的一个用于画线的组件,使用它我们可以在场景中绘制线段。

一般可以用于:

  1. 绘制攻击范围
  2. 武器红外线
  3. 辅助功能
  4. 其它画线功能

知识点二 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;
//将z改为10,这个是距离
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

知识回顾 物理系统之碰撞检测

碰撞产生的必要条件

  1. 至少一个物体有刚体
  2. 两个物体都必须有碰撞器

碰撞和触发

  • 碰撞会产生实际的物理效果
  • 触发看起来不会产生碰撞,但是可以通过函数监听触发
  • 碰撞检测主要用于实体物体之间产生物理效果时使用

知识点一 什么是范围检测

游戏中瞬时的攻击范围判断一般会使用范围检测

举例:

  1. 玩家在前方 5m 处释放一个地刺魔法,在此处范围内的对象将受到地刺伤害;
  2. 玩家攻击,在前方 1 米圆形范围内对象都受到伤害。

等等

类似这种并没有实体物体,只想要检测在指定某一范围,是否让敌方受到伤害时,便可以使用范围判断。

简而言之:在指定位置,进行范围判断,我们可以得到处于指定范围内的对象。

目的是对对象进行处理,比如受伤、减血等等。

知识点二 如何进行范围检测

Physics-OverlapBox - Unity 脚本 API

必备条件:想要被范围检测到的对象,必须具备碰撞器(Collider)

注意点:

  1. 范围检测相关 API,只有当执行该句代码时,进行一次范围检测,它是瞬时的。
  2. 范围检测相关 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

  • 返回值:碰撞到的碰撞器数量

  • 参数:传入一个数组进行存储

  • Physics.OverlapSphereNonAlloc

    1
    2
    3
    4
    if( Physics.OverlapSphereNonAlloc(Vector3.zero, 5, colliders) != 0 )
    {

    }

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 键可以控制其前后移动和旋转。

请结合所学知识实现

  1. 按 J 键在立方体面朝向前方 1 米处进行立方体范围检测
  2. 按 K 键在立方体前面 5 米范围内进行胶囊范围检测
  3. 按 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);

//按J键在立方体面朝向前方1米处进行立方体范围检测
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()
+ "米");
}
}

//按K键在立方体前面5米范围内进行胶囊范围检测
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()
+ "米");
}
}

//按L键以立方体脚下为原点,半径10米内进行球形范围检测
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. 碰撞检测——必备条件 1 刚体 2 碰撞器
  2. 范围检测——必备条件 碰撞器

如果想要做这样的碰撞检测呢?

  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
//就是上一个API,不过射线换成了临时创建的。
if (Physics.Raycast(Vector3.zero,
Vector3.forward,
1000,
1 << LayerMask.NameToLayer("Monster"),
QueryTriggerInteraction.UseGlobal))
{
print("碰撞到了对象2");
}

2.获取相交的单个物体信息

物体信息类RaycastHit

1
RaycastHit hitInfo;
  • 参数一:射线
  • 参数二: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;
// Start is called before the first frame update
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");
}

// Update is called once per frame
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;

// Update is called once per frame
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;
}

//下面这个是左键抬起就不选
//if (Input.GetMouseButtonUp(0) || Input.GetMouseButtonDown(1))
//{
// selectedObj = null;
//}
}
}

总结

学习的主要内容

  1. 3D 数学
    1. Mathf
    2. 三角函数
    3. Unity 当中的坐标系
    4. Vector3 向量
    5. Quaternion 四元数
  2. Mono 当中的重要内容
    1. 延迟函数
    2. 协同程序
    3. 协同程序原理
  3. Resources 资源加载
    1. Unity 中的特殊文件夹
    2. Resources 同步加载
    3. Resources 异步加载
    4. Resources 卸载资源
  4. 场景异步加载
  5. 画线功能:LineRenderer
  6. 核心系统
    1. 物理系统之范围检测
    2. 物理系统之射线检测

Unity 基础中知识点的重要性

  • 向量和四元数——游戏中移动旋转都和它有关
  • 协程——可以分时分步处理逻辑,避免卡顿
  • 范围检测——动作游戏必备
  • 射线检测——交互功能必备
  • 资源场景的同步异步加载——所有功能必备

如何学好 Unity?

用所学知识点,独立的去模拟,你喜欢的游戏中的一些基础功能,从简单功能开始入手去练习。

随着你实现的功能越多知识点自然可以融会贯通。

强调

不要基础知识点都没有弄明白,就急于求成的去照着实践教学视频学习,实践视频是不会给讲知识点原理的。
要积少成多,慢慢积累,理论结合实践的提升自己的编程和逻辑能力。