Unity3D功能开发入门系列(三)
- 一、运动脚本
- (一)物体的运动
- (二)相对运动
- (三)运动的方向
- 二、旋转脚本
- (一)物体的旋转
- (二)相对旋转
- (三)自传与公转
- (四)官方文档
- 三、脚本的运行
- (一)脚本的运行
- (二)消息函数
- (三)脚本执行顺序
- (四)主控脚本
- 四、脚本的参数
- (一)脚本的参数
- (二)参数的赋值
- (三)值类型
- (四)引用类型
- (五)运行时调试
- 五、鼠标键盘输入
- (一)鼠标输入
- (二)屏幕坐标
- (三)键盘输入
一、运动脚本
准备工作:
- 布置测试场景,添加小火车
- 添加脚本 SimpleLogic,控制小火车移动
(一)物体的运动
一般使用 transform.Translate( ),实现相对运动
transform.Translate( dx, dy, dz, …) ,其中 dx, dy, dz 是坐标增量
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
float speed = 1;
float distance = speed * Time.deltaTime;
//Vector3 pos = this.transform.localPosition;
//pos.z += distance; // 0.005f;
//this.transform.localPosition = pos;
this.transform.Translate(distance, 0, distance);
}
}
(二)相对运动
transform.Translate( dx, dy, dz, space),其中第 4 个参数:
-
Space.World,相对于 世界坐标系
-
Space.Self,相对于 自身坐标系(本地坐标系)<更常用>
在建模时,要求物体的 脸的朝向 与物体 +Z轴 一致
(三)运动的方向
使物体朝着目标方向移动(这里以小红旗为例),到达目标后停下来
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Unity.IO.LowLevel.Unsafe.AsyncReadManagerMetrics;
public class MoveLogic : 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()
{
GameObject flag = GameObject.Find("红旗");
Vector3 p1 = this.transform.position;
Vector3 p2 = flag.transform.position;
Vector3 p = p2 - p1;
float distance = p.magnitude; // magnitude 取向量的模
if(distance > 0.3f)
{
float speed = 2;
float move = speed * Time.deltaTime;
this.transform.Translate(0, 0, move, Space.Self);
}
}
}
- 其中:
GameObject.Find( name_or_path),根据 名字 / 路径 查找物体
transform.LookAt( target),使物体的 Z轴 指向 target 物体(若目标在空中,火车就原地起飞了)
Space.self,沿物体自身坐标系的轴向运动- 如果想让视角随着物体运动
二、旋转脚本
(一)物体的旋转
给物体调转一个旋转的角度
- Quaternion 四元组(x, y, z, w)
transform.rotation = … 不便操作,官方也不建议使用 - 欧拉角 Euler Angle
transform.eulerAngles = new Vector3( 0, 45, 0);
transform.localEulerAngles = new Vector( 0, 45, 0);
注:new Vector( 0, 45, 0)、new Vector( 0, 405, 0)、new Vector( 0, -315, 0) 效果相同
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RotateLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Application.targetFrameRate = 60;
}
// Update is called once per frame
void Update()
{
float rotateSpeed = 60; // 每秒转 60°
Vector3 angles = this.transform.localEulerAngles;
angles.y += rotateSpeed * Time.deltaTime; //0.5f;
this.transform.localEulerAngles = angles;
}
}
(二)相对旋转
Rotate( ),旋转一个相对角度
transform.Rotate( dx, dy, dz, space)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RotateLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Application.targetFrameRate = 60;
}
// Update is called once per frame
void Update()
{
float rotateSpeed = 60; // 每秒转 60°
//Vector3 angles = this.transform.localEulerAngles;
//angles.y += rotateSpeed * Time.deltaTime; //0.5f;
//this.transform.localEulerAngles = angles;
this.transform.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.Self);
}
}
(三)自传与公转
- 自转,绕着自身轴旋转
- 公转,围绕另一个物体旋转
当父物体转动时,带动子物体一并旋转
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/** 此脚本挂在卫星上
*
*/
public class MoonLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
float rotateSpeed = 60;
// 找到父物体
Transform parent = this.transform.parent;
// 控制父物体旋转
parent.Rotate(0, rotateSpeed* Time.deltaTime, 0, Space.Self);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/** 地球运动的脚本
*
*/
public class PlanetLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
float rotateSpeed = 10;
this.transform.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.Self);
}
}
(四)官方文档
手册 Manual + Script API:https://docs.unity.cn/cn/2022.3/ScriptReference/index.html
三、脚本的运行
(一)脚本的运行
Unity 是一款游戏引擎,用于驱动游戏逻辑
(1)场景的加载过程(伪代码)
- 创建节点:GameObject node = new GameObject( )
- 实例化组件:MeshRendered comp = new MeshRender( )
- 实例化脚本组件:SimpleLogic script1 = new SimpleLogic( )
- 调用事件函数:初始化 script1.Start( )、帧更新 script1.Update( )
注:Unity 是一个纯面向对象的框架,对象由框架创建
(2)再添加一个物体,也挂载相同的 SimpleLogic 脚本(伪代码)
- GameObject node2 = new GameObject( )
- SimpleLogic script2 = new SimpleLogic( )
- script2.Start( )
- script2.Update( )
其中,又创建了一个 SimpleLogic 实例,挂在了新的节点下
(二)消息函数
所有的脚本,一般应继承于 MonoBehavour。消息函数,或称 事件函数,一些回调函数
常见的消息函数:
函数 | 说明 |
---|---|
Awake | 初始化,仅执行一次 |
Start | 初始化,仅执行一次 |
Update | 帧更新,每帧调用一次 |
OnEnable | 每当组件启用时调用 |
OnDisable | 每当组件禁用时调用 |
注:
- Awake 只执行一次,总是先于 Start 调用,且总是调用(即使组件被禁用)
- Start 只执行一次,第一次启用时调用,若组件被禁用,就不调用
(三)脚本执行顺序
Unity 默认是单线程的协程异步
1. 消息函数的调用顺序:
第1阶段初始化:script1.Awake( ),script2.Awake( ),…
第2阶段初始化:script1.Start( ),script2.Start( ),…
帧更新:script1.Update( ),script2.Update( ),…
2. 脚本优先级 Script Execution Order
默认的,所以脚本的优先级为0,无特定顺序(执行顺序和在 Hierarchy 中的顺序无关)
优先级的设定:
- 选中一个脚本,打开 Execution Order 对话框
- 点 + 按钮,添加要调整优先级的脚本
- 指定优先级,值越小、优先级越高;或者,直接拖动调节顺序
一般的,并不需要显示设置 Execution Order,默认即可
(四)主控脚本
主控脚本,即游戏的主控逻辑,设置一些全局性的设置、全局性的逻辑,其他的脚本就只用负责各种的功能了
四、脚本的参数
(一)脚本的参数
脚本的参数,用于控制脚本组件的功能
添加下面这行代码到 RotateLogic 的全局中后:
public float rotateSpeed = 30f;
参数的用法:
- 参数必须为 public,才可以在检查器中显示
- 参数的名称,即变量名 rotateSpeed -> Rotate Speed
- 参数的默认值,即变量的默认值(可以 Reset 菜单重置)
- 参数的工具提示,可以用
[ Tooltip( ) ]
指定,如:[ Tooltip(" 旋转速度 ") ]
[Tooltip("这个是Y轴向的角速度")]
public float rotateSpeed = 30f;
这样同一个脚本就可以在多个物体上使用,其相关参数可以通过检查器调节
(二)参数的赋值
以下按时间顺序:
- 定义默认值 public float rotateSpeed = 30f;
- 在检查器中赋值 script.rotateSpeed = 180f; // 由 Unity 框架对参数赋值
- 在 Awake 中初始化
- 在 Start 中初始化
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RotateLogic : MonoBehaviour
{
[Tooltip("这个是Y轴向的角速度")]
public float rotateSpeed = 30f;
private void Awake()
{
this.rotateSpeed = 90;
Debug.Log("** Awake , rotateSpeed=" + this.rotateSpeed);
}
void Start()
{
this.rotateSpeed = 120;
Debug.Log("** Start , rotateSpeed=" + this.rotateSpeed);
}
void Update()
{
// float speed = 30f; // 每秒转
Debug.Log("** 速度=" + this.rotateSpeed);
this.transform.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.Self);
}
}
伪代码表示 (值最终是120):
RotateY script = new RotateY( ) // 30
script.rotateSpeed = 180f // 180
script.Awake( ) // 90
script.Start( ) // 120
(三)值类型
参数的类型,分为 值类型(栈上)、引用类型(堆上)
值类型,如 int,float,bool
值类型 (struct) ,如 Vector3,Color
引用类型 (class) ,如 GameObject,Transform,MeshRendered …
- 值类型的特点:
- 本身是一个值,可以直接赋值
- 若为赋值,则默认为 0
- 不能为 null
- 结构体 struct,也是值类型(特点与1同)
设初始值要new
eg. public Vector3 rotateSpeed = new Vector3(1, 1, 1)
其实,C# 里面,int(struct System.int32)、float(struct System.Single) 本质也是 struct 类型;string 原则上属于 class 类型。C# 内部本质只有 struct 和 class 两种类型
(四)引用类型
参数也可以是引用类型
- 节点,GameObject
- 组件,如 Transform、MeshRenderer、AudioSourse …
- 资源,如 Material、Texture、AudioClip
- 数组类型
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TrainLogic : MonoBehaviour
{
public GameObject target;
void Start()
{
Application.targetFrameRate = 60;
this.transform.LookAt(target.transform);
}
void Update()
{
Vector3 p1 = this.transform.position;
Vector3 p2 = target.transform.position;
Vector3 p = p2 - p1;
float distance = p.magnitude;
if(distance > 0.3f)
{
float speed = 2;
float move = speed * Time.deltaTime;
this.transform.Translate( 0, 0, move, Space.Self);
}
}
}
(五)运行时调试
在游戏运行时,可以对 物体 / 组件 进行 实时调试,但是在运行模式下,所有的参数不能保存到场景。保存参数的办法:
- 在 Play Mode 下,暂停一下游戏,组件 Copy Component
- 在 Edit Mode 下,组件 Paste Component Values
五、鼠标键盘输入
(一)鼠标输入
游戏的输入,可以来自鼠标、键盘、触摸屏、游戏手柄等
鼠标输入相关 API:
指令 | 含义 |
---|---|
Input.GetMouseButtonDown( ) | 探测用户是否按下鼠标 |
Input.GetMouseButtonUp( ) | 探测用户是否抬起鼠标 |
Input.GetMouseButton( ) | 状态探测,探测用户是否一直按着鼠标 |
1. 事件探测
其中,
0 左键 / 1 右键 / 2 中键
,且鼠标事件只触发一次,不会重复触发
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FanLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
Debug.Log("** 鼠标按下");
}
if(Input.GetMouseButtonUp(0))
{
Debug.Log("** 鼠标抬起");
}
float speed = 180;
this.transform.Rotate(0, speed * Time.deltaTime, 0, Space.Self);
}
}
练习:鼠标按下时,旋转;鼠标松开时,停止 。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FanLogic : MonoBehaviour
{
float m_speed = 0;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
Debug.Log("** 鼠标按下");
m_speed = 180;
}
if(Input.GetMouseButtonUp(0))
{
Debug.Log("** 鼠标抬起");
m_speed = 0;
}
// float speed = 180;
this.transform.Rotate(0, m_speed * Time.deltaTime, 0, Space.Self);
}
}
注:操作时,鼠标应该在 Game 窗口里点击,不是 Scene窗口
2. 状态探测
区分: 事件探测 VS 状态探测
鼠标事件,只触发一次:Input.GetMouseButtonDown() 、Input.GetMouseButtonUp()
鼠标状态,表示当前是否正在被按下:Input.GetMouseButton()
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FanLogic : MonoBehaviour
{
float m_speed = 0;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButton(0))
{
Debug.Log("** 鼠标按下ing,正被按下");
m_speed = 180;
}
else
{
m_speed = 0;
}
// float speed = 180;
this.transform.Rotate(0, m_speed * Time.deltaTime, 0, Space.Self);
}
}
- 如果要实现加减速等更复杂的游戏逻辑,事件显然比状态好
- 鼠标事件是全局的,每个脚本 互不影响,大家都可以探测,不会被哪个物体截断
(二)屏幕坐标
1. 鼠标按下时,取得鼠标的当前所在位置(其中,mousePosition 是鼠标在屏幕上的坐标):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
int width = Screen.width;
int height = Screen.height;
Debug.Log("** 屏幕 : " + width + ", " + height);
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector3 mousePos = Input.mousePosition;
Debug.Log("** 鼠标位置" + mousePos);
}
}
}
2. 一个物体,在屏幕上的位置:
其中所谓 屏幕,相对于摄像机而言的,实际上是指摄像头的屏
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
//int width = Screen.width;
//int height = Screen.height;
//Debug.Log("** 屏幕 : " + width + ", " + height);
Vector3 pos = this.transform.position;
Vector3 screenPos = Camera.main.WorldToScreenPoint(pos);
Debug.Log("** 物体的屏幕坐标:" + screenPos);
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector3 mousePos = Input.mousePosition;
Debug.Log("** 鼠标位置" + mousePos);
}
}
}
练习:物体运动时,检查是否超出屏幕的边界
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
//int width = Screen.width;
//int height = Screen.height;
//Debug.Log("** 屏幕 : " + width + ", " + height);
Vector3 pos = this.transform.position;
Vector3 screenPos = Camera.main.WorldToScreenPoint(pos);
Debug.Log("** 物体的屏幕坐标:" + screenPos);
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector3 mousePos = Input.mousePosition;
Debug.Log("** 鼠标位置" + mousePos);
}
Vector3 pos = this.transform.position;
Vector3 screenPos = Camera.main.WorldToScreenPoint(pos);
if(screenPos.x < 0 || screenPos.x > Screen.width)
{
Debug.Log("** 物体已出屏幕边界");
}
float speed = 4;
transform.Translate(speed*Time.deltaTime, 0, 0, Space.World);
}
}
(三)键盘输入
相关 API:
指令 | 解释 |
---|---|
Input.GetKeyDown( key ) | 按键事件,按下 |
Input.GetKeyUp( key ) | 按键事件,抬起 |
Input.GetKey( key ) | 按键状态,是否正被按下 |
演示:当按下 W 键时,向前运动
键值常量,可以参考官方文档:https://docs.unity.cn/cn/2022.3/ScriptReference/KeyCode.html
KeyCode.A:A
KeyCode.Space:空格
KeyCode.LeftArrow:向左的箭头键…
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FlyLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
float zSpeed = 0;
if(Input.GetKey(KeyCode.W))
{
// Debug.Log("* 按键 w 被按下");
zSpeed = 1;
}
this.transform.Translate(0, 0, zSpeed * Time.deltaTime, Space.Self);
}
}
注:运动游戏时,如果发现按键没有反应,应该先用鼠标
点一下 Game 窗口
,然后才能获得输入焦点
。
完