帧
帧更新
帧相关概念:
Frame ,一个游戏帧
FrameRate ,帧率 / 刷新率
FPS , Frames Per Sencond ,每秒更新多少帧Update()
,称为帧更新
此方法会被游戏引擎定时调用,以更新游戏的状态,每更新1帧,调用一次Update()
方法
帧率观察:
- Time.time ,游戏时间
- Time.deltaTime ,距上次更新的时间差
显然,帧率是不固定的,Unity 会尽量较快地更新
Unity 不支持固定帧率,但可以设定一个近似帧率Application.targetFrameRate = 60;
其中,指定 Unity 尽量以 FPS = 60 的帧率更新游戏
移动物体
在 Update () 方法体中,移动物体的位置,添加如下代码:
1 | void Update() |
运行游戏,则物体沿 X 轴正向移动。
物体的运动并不是匀速的。
因为每次运动 0.01f ,但是每次运动间隔的 deltaTime 不固定
比如,
第1次,deltaTime = 0.0161536 秒,运动 0.01f
第2次,deltaTime = 0.0170517 秒,运动 0.01f
可以使用 deltaTime ,让物体的匀速运动:
1 | void Update() |
物体的运动
实现物体运动
- 布置测试场景
- 添加 小火车
- 添加脚本 SimpleLogic,控制 小火车 的运动
1 | void Update() |
实现小车向正北 ( Z轴正方向 ) 以1的速度移动
一般可直接使用 transform.Translate() 方法 ,实现相对运动transform.Translate( dx, dy, dz , ..)
其中,dx , dy, dz 是坐标增量。上述代码可改写为:
1 | void Update() |
相对运动
transform.Translate ()
,可以实现物体的运动transform.Translate (dx, dy, dz, space)
,注意比上节多传入一个参数space
其中,第4个参数space :
Space.World ,表示世界坐标系
Space.Self ,表示自身坐标系 ( 本地坐标系 )
Space.Self 更常用,沿自己的坐标轴、前后左右 移动
在建模时,要求物体的 正前面的朝向 与物体 +Z轴 一致
1
2
3
4
5
6
7void Update()
{
float speed = 1;
float distance = speed * Time.deltaTime;
// 向自身正前方运动
this.transform.Translate(0, 0, distance, Space.Self);
}
运动的方向
使物体朝着目标方向运动,添加一个GameObject “红旗”作为目的地
1 | public class SimpleLogic : MonoBehaviour |
其中重要的方法/传参:
GameObject.Find (name_or_path)
,根据名字/路径来查找物体transform.LookAt (target)
,使物体的Z轴指向目标物体Space.self
,沿物体自身坐标系的轴向运动
运动与停止
实现火车朝红旗运动,到红旗后停下来
1 | public class SimpleLogic : MonoBehaviour |
选中物体 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
7void Start()
{
// Quaternion类官方不建议使用
//var rot = transform.rotation;
// 欧拉角
transform.localEulerAngles = new Vector3(0, 45, 0);
}
但是这样只会在开始时执行start()方法加载,物体瞬间变成旋转45度后状态。所以需要在帧更新update()方法中添加代码
1
2
3
4
5
6
7
8
9void 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
18void 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 | void Update() |
可简化上节中使用欧拉角旋转的代码
自转与公转
自传,绕着自身轴旋转。
公转,围绕另一个物体旋转。
当父物体转动时,带动子物体一并旋转。
新建一个Empty Object,作为地球的父节点,是地球的旋转中心;
新建地球,并挂载旋转逻辑脚本;
1
2
3
4
5
6
7
8
9
10
11
12
13public class PlanetLogic : MonoBehaviour
{
void Start()
{
}
void Update()
{
float rotateSpeed = 10;
float rotateAngle = rotateSpeed * Time.deltaTime;
// 自转
this.transform.Rotate(0, rotateAngle, 0, Space.Self);
}
}
在地球中心位置再新建一个Empty Object,作为卫星的旋转中心;
新建卫星,并挂载卫星旋转逻辑脚本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public 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);
}
}
结构如下:
注意:第3步的作用是使卫星围绕地球旋转的转速与地球自转转速保持独立,如果直接把卫星作为地球的子节点,则卫星会以父节点地球自转的转速旋转。
脚本
场景加载机制
Unity 是一款游戏引擎,用于驱动游戏逻辑。
场景的加载过程:
创建节点
1
GameObject node = new GameObject();
实例化组件
1
MeshRenderer comp = new MeshRenderer();
实例化脚本组件
1
SimpleLogic script1 = new SimpleLogic()
调用事件函数
初始化
1
script1.Start();
帧更新
1
script1.Update();
注意:
- Unity 是一个纯面向对象的框架,对象由框架创建
- 同一个脚本,可以多次使用,挂到不同节点下
消息函数
MonoBehaviour 类是所有的脚本的基类,所有脚本一般应继承于 MonoBehaviour 类。
消息函数 ,也称为 事件函数,回调函数。
常见消息函数:
- Awake() ,初始化,仅执行一次
- Start() ,初始化,仅执行一次
- Update() ,帧更新,每帧调用一次
- OnEnable() ,每当组件启用时调用
- OnDisable() ,每当组件禁用时调用
我们在脚本中加上一些控制台打印信息
1 | private void Awake() |
运行游戏,多次启/禁用脚本,可以得到以下结果及结论:
- 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 中的顺序无关
优先级的设定:
- 选中一个脚本,打开 Execution Order 对话框
- 点 + 按钮,添加一个脚本
- 手动指定优先级,值越小、优先级越高。或者直接拖动栏目调节优先级顺序,越靠上面的脚本优先级越高
主控脚本
主控脚本,即游戏的主控逻辑,一般用于存放游戏的全局属性和全局配置。比如运行帧率的设置:
1 | private void Awake() |