Unity学习笔记(三)

帧更新

帧相关概念:

Frame ,一个游戏帧
FrameRate ,帧率 / 刷新率
FPS , Frames Per Sencond ,每秒更新多少帧
Update() ,称为帧更新
此方法会被游戏引擎定时调用,以更新游戏的状态,每更新1帧,调用一次Update()方法

帧率观察:

  • Time.time ,游戏时间
  • Time.deltaTime ,距上次更新的时间差

显然,帧率是不固定的,Unity 会尽量较快地更新

Unity 不支持固定帧率,但可以设定一个近似帧率
Application.targetFrameRate = 60;

其中,指定 Unity 尽量以 FPS = 60 的帧率更新游戏

移动物体

在 Update () 方法体中,移动物体的位置,添加如下代码:

1
2
3
4
5
6
7
8
9
10
void Update()
{
Debug.Log("***Update()帧更新,时间差为:" + Time.deltaTime);
// 获取当前位置
Vector3 pos = this.transform.localPosition;
// 当前位置x坐标加0.01
pos.x += 0.01f;
// 把移动后结果赋给当前位置
this.transform.localPosition = pos;
}

运行游戏,则物体沿 X 轴正向移动。

物体的运动并不是匀速的。

因为每次运动 0.01f ,但是每次运动间隔的 deltaTime 不固定

比如,image-20220429225625975

第1次,deltaTime = 0.0161536 秒,运动 0.01f

第2次,deltaTime = 0.0170517 秒,运动 0.01f

可以使用 deltaTime ,让物体的匀速运动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Update()
{
Debug.Log("***Update()帧更新,时间差为:" + Time.deltaTime);
// 初始化速度
float speed = 3;
// 每次update行进的距离
float distance = speed * Time.deltaTime;
// 获取当前位置
Vector3 pos = this.transform.localPosition;
// 当前位置x坐标加上每次update行进的距离
pos.x += distance;
// 把移动后结果赋给当前位置
this.transform.localPosition = pos;
}

物体的运动

实现物体运动

  1. 布置测试场景
  2. 添加 小火车
  3. 添加脚本 SimpleLogic,控制 小火车 的运动
1
2
3
4
5
6
7
8
void Update()
{
float speed = 1;
float distance = speed * Time.deltaTime;
Vector3 pos = this.transform.localPosition;
pos.z += distance;
this.transform.localPosition = pos;
}

实现小车向正北 ( Z轴正方向 ) 以1的速度移动
一般可直接使用 transform.Translate() 方法 ,实现相对运动
transform.Translate( dx, dy, dz , ..)
其中,dx , dy, dz 是坐标增量。上述代码可改写为:

1
2
3
4
5
6
7
8
9
10
11
void Update()
{
float speed = 1;
float distance = speed * Time.deltaTime;

//Vector3 pos = this.transform.localPosition;
//pos.z += distance;
//this.transform.localPosition = pos;
// Z 方向增加 distance
this.transform.Translate(0, 0, distance);
}

相对运动

transform.Translate () ,可以实现物体的运动
transform.Translate (dx, dy, dz, space),注意比上节多传入一个参数space

其中,第4个参数space :

  • Space.World ,表示世界坐标系

  • Space.Self ,表示自身坐标系 ( 本地坐标系 )

  • Space.Self 更常用,沿自己的坐标轴、前后左右 移动

  • 在建模时,要求物体的 正前面的朝向 与物体 +Z轴 一致

    1
    2
    3
    4
    5
    6
    7
    void Update()
    {
    float speed = 1;
    float distance = speed * Time.deltaTime;
    // 向自身正前方运动
    this.transform.Translate(0, 0, distance, Space.Self);
    }

运动的方向

使物体朝着目标方向运动,添加一个GameObject “红旗”作为目的地

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SimpleLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
// 找到目标物体
GameObject flag = GameObject.Find("红旗");
// 使物体转向目标物体
this.transform.LookAt(flag.transform);
}

// Update is called once per frame
void Update()
{
float speed = 1;
float distance = speed * Time.deltaTime;

this.transform.Translate(0, 0, distance, Space.Self);
}
}

其中重要的方法/传参:

  • GameObject.Find (name_or_path) ,根据名字/路径来查找物体

  • transform.LookAt (target),使物体的Z轴指向目标物体

  • Space.self,沿物体自身坐标系的轴向运动

运动与停止

实现火车朝红旗运动,到红旗后停下来

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
public class SimpleLogic : MonoBehaviour
{
// 定义目标物体
GameObject flag;

void Start()
{
// 初始化目标物体
flag = GameObject.Find("红旗");
// 使物体转向目标物体
this.transform.LookAt(flag.transform);
}

void Update()
{
// 火车坐标
Vector3 p1 = this.transform.position;
// 红旗坐标
Vector3 p2 = flag.transform.position;
// 火车与红旗的距离
Vector3 p = p2 - p1;
float distance = p.magnitude;

if (distance > 0.3f)
{
float speed = 1;
float move = speed * Time.deltaTime;
this.transform.Translate(0, 0, move, Space.Self);
}
}
}

选中物体 Lock View to Selected ,将3D视图锁定到目标物体
在物体运动时,3D视角随之移动。

物体的旋转

实现物体旋转

给物体调转一个旋转角度。

  • transform.Quaternion类 传入四元组 ( x, y, z, w )
    transform.rotation = ... 不便操作,官方不建议使用
    官方文档:https://docs.unity.cn/cn/current/Manual/index.html

  • 欧拉角 Euler Angle

    1
    2
    3
    4
    5
    6
    7
    void Start()
    {
    // Quaternion类官方不建议使用
    //var rot = transform.rotation;
    // 欧拉角
    transform.localEulerAngles = new Vector3(0, 45, 0);
    }
  • 但是这样只会在开始时执行start()方法加载,物体瞬间变成旋转45度后状态。所以需要在帧更新update()方法中添加代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void Update()
    {
    // 获取当前欧拉角
    Vector3 angles = this.transform.localEulerAngles;
    // 欧拉角增加0.5度
    angles.y += 0.5f;
    // 重新赋给当前欧拉角
    this.transform.localEulerAngles = angles;
    }

    优化点:因为每次帧更新时间间隔不同,所以上述代码并非匀速旋转,要想使其匀速旋转,需要定义旋转速度

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    void Start()
    {
    // 固定帧率,使间帧更新隔时间相对稳定
    Application.targetFrameRate = 60;
    }
    void Update()
    {
    // 初始化旋转速度
    float rotateSpeed = 30;
    // 每两次帧更新间隔时间内旋转的角度
    float rotateAngle = rotateSpeed * Time.deltaTime;
    // 获取当前欧拉角
    Vector3 angles = this.transform.localEulerAngles;
    // 欧拉角增加间隔时间内旋转角度
    angles.y += rotateAngle;
    // 重新赋给当前欧拉角
    this.transform.localEulerAngles = angles;
    }

相对旋转

Rotate() 方法,旋转一个相对角度
transform.Rotate (dx, dy, dz, space)

1
2
3
4
5
6
7
8
9
void Update()
{
// 初始化旋转速度
float rotateSpeed = 30;
// 每两次帧更新间隔时间内旋转的角度
float rotateAngle = rotateSpeed * Time.deltaTime;
// 使用Transform.Rotate() 方法旋转
this.transform.Rotate(0, rotateAngle, 0, Space.Self);
}

可简化上节中使用欧拉角旋转的代码

自转与公转

自传,绕着自身轴旋转。
公转,围绕另一个物体旋转。

当父物体转动时,带动子物体一并旋转。

  1. 新建一个Empty Object,作为地球的父节点,是地球的旋转中心;

  2. 新建地球,并挂载旋转逻辑脚本;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class PlanetLogic : MonoBehaviour
    {
    void Start()
    {
    }
    void Update()
    {
    float rotateSpeed = 10;
    float rotateAngle = rotateSpeed * Time.deltaTime;
    // 自转
    this.transform.Rotate(0, rotateAngle, 0, Space.Self);
    }
    }
  1. 在地球中心位置再新建一个Empty Object,作为卫星的旋转中心;

  2. 新建卫星,并挂载卫星旋转逻辑脚本。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
      public class SatelliteLogic : MonoBehaviour
    {
    void Start()
    {
    }

    void Update()
    {
    float rotateSpeed = 60;
    float rotateAngle = rotateSpeed * Time.deltaTime;
    // 子物体随父物体变换
    Transform parent = this.transform.parent;
    // 让父物体转动起来,即卫星旋转中心
    parent.Rotate(0, rotateAngle, 0, Space.Self);
    }
    }

结构如下:
image-20220430234959422

注意:第3步的作用是使卫星围绕地球旋转的转速与地球自转转速保持独立,如果直接把卫星作为地球的子节点,则卫星会以父节点地球自转的转速旋转。

脚本

场景加载机制

Unity 是一款游戏引擎,用于驱动游戏逻辑。
场景的加载过程:

  1. 创建节点

    1
    GameObject node = new GameObject();
  2. 实例化组件

    1
    MeshRenderer comp = new MeshRenderer();
  3. 实例化脚本组件

    1
    SimpleLogic script1 = new SimpleLogic()
  4. 调用事件函数

    • 初始化

      1
      script1.Start();
  • 帧更新

    1
    script1.Update();

注意:

  • Unity 是一个纯面向对象的框架,对象由框架创建
  • 同一个脚本,可以多次使用,挂到不同节点下

消息函数

MonoBehaviour 类是所有的脚本的基类,所有脚本一般应继承于 MonoBehaviour 类。
消息函数 ,也称为 事件函数,回调函数。

常见消息函数:

  • Awake() ,初始化,仅执行一次
  • Start() ,初始化,仅执行一次
  • Update() ,帧更新,每帧调用一次
  • OnEnable() ,每当组件启用时调用
  • 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
private void Awake()
{
Debug.Log("Awake方法调用");
}

void Start()
{
Debug.Log("Start方法调用");
}

void Update()
{

}

private void OnEnable()
{
Debug.Log("OnEnable方法调用");
}

private void OnDisable()
{
Debug.Log("OnDisable方法调用");
}

运行游戏,多次启/禁用脚本,可以得到以下结果及结论:
image-20220501225631243

  • Awake() 先于 Start() 调用,如果对象A的初始化代码需要依赖已初始化的对象B,则这一点会非常有用:此时,B的初始化应在Awake()中完成,A则应该在Start()中完成
  • 当组件被禁用时,运行游戏,Awake()方法仍会被调用;但是Start()和Update()方法不会
  • Start()和Awake()方法 ,都只执行一次,第一次启用时调用

注意点:

  • 已禁用的组件,其消息函数 Start() / Update() 不会被调用
  • Awake() / Start() ,都只会执行一次

脚本执行顺序

消息函数的调用顺序:
第1阶段初始化,所有脚本的Awake()方法:script1.Awake() , script2.Awake() , ...
第2阶段初始化,所有脚本的Start()方法:script1.Start() , script2.Start() , ...
帧更新,所有脚本的Update()方法:script1.Update(), script2.Update() , ...

  • 脚本的优先级 Script Execution Order
  • 默认地,所有脚本的优先级为0 ,无特定顺序
  • 执行顺序 和 在 Hierarchy 中的顺序无关

优先级的设定:
image-20220501230804089

  1. 选中一个脚本,打开 Execution Order 对话框
  2. 点 + 按钮,添加一个脚本
  3. 手动指定优先级,值越小、优先级越高。或者直接拖动栏目调节优先级顺序,越靠上面的脚本优先级越高

主控脚本

主控脚本,即游戏的主控逻辑,一般用于存放游戏的全局属性全局配置。比如运行帧率的设置:

1
2
3
4
private void Awake()
{
Application.targetFrameRate = 60;
}
0%