Unity 脚本编程

脚本的使用

基础使用

在 Unity 中, 我们使用的脚本是 C# 语言. 首先, 我们在资源目录中创建一个脚本文件夹.

|139

随后, 可以创建一个 C# 文件, 这个文件名称按照类名进行书写即可, 因为一个脚本其实就是一个类. 这里直接创建一个 BasicLogic 的脚本, 作为基础的逻辑.

|375

创建后, 双击打开即可.

[!success] 注意
脚本编辑器我这里推荐使用 JetBrains 家的 Rider, 比 VS 好看, 也更加好用.

我们可以使用 DEBUG, 输出一个日志.

1
2
3
4
5
6
7
8
9
10
11
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasicLogic : MonoBehaviour
{
void Start()
{
Debug.Log("** 我的第一个脚本 **");
}
}

随后保存代码, 回到 Unity. 我们为了运行这个脚本, 需要将脚本进行挂载. 有两种挂载脚本的方式, 一个是在挂载的物体上, 添加一个 script.

|275

一个就是直接把脚本文件拖动到物体上, 更加简单一些. 随后, 运行游戏, 就可以看到控制台的输出了.

[!abstract] 注意

  1. 文件名和类名必须一致
  2. 最好使用大驼峰命名法

脚本的参数

脚本的参数其实就是脚本组件的参数. 我们可能给一个角色设置一下移动速度, 肯定不能直接在代码中写死了, 不然改起来非常的复杂.

所谓参数, 就是类似于下面这种, 可以直接修改的数据.

|325

我们的脚本也是可以有这种参数的. 随便创建一个脚本, 挂载到 gameObject 上后, 在类中提供一下 public 的属性, 就可以视作一个参数. 默认这里是啥都没有的:

|250

我们来到脚本中, 添加一些参数, 一定要是 public 的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ScriptParams : MonoBehaviour
{
// 可以添加一些脚本组件的参数 从外部进行设置
// 这里设置的是默认值
public float speed = 10f;
private void Start()
{
Debug.Log("speed 为 " + speed);
}
}

这里回到编辑器, 就可以看到输入框了.

|275

这里的名称和我们的变量名称对应, 如果是其他的单词, 会自动拆分. 建议使用小驼峰.


别的类型都是可以的, 比如下面这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ScriptParams : MonoBehaviour
{
public float speed = 10f;
public bool flag;
public int times;
public GameObject flagObject;
private void Start()
{
Debug.Log("speed 为 " + speed);
}
}

|300


另外, 我们也是可以给这些参数添加注释的, 方便我们查看参数的用途. 只需要在参数的上面使用如下语法即可添加注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ScriptParams : MonoBehaviour
{
[Tooltip("这是运动的速度")] public float speed = 10f;

private void Start()
{
Debug.Log("speed 为 " + speed);
}
}

回到 Unity, 鼠标移动到上面就有提示了.

|275

获取游戏内容

获取当前物体

我们使用脚本是用来操作物体的. 不妨查看一下一个物体的结构:

|325

其实一个物体的位置之类的东西, 都是这个 Transform 组件中的. 所以我们可以获取当前物体的这个组件进行修改即可.

来到脚本中, 我们知道: Start 方法是会自动调用的, 相当于进行了一个初始化的操作. 我们直接在 Start 中进行书写.

我们可以使用如下代码获取当前的物体:

1
GameObject obj = this.gameObject;

获取各种属性

一个物体有很多的属性, 比如名称. 可以直接获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasicLogic : MonoBehaviour
{
private void Start()
{
// 游戏物体的类型是 GameObject 获取脚本执行的gameObject即可.
GameObject obj = this.gameObject;

// 获取物体的名称
string name = obj.name;
Debug.Log("物体的名字为" + name);
}
}

|500


另外也可以获取物体的位置, 通过 transform 即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasicLogic : MonoBehaviour
{
private void Start()
{
// 游戏物体的类型是 GameObject 获取脚本执行的gameObject即可.
GameObject obj = this.gameObject;

// 获取物体的名称
string name = obj.name;
Debug.Log("物体的名字为 " + name);

// 也可以直接获取transform 类型就是transform
// 注意 这里不是 this.transform
Transform tr = obj.transform;

// 获取物体的位置
Vector3 pos = tr.position;
Debug.Log("物体的位置为 " + pos);
}
}

|225

其中的三个量就是对应的 x, y 和 z 坐标.

物体的坐标

坐标分类

物体的坐标有两种:

  • transform.position 世界坐标, 相对于整个坐标轴的坐标
  • transform.localPosition 本地坐标, 相对于父物体的坐标

例如现在有这样的两个物体:

我们对这个摄像头添加脚本, 查看坐标如何.

可以看到, 全局坐标减去本地坐标就是父物体的坐标了.

[!tip] 简化
其实 this.gameObject.transform.position 可以进行简化, 简化为 this.transform.position 即可.

设置坐标

获取坐标就是直接使用, 这里不多说. 对于设置坐标, 其实位置是一个三维向量 (float), 可以通过 x, y 和 z 来分别获取. 但是设置的时候, 只能通过直接设置 localPosition 之类的方式来进行设置. 不能直接修改 x, y, z.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasicLogic : MonoBehaviour
{
private void Start()
{
// 获取当前物体
GameObject obj = this.gameObject;
Debug.Log("初始位置为 " + obj.transform.position);

// 设置物体的位置
this.transform.position = new Vector3(0, 0, 0);
Debug.Log("新的位置为 " + obj.transform.position);
}
}

运动

帧更新

Frame, 就是一个游戏帧. 帧率就是每秒钟刷新多少次的意思. 我们在 Unity 中, 需要使用 Update 方法, 每帧会被游戏引擎自动调用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasicLogic : MonoBehaviour
{
// 帧更新
private void Update()
{
// 不妨直接输出Log试试
// 这里的time就是游戏时间, 游戏运行的时间
Debug.Log("Update 更新了, 当前游戏时间为 " + Time.time);
}
}

|350

可以看到每一帧都调用了该方法. 同时观察发现, 每次输出的时间差是不固定的. 这是因为 Unity 没有固定帧率, 但是它会尽可能提高帧率.

这个时间差也可以通过 Time.deltaTime 来获取.


无论如何, 我们可以给 Unity 一个近似的帧率, Unity 只会尽量的遵循我们的要求.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasicLogic : MonoBehaviour
{
private void Start()
{
// 设置默认帧率
Application.targetFrameRate = 60;
}

// 帧更新
private void Update()
{
// 不妨直接输出Log试试
// 这里的time就是游戏时间, 游戏运行的时间
Debug.Log("Update 更新了, 当前游戏时间为 " + Time.deltaTime);
}
}

根据计算, 如果是 60 帧, 那么大约是 16 毫秒左右进行更新.

|350

可以看到, 更新时间差大概就是 16 毫秒上下进行浮动.

移动物体

有了帧, 我们就可以让物体自己移动了. 首先获取当前的位置, 然后让物体的 x 进行移动试试:

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;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasicLogic : MonoBehaviour
{
private void Start()
{
Application.targetFrameRate = 60;
}

// 帧更新
private void Update()
{
// 获取物体的当前位置
Vector3 pos = this.transform.position;
// 移动一下
pos.x += 0.01f;
// 重新设定位置, 相当于移动了
this.transform.position = pos;
}
}

回到游戏, 运行就可以看到效果了.

匀速运动

其实物体的运动不是匀速的, 因为每次 Update 都是一个固定的距离, 但是我们的时间间隔是不固定的, 每次都不一样.

如果这是游戏, 我们肯定不希望帧数不同我们的运动速度就不同了, 这是不太合理的. 所以我们要解决这个问题.

通常来说, 我们通过 deltaTime 来计算出来我们的运动距离是多少. 我们只需要设置它的移动速度即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasicLogic : MonoBehaviour
{
// 设置移动速度
private int _moveSpeed = 3;
private void Start()
{
Application.targetFrameRate = 60;
}

// 帧更新
private void Update()
{
Vector3 pos = this.transform.position;
// 移动的距离 通过时间进行计算
pos.x += _moveSpeed * Time.deltaTime;
// 重新设定位置, 相当于移动了
this.transform.position = pos;
}
}

这样, 我们的物体移动就是匀速的了.

[!info] 这里的 speed 是什么
可以理解为移动的米数, 这里刚好就是格子的数目, 3 就是三米每秒.

Translate

刚才写的方法还是太麻烦了, 我们直接使用一个 API 来实现即可. 我们可以使用 transform.Translate(dx, dy, dz) 来设置物体在对应方向的坐标增量.

比如, 修改一下刚才的代码:

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;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasicLogic : MonoBehaviour
{
// 设置移动速度
private int speed = 3;
private void Start()
{
Application.targetFrameRate = 60;
}

// 帧更新
private void Update()
{
// 计算distance距离
float distance = speed * Time.deltaTime;
// 直接
this.transform.Translate(distance, 0, 0);
}
}

现在还是可以实现往 x 方向移动的效果.


如果希望往反方向移动, 只需要设置大小为 -distance 即可.

1
this.transform.Translate(-distance, 0, 0);

相对运动

我们的运动, 可以相对于世界坐标系, 也就是我们直接看到的 XYZ, 也可以相对于自己的坐标系. 这个自己的坐标系, 就是自己的方向为正向, 比如下面这样:

蓝色的 z, 并不是全局的 z, 这就是自己的坐标系. 我们的 Translate 中, 可以指定参考的坐标系是什么.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasicLogic : MonoBehaviour
{
// 设置移动速度
private int speed = 3;
private void Start()
{
Application.targetFrameRate = 60;
}

// 帧更新
private void Update()
{
float distance = speed * Time.deltaTime;
// 第三个参数 指定参考系
this.transform.Translate(0, 0, distance, Space.Self);
}
}

这里相对的是自身的, 那么就会按照自身的方向走.

其实默认的, 啥都不写就是参考自身的坐标系; 如果写了, 我们只需要写一个 Space.World, 参考世界坐标系, 就可以看到如下样子了.

1
this.transform.Translate(0, 0, distance, Space.World);

[!tip] 建模规范
其实我们建模的时候, 一般就是要求物体的朝向与 +Z 轴一致, 这样移动就是移动 Z 轴即可.

运动的方向

运动肯定不是漫无目的的, 而是有一个方向. 下面我构建这样一个场景, 有一个红色的物体, 我希望我的小方块往这个红色的东西运动.

基本思路其实很简单:

  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
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasicLogic : MonoBehaviour
{
public float speed = 3;
private void Start()
{
// 找到目标物体
GameObject flag = GameObject.Find("Flag");
// 转向目标 这里使用 LookAt 这个 API, 传入目标物体的 transform
this.transform.LookAt(flag.transform);
}

private void Update()
{
float distance = speed * Time.deltaTime;
// 直接移动z坐标即可
this.transform.Translate(0, 0, distance);
}
}

现在就实现了效果了.

如果我们物体存在重名, 则需要使用路径进行查找. 父物体/子物体这样子.

小练习

我们想要这个东西运动到目标后就停下来, 不走了, 应该怎么写呢?

只需要计算两个物体之间的距离, 只要距离足够小, 就不移动了即可. 这里需要用到向量的计算. 计算两个物体之间的距离, 需要两个坐标, 坐标一减就是向量, 取向量的模即可.

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;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasicLogic : MonoBehaviour
{
public float speed = 3;
public GameObject flagObject;
private void Start()
{
// 转向目标 这里使用 LookAt 这个 API, 传入目标物体的 transform
this.transform.LookAt(flagObject.transform);
}

private void Update()
{
// 移动距离
float distance = speed * Time.deltaTime;

// 获取距离
Vector3 pos1 = this.transform.position;
Vector3 pos2 = flagObject.transform.position;
Vector3 disVec = pos1 - pos2;
float dis = disVec.magnitude;

// 判断是否移动
if (dis >= 0.5)
{
this.transform.Translate(0, 0, distance, Space.Self);
}
}
}

现在已经会在附近停止了.

[!error] 注意
这里我提前使用了一个小知识点, 就是物体是可以从外部传入的. 只要是 public 的内容, 均可在 Unity 的组件面板处进行设置.

|325

旋转

什么是旋转

Unity 中, 物体的旋转是相对于轴心的. 也就是我们物体上的原点.

我们修改角度, 就会顺时针旋转对应的角度.

脚本控制

还是一样, 给这个物体添加一个新的脚本, 这里就叫做 RotateLogic. 我们之前位置修改的是 position, 但是 rotation 一般是内部修改的, 我们直接操作是不好的. 因为旋转有四个值.

代码中, 我们使用欧拉角来实现. 这个欧拉角就是我们看到的三个值了. 当然, 对应的也有相对于什么进行转动.

1
2
3
4
5
6
7
8
9
10
11
12
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RotateLogic : MonoBehaviour
{
void Start()
{
// 我们使用欧拉角来进行旋转的操作
this.transform.localEulerAngles = new Vector3(0, 45, 0);
}
}

运行游戏, 角度就变了.

|250

欧拉角, 360 是一个周期, 所以如果超出去了, 数值为 $2\pi + n$ 度, 则直接就是 $n$ 度.

旋转效果

其实就是在每次更新的时候, 都修改一下当前的欧拉角即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RotateLogic : MonoBehaviour
{
private void Update()
{
// 旋转
// 获取当前的旋转角
Vector3 angles = this.transform.localEulerAngles;
// 修改
angles.y += 0.5f;
// 设置
this.transform.localEulerAngles = angles;
}
}

|325

匀速转动

和运动的是一样的原理, 我们可以直接给一个旋转角速度, 然后旋转的角度是角速度乘以时间差即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RotateLogic : MonoBehaviour
{
// 设置角速度 这里的角速度代表一秒多少度
public float rotateSpeed = 30;
private void Update()
{
Vector3 angles = this.transform.localEulerAngles;
angles.y += rotateSpeed * Time.deltaTime;
this.transform.localEulerAngles = angles;
}
}

|250

Rotate

和运动一样, 我们之前的写法还是比较麻烦. 这里可以直接使用 Rotate 旋转相对角度, 分表代表 dx, dy, dz, 三个方向旋转的角度.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RotateLogic : MonoBehaviour
{
// 设置角速度 这里的角速度代表一秒多少度
public float rotateSpeed = 30;

private void Update()
{
this.transform.Rotate(rotateSpeed * Time.deltaTime, rotateSpeed * Time.deltaTime, rotateSpeed * Time.deltaTime);
}
}

这样旋转写起来就简单很多了.

|300

自转与公转

自转, 就是物体绕着自身的轴进行旋转; 公转就是绕着别的东西进行旋转. 为了实现公转, 其实就是父物体转动带动子物体, 那么视觉上就实现了一个公转的效果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RotateLogic : MonoBehaviour
{
// 设置角速度 这里的角速度代表一秒多少度
public float rotateSpeed = 60;

private void Update()
{
this.transform.Rotate(0, rotateSpeed * Time.deltaTime, 0);
}
}

注意, 这里转的是 y 轴.

|425

鼠标与键盘输入

我们玩游戏, 肯定需要使用鼠标和键盘, 我们自然需要获取键盘的输入, 否则无法与游戏进行交互的逻辑. 还是直接给一个小方块作为演示, 这里的脚本名称就叫做 KeyTest 了.

对于键盘鼠标的事件, 肯定是随时进行监听, 所以我们需要在 Update 里面实现. 首先实现鼠标的点击.

鼠标

我们通过 Input 来获取输入, 这里的是鼠标, 如果想仅仅在按下的瞬间执行, 可以使用如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class KeyTest : MonoBehaviour
{
void Update()
{
// 获取鼠标的点击
// 按下鼠标的 0 左键, 1 右键, 2 滚轮
if (Input.GetMouseButtonDown(0))
{
// 只有按下去的瞬间执行
Debug.Log("按下了鼠标左键");
}
}
}


如果持续的按下, 其实就是没有 Down 的方法. 每一帧都会调用这个方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class KeyTest : MonoBehaviour
{
void Update()
{
// 持续按下左键
if (Input.GetMouseButton(0))
{
this.transform.Translate(0, 0, 0.5f);
}
}
}

现在物体就会在按下鼠标的时候往前走了.


当然, 鼠标抬起也是可以有方法的, 自然就是 Up 了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class KeyTest : MonoBehaviour
{
void Update()
{
// 持续按下左键
if (Input.GetMouseButton(0))
{
this.transform.Translate(0, 0, 0.5f);
}
// 抬起鼠标左键
if (Input.GetMouseButtonUp(0))
{
this.transform.position = new Vector3(0, 0, 0);
}
}
}

这样就会在按下去的时候离开, 松开立马回来了.

键盘

对应鼠标, 也是三种情况, 按下, 抬起, 和按住键盘. 命名方式和鼠标的是一摸一样的. 不过鼠标是 Mouse, 键盘是 Key.

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class KeyTest : MonoBehaviour
{
void Update()
{
// 按下键盘的按键
if (Input.GetKey(KeyCode.D))
{
this.transform.Translate(0.1f, 0, 0);
}

if (Input.GetKey(KeyCode.A))
{
this.transform.Translate(-0.1f, 0, 0);
}

if (Input.GetKey(KeyCode.W))
{
this.transform.Translate(0, 0.1f, 0);
}

if (Input.GetKey(KeyCode.S))
{
this.transform.Translate(0, -0.1f, 0);
}
}
}

现在就实现了一个很简单的上下左右移动了.

官方文档

官方文档分为两个部分, Manual 和 API, 这是我们最最重要的东西, 几乎遇到问题, 都会需要查阅这些文档. 默认的, 安装的时候自带了一份英文文档:

我们一般也就看看 API 参考手册了. 因为一些类的方法之类的东西, 都会在文档中提及.