Unity3D入门教程 - 阿发

第 01 章 课程介绍

1.1 课程介绍

Unity ,一款 3D 游戏引擎。

常见的游戏引擎:

  • Unity ( C# )
  • Unreal Engine ( C ++ )
  • Cocos Creator ( JavaScript )
  • Frostbite Engine、Source Engine、Cry Engine …

游戏引擎 VS 建模

游戏建模:创建游戏 模型 ,面向美术人员

游戏引擎:驱动 游戏逻辑 ,面向开发人员

CG 建模软件:3Dmax、Maya、Blender 、Cinema4D、ZBrush …

1.2 资源下载

官网 https://afanihao.cn

第 02 章 开发环境

需要许可证才能使用,直接添加一个免费的个人版许可证即可。

第 03 章 场景与视图

3.1 场景

Scene,即游戏中的一处 关卡 \ 场景

默认场景中,只有一个主摄像机平行光源

3.2 游戏物体

GameObject,游戏中的任何角色、道具、建筑

3.3 3D 视图

了解 3D 视图的操作。

  • 导航器 Gizmo,表示世界坐标的方向
  • 栅格 Grid,表示 XZ 坐标平面
  • 天空盒 Skybox,表示游戏世界的背景

视图相关操作

平移:鼠标中键

头部旋转:鼠标右键

旋转中心旋转:ALT + 鼠标左键

修改旋转中心:鼠标中键

缩放:鼠标滚轮

导航器相关操作

重置方向:Shift + 鼠标左键中心方块

3.4 世界坐标系

3D 游戏世界的坐标系(左手系)

image-20231115232145354

3.5 视野中心

框显所选
选中一个物体
F键,置于视图中心
此时旋转视图,似是绕着物体旋转。

3.6 透视与正交

透视视图Perspective, 近大远小
正交视图Orthographic, 又称等距视图Isometric
透视图下,物体‘近大远小’。正交视图下,物体的显示与距离无关

第 04 章 游戏物体

4.1 3D 原生物体

Cube ,立方体

Sphere ,球体

Capsule ,胶囊体

Cylinder ,圆柱体

Plane ,平面

4.2 物体的移动

Move Tool ,移动工具

Rotate Tool ,旋转工具

Scale Tool ,缩放工具

4.3 旋转与缩放

Rotate Tool ,旋转工具

演示:选中一个物体,绕 Z 轴旋转。

其中,

  • 逆时针为正,顺时针为负

  • 可以在 Inspector 里精确指定

  • 按住 CTRL 键时,角度增量为 15 度

    Edit | Grid and Snap Settings 栅格吸附设置

1 可以用快捷键 W 、E 、R 切换

2 操作模式,保持默认

  • Pivot 轴心 / Center 中心点
  • Global 世界坐标系 / Local 局部坐标系

4.4 多选复制对齐

1 多选

  • 按 CTRL 或 SHIFT 键,点选

  • 在 3D 视图中,鼠标 框选

2 复制

  • 选中物体,CTRL + D 复制

  • 物体右键 Duplicate

3 聚焦

  • 选中物体,按 F 键 ,置于视图中心

  • 或者 双击 物体

4 激活

  • Active 选中物体,在检查器中 激活 / 禁用 物体

5 对齐

  • 对初学者,直接 目视对齐 即可

  • 进入 正交顶视图 / 正交右视图 / 正交后视图 ,方便观察

4.5 练习

练习,使用基本体做一个简单造型。

4.6 AF 扩展插件

AF 扩展插件 ,AfSimpleExtension.cs

相关功能:

  • 获取物体的尺寸

  • 置于视图中心,G键

  • 切换到 正交顶视图,1键

第 05 章 模型

5.1 网格

网格Mesh,存储了模型的形状数据

  • 模型的形状,由若干个小面围合而成
  • 模型是中空的
  • Mesh 中包含了顶点坐标面的法向等数据

5.2 材质

材质Material,.定义了物体的表面细节

  • 颜色
  • 金属/非金属
  • 光滑/粗糙
  • 透明/半透明/不透明
  • 凹陷/凸起

5.3 纹理

纹理Texture,也称贴图,用一张图定义物体的表面颜色

5.4 外部模型

Unity 支持各种 CG 建模软件:3Dmax/Maya/Blender/Cinema4D/ZBrush,.
标准模型格式:FBX

在建模软件中,将模型导出为 FBX 格式即可。

5.5 更多细节

模型没有厚度,正面可见,背面透明

物体具有默认材质 Default-Material,否则显示为紫色

5.6 FBX 的使用

在一个 FBX 模型文件中,一般包含:

  • Mesh 网格,定义物体的形状
  • Material 材质,定义表面的光学特性
  • Texture 贴图,定义表面的像素颜色

FBX 第一种使用方式:
演示:

  • 选择*.fbx文件,直接拖到层级窗口
  • 观察节点图标
  • 选中节点,观察 Inspector 中的操作按钮

贴图,

  • 有的模型需要贴图文件
  • 贴图文件的路径是约定好的
  • 与 fbx 相同目录,或者同级Textures目录

材质,

  • 有的模型定义了多个材质
  • 在 Materials 属性中定义了一个数组

5.7 FBX 的使用(二)

FBX 第二种使用方式:材质替换
演示:材质的重映射 Remap

  • 选中*.fbx 资源文件

  • 在 Inspector 中切到 Materials 属性

    Use Embeded Materials

    On Demand Remap: 映射新的材质

  • Apply应用设置

5.7 FBX 的使用(三)

FBX 第三种使用方式:分解重组
演示:

  • -选择 fbx 中的 网格
  • -选择 bx 中的 材质,或者自定义一个材质
  • -如果存在贴图,则使用 x 配套的贴图文件

第 06 章 资源文件

6.1 资源文件

Assets目录下的文件,称为资源
常见类型:

  • 模型文件 Model(*.fbx)
  • 图片文件 Texture(*jpg/png psd/tif)
  • 音频文件 AudioClip(*.mp3/wav/aif)
  • 脚本文件 Script(*.cs)
  • 材质文件*.mat,场景文件*.unity。。

Meta,描述文件
每一个资源文件/文件夹,都对一个*.meta描述文件

6.2 场景文件

场景文件*unity,记录了场景中的节点数据
包含:

  • 场景全局设置
  • 节点
  • 节点下的组件
  • 每个组件的参数值

6.3 资源包

资源包Unity Package,即对 Assets 下的资源打包
演示:

  • -选择一个资源文件,或者一个目录
  • -右键,Export Package
  • -生成*.unitypackage 资源包

6.4 Unity 资源商店

https://assetstore.unity.com/

第 07 章 父子关系

7.1 轴心

轴心Pivot,指一个物体的操作基准点
演示:

  • 移动一个物体
  • 旋转一个物体
  • 对于基本体来说,轴心点默认位于几何中心

7.2 父子关系

父子级,指两个物体之间的关系。
在 Hierarchy 窗口中,

  • 拖动物体 B,到物体 A 下
  • 子物体会随着父物体一并移动
  • 删除父物体时,子物体一并删除

相对坐标:子物体的坐标,是相对于父物体的。
移动父物体时,观察子物体的坐标。。

7.3 空物体

空物体EmptyObject,即空对象、空节点。
演示:

  • 添加一个空物体
  • 空物体不可见,(没有网格)
  • 空物体也有坐标,可以移动

空物体很常见,其作用:

  • -用于节点的组织和管理
  • -用于标记一个位置

要点与细节

GameObject,其实是一个节点/容器
一般所谓的‘物体’,即有形状的东西,对应的 Mesh

7.4 Global 与 Local

  1. Global,即世界坐标系

    • -以世界中心为轴
    • -6 个方向代表:上下东西南北
  2. Local,即本地坐标系

    • -以物体自身为轴
    • -6 个方向代表:上下前后左右

y 轴称为up,z 轴称为forward,x 轴称为right

一般地,要求模型的正脸与 Z 轴方向一致

7.5 Pivot 与 Center

Pivot,轴心
Center,几何中心
一般来说,物体的轴心并不在几何中心处

第 08 章 组件

8.1 组件

组件Component,代表一个功能
例如,

  • Light,光源
  • Mesh Filter,网格过滤器
  • Mesh Renderer,网格渲染器。。

8.2 添加删除组件

几个组件操作:

Add Component,添加一个组件

Remove Component,删除一个组件

练习:Mesh Filter 和 Mesh Renderer 组件。

  • 添加一个空物体
  • 添加Mesh|Mesh Filter,加载网格数据
  • 添加Mesh|Mesh Renderer,渲染网格

8.3 AudioSource 组件

AudioSource组件,用于播放音乐/音效

  1. 添加一个音乐文件,*.mp3/wav/aiff
  2. 创建一个物体
    • Add Component, Audio | Audio Source
    • 将音乐文件拖到 AudioSource.AudioClip 属性
    • 在 3D 窗口上方,选 Toggle Audio On

8.4 Transform 组件

Transform,称为变换组件。

物体的基本参数:

  • Position,位置(相对坐标)
  • Rotation,旋转(欧拉角)
  • Scale,缩放

Transform 组件的特点:

  • 1 所有物体都有
  • 2 不能被删除

8.5 摄像机

摄像机Camera,负责拍摄游戏画面。
演示:

  • 1 调整窗口布局,Scene/Game并排显示
  • 2 选中摄像机,观察镜头范围
  • 3 摄像机的 Z 轴指向,即为拍摄方向
  • 4 拍到的画面,呈现在 Game 窗口中

Game 窗口中的画面,即为玩家最终所见画面

调整摄像机的角度,两种方法:

  • 1 手工移动、旋转摄像机,对准目标
  • 2 Align with View,与 3D 视图对齐
    • 先在 3D 视图里摆好角度,此为观察者视角
    • 然后选中 Main Camera,执行 Align with View

此时,摄像机视角与观察者视角完全相同

第 09 章 脚本

9.1 脚本

文件名即为类名 。如 SimpleLogic

9.2 当前物体

在 SimpleLogic.cs 中,获取当前物体

  1. this,当前脚本组件
  2. this.gameObject,当前物体
    this.gameObject.name,当前物体的名字
  3. this.gameObject.transform,当前物体下的 Transform 组件
    为了简化书写,也可写作this.transform,效果相同
1
2
3
4
5
6
GameObject obj = this.gameObject;

string name = obj.name;
Debug.Log("物体名字:" + name)

Transform tr = transform;

9.3 物体的坐标

物体的坐标,

  • transform.position,世界坐标
  • transform.localPosition,本地坐标

一般常使用的是 localPosition,与 Inspector 中的值一致

1
2
3
4
5
6
Transform tr = transform;
Vector3 pos = tr.position;
Debug.Log("世界坐标:" + pos.ToString("F3"));

Vector3 localPos = tr.localPosition;
Debug.Log("本地坐标:" + localPos.ToString("F3"));

Vectors3 类型,即三维向量,含 x y z 三个分量(float)
设置物体的坐标:

1
transform.localPosition = new Vector3(1.5f, 0, 2.0f);

其中,float 型的数值,在书写时应以 f 结尾

9.4 播放模式

Edit Mode,编辑模式
Play Mode,播放/运行模式
在播放模式下,

  • 实时显示场景中的物体和属性
  • 游戏中的状态不可保存

You must exit play mode to save the scene

第 10 章 帧更新

10.1 帧更新

Frame,一个游戏帧
FrameRate,帧率/刷新率
FPS,Frames Per Sencond,每秒更新多少帧

Update(),称为帧更新
此方法会被游戏引擎定时调用,以更新游戏的状态
演示:在 Update()中添加日志输出,在 Console 中观察。。

帧率观察:

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

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

1
2
3
4
Debug.Log("帧更新:" + Time.time);
Debug.Log("帧更新时间差:" + Time.deltaTime);
//帧数计算
Debug.Log("帧数:" + (1 / Time.deltaTime));

Unity 不支持固定帧率,但可以设定一个近似帧率

1
Application.targetFrameRate = 60;

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

10.2 移动物体

在 Update()中,移动物体的位置
例如,

1
2
3
Vector3 pos = transform.localPosition;
pos.X += 0.01f;
transform.localPosition = pos;

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

物体的运动并不是匀速的。
每次运动 0.01 米,但是间隔的 deltaTime 不固定
比如,
第 1 次,deltaTime=0.016 秒,运动 0.01 米
第 2 次,deltaTime=0.030 秒,运动 0.01 米。

匀速运动

使用deltaTime,让物体的匀速运动
例如,

1
2
float speed = 3;
float distance = speed * Time.deltaTime;

第 11 章 物体的运动

11.1 物体的运动

本章研究物体的运动。

  • 1 布置测试场景
  • 2 添加小火车
  • 3 添加脚本SimpleLogic,控制小火车的运动

修改 position/localPosition,可以让物体运动
例如,

1
2
3
Vector3 pos = transform.localPosition
pos.z += distance;
transform.localPosition = pos;

此时,小车向正北(+Z)方向运动

一般使用transform.Translate(),实现相对运动

1
transform.Translate(dx,dy,dz,.)

其中,dx,dy,dz 是坐标增量
例如,

1
transform.Translate(O, O, distance); //Z方向增加distance

11.2 相对运动

transform.Translate(),可以实现物体的运动

1
transform.Translate(dx, dy, dz, space);

其中,第 4 个参数:
Space.World,相对于世界坐标系
Space.Self,相当于自身坐标系(本地坐标系)

要点与细节

在建模时,要求物体的脸的朝向与物体+Z轴一致

11.3 运动的方向

运动的方向,使物体朝着目标方向运动
演示:添加一个红旗,作为目标。。

  1. 获取目标物体

    1
    GameObject flag = GameObject.Find("红旗");
  2. 转向目标

    1
    transform.LookAt(flag.transform);
  3. 向‘前’运动,forward,+Z 方向

    1
    transform.Translate(0, 0, dz, Space.Self);

其中,
GameObject.Find(name_or_path),根据名字/路径来查找物体
transform.LookAt(target),使物体的 Z 轴指向物体
Space.self,沿物体自身坐标系的轴向运动

11.4 小练习

练习:小火车朝向目标运动,到达目标后停止。

向量测距:

1
2
3
4
p1 = 火车.transform.position
p2 = 红旗.transform.position
p = p1 - p2
distance = p.magnitude

第 12 章 物体的旋转

12.1 物体的旋转

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

  1. Quaternion 四元组(x,y,z,w)

    1
    transform.rotation=…不便操作,官方不建议使用

    https://docs.unity3d.com/cn/2023.2/ScriptReference/Quaternion.html

  2. 欧拉角 Euler Angle

    1
    2
    transform.eulerAngles = new Vector3(0, 45, 0);
    transform.localEulerAngles = new Vector3(0, 45, 0);

在 Update()修改角度,持续旋转

1
2
3
Vector3 angles = transform.localEulerAngles;
angles.y += 0.5f;
transform.localEulerAngles = angles;

优化,使之匀速旋转,

1
2
3
float rotateSpeed = 30; //每秒转30度角

angles.y += rotateSpeed * Time.deltaTime; //0.5f;

12.2 相对旋转

Rotate(),旋转一个相对角度

1
transform.Rotate(dx, dy, dz, space);

相当于,

1
2
3
Vector3 angles = transform.localEulerAngles;
angles += new Vector3(dx, dy, dz);
transform.localEulerAngles = angles;

12.3 自转与公转

自转,绕着自身轴旋转。
公转,围绕另一个物体旋转。
当父物体转动时,带动子物体一并旋转。

1
2
3
4
5
// 找到父物体
Transform parent = transform.parent;

// 控制父物体旋转
parent.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.Self);

12.4 官方文档

https://docs.unity3d.com/cn/2023.2/ScriptReference/index.html

第 13 章 脚本的运行

13.1 脚本的运行

场景的加载过程:

  1. 创建节点

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

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

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

    • 初始化script1.Start()
    • 帧更新script1.Update()

注意,Unity 是一个纯面向对象的框架,对象由框架创建

13.2 消息函数

所有的脚本,一般应继承于MonoBehaviour
消息函数,或称事件函数,一些回调函数

https://docs.unity.cn/cn/current/ScriptReference/MonoBehaviour.html

常见的消息函数:

  • Awake,初始化,仅执行一次
  • Start,初始化,仅执行一次
  • Update,帧更新,每帧调用一次
  • OnEnable,每当组件启用时调用
  • OnDisable,每当组件禁用时调用

其中,
Awake先于 Start 调用
Awake总是调用,即使组件被禁用
Start,只执行一次,第一次启用时调用
对于初学者,使用 Start 来初始化即可。

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

13.3 脚本执行顺序

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

脚本的优先级 Script Execution Order
默认地,所有脚本的优先级为 0,无特定顺序

优先级的设定:

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

要点与细节

  1. 脚本的执行顺序,和 Hierarchy 中的层级顺序无关
  2. 一般地,并不需要显式设置 Execution Order,默认即可

13.4 主控脚本

主控脚本,即游戏的主控逻辑。
演示:添加一个脚本MainLogic,作为主控脚本。。

1
Application.targetFrameRate = 60;

第 14 章 脚本的参数

14.1 脚本的参数

脚本的参数,用于控制脚本组件的功能。
修改 RotateY.cs,添加一个参数。。

1
public float rotateSpeed=30f;

参数的用法:

  1. 参数必须为public,才可以在检查器中显示

  2. 参数的名称即变量名
    rotateSpeedRotate Speed

  3. 参数的默认值,即变量的默认值
    可以Reset菜单重置

  4. 参数的工具提示,可以用[Tooltip()]指定

    1
    [Tooltip("旋转角速度")]
1
2
[ Tooltip("这个是Y轴向的角速度") ]
public float rotateSpeed = 30f;

14.2 参数的赋值

脚本参数的赋值(以下按时间顺序)

  1. 定义默认值

    1
    public float rotateSpeed = 30f;
  2. 在检查器中赋值

    1
    script.rotateSpeed = 180f; //由Unity框架对参数赋值
  3. 在 Awake 中初始化在 Start 中初始化

  4. 在 Start 中初始化

伪代码表示:

1
2
3
4
RotateY script = new RotateY()
script.rotateSpeed = 180f
script.Awake()
script.Start()

14.3 值类型

参数的类型,分为值类型、引用类型
值类型:如 int, float, bool
值类型(struct):如 Vector3, Color
引用类型(class):如 GameObject, Transform, MeshRenderer…

值类型

值类型的特点:

  • 本身是一个值,可直接赋值

  • 若未赋值,则默认为 0

  • 不能为 null

    1
    public float speed = null ??

结构体 struct,也是值类型
所以,

  • 若未赋值,则各字段值为 0

  • 不能设为 null

    1
    Vector3 rotateSpeed = null ??

其实,在 C#里面,int, float 本质也是 struct 类型
string,原则上属于 class 类型

14.4 引用类型

参数也可以是引用类型。

  • -节点,GameObject
  • -组件,如 Transform、MeshRenderer、AudioSource
  • -资源,如 Material、Texture、AudioClip、…
  • -数组类型

练习:添加 TrainLogic,使小火车朝着目标运动
但是,有两个红旗目标。。

14.5 运行时调试

在游戏运行时,可以对物体/组件进行实时调试

显然,在运行模式下,所有参数不能保存到场景
保存参数的办法:

  • -在 Play Mode 下,组件Copy Component
  • -在 Edit Mode 下,组件Paste Component Values

第 15 章 鼠标输入

15.1 鼠标输入

鼠标输入 相关 API :

1
2
3
Input.GetMouseButtonDown()
Input.GetMouseButtonUp()
Input.GetMouseButton()

15.2 几个细节

1 区分 事件探测 VS 状态探测

鼠标事件,只触发一次

Input.GetMouseButtonDown()

Input.GetMouseButtonUp()

鼠标状态,表示当前是否正在被按下

Input.GetMouseButton()

2 鼠标事件是全局的,每个脚本 互不影响

15.3 屏幕坐标

Input.mousePosition

1
2
3
4
5
if(Input.GetMouseButtonDown(0))
{
Vector3 mousePos = Input.mousePosition;
Debug.Log("鼠标点击位置:" + mousePos);
}

世界坐标屏幕坐标转换

1
2
3
4
5
6
// 一个物体,在屏幕上的位置 :
worldPos = transform.position;
// screenPos 屏幕坐标
screenPos = Camera.main.WorldToScreenPoint(worldPos);

// 其中所谓 屏幕,相对于摄像机而言的,实际上是指摄像头的屏

15.4 键盘输入

获取键盘输入,相关 API :

Input.GetKeyDown(key) 按键事件,按下

Input.GetKeyUp(key) 按键事件, 抬起

Input.GetKey(key) 按键状态,是否正被按下

演示:当按下 W 键时,向前运动 。

键值常量,可参考官方文档:

KeyCode.A

KeyCode.Space

KeyCode.LeftArrow

第 16 章 组件的访问

16.1 组件的调用

组件Component,代表一个功能。
例如,AudioSource可用于播放音乐、音效。。
其中,Play on Awake表示自动播放

在代码中,也可以用 API 来使其播放音乐。

  1. 获取 AudioSource 组件

    1
    AudioSource audio = GetComponent<AudioSource>();
  2. 播放

    1
    audio.Play();

    其中,<>表示泛型,即获取<AudioSource>类型的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void Update()
{
if(Input.GetMouseButtonDown(0))
{
PlayMusic();
}
}

void PlayMusic()
{
// 获取组件
AudioSource audio = GetComponent<AudioSource>();

if( audio.isPlaying )
{
Debug.Log("* 停止播放");
audio.Stop();
}
else
{
Debug.Log("* 开始播放音乐 ..");
audio.Play();
}
}

16.2 组件的参数

组件的参数,也可以在代码中访问。
例如,
AudioClip,音频资源
Mute,是否静音
Loop,是否循环播放
Volume,音量

1
audio.mute = true; //设置静音

https://docs.unity3d.com/cn/2023.2/ScriptReference/AudioSource.html

16.3 引用别的组件

在主控脚本里面访问音频组件

第一种办法,

(1)添加一个变量,

1
public GameObject bgmNode;

然后在检查器里指定这个引用。

(2)访问节点下的组件,

1
AudioSource audio = bgmNode.getComponent<AudioSource>();

第二种办法:(推荐)

直接添加一个变量,

1
public AudioSource bgm;

然后在检查器里指定这个引用 。自动找到背景音乐节点下面的 AudioSource 组件

16.4 引用脚本组件

一个脚本里,访问另一个脚本组件。
和普通组件一样,

  1. API 获取

    1
    FanLogic fan = node.getComponent<FanLogic>();
  2. 直接引用

    1
    public FanLogic fan;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 目标组件,直接在检查里引用
public FanLogic fan;

void Update()
{
if(Input.GetMouseButtonDown(0))
{
DoWork();
}
}

void DoWork()
{
//FanLogic fan = fanNode.GetComponent<FanLogic>();
fan.rotateSpeed = 180;
}

16.5 消息调用

不推荐使用这种方法

消息调用SendMessage,以‘消息’的形式来调用另一个组件

1
2
3
4
//找到目标节点
public GameObject target;
//向目标节点发送‘消息’
target.SendMessage(methodName, value)

SendMessage的内部执行(反射机制):

  1. 找到 target 节点下的所有组件
  2. 在组件下寻找 methodName 这个函数
    • 若存在此函数,则调用它
    • 若不存在,则继续查找
    • 若最终无法匹配,则报错

https://docs.unity3d.com/cn/2023.2/ScriptReference/Component.SendMessage.html

16.6 练习 简单飞控

练习,添加无人机,控制起降。

MainLogic.cs

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 MainLogic : MonoBehaviour
{
public RotateLogic rotateLogic;
public FlyLogic flyLogic;

// Start is called before the first frame update
void Start()
{
Application.targetFrameRate = 60;
rotateLogic.DoRotate();
}

// Update is called once per frame
void Update()
{
// w 键,起飞, s 键 ,降落
if(Input.GetKeyDown(KeyCode.W))
{
flyLogic.Fly();
}
if (Input.GetKeyDown(KeyCode.S))
{
flyLogic.Land();
}
}
}

RotateLogic.cs

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

public class RotateLogic : MonoBehaviour
{
float m_rotateSpeed;

// Start is called before the first frame update
void Start()
{
}

// Update is called once per frame
void Update()
{
transform.Rotate(0, m_rotateSpeed * Time.deltaTime, 0, Space.Self);
}

public void DoRotate()
{
m_rotateSpeed = 360*3;
}
public void DoStop()
{
m_rotateSpeed = 0;
}
}

FlyLogic.cs

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 FlyLogic : MonoBehaviour
{
float m_speed = 0;

// Start is called before the first frame update
void Start()
{
}

// Update is called once per frame
void Update()
{
float height = transform.position.y;
float dy = m_speed * Time.deltaTime;

if( dy > 0 && height < 4 )
{
transform.Translate(0, dy, 0, Space.Self);
}
if ( dy < 0 && height > 0)
{
transform.Translate(0, dy, 0, Space.Self);
}
}

public void Fly()
{
m_speed = 1;
}

public void Land()
{
m_speed = -1;
}
}

第 17 章 物体的访问

17.1 获取物体

游戏物体GameObject,也可以叫节点

  1. 按 名称/路径 获取(不推荐),通过Find()查找,名字变了就找不到了

    1
    2
    3
    4
    5
    6
    7
    //若不重名,可以按名称获取
    GameObject node = GameObject.Find("旋翼");
    //最好指定全路径
    GameObject node = GameObject.Find("无人机/旋翼");

    RotateLogic rotateLogic = node.GetComponent<RotateLogic>();
    rotateLogic.DoRotate();
  2. 引用获取,添加一个变量,在检查器中引用

1
2
3
4
5
6
7
public GameObject wingNode;

void Start()
{
RotateLogic rotateLogic = wingNode.GetComponent<RotateLogic>();
rotateLogic.DoRotate();
}

17.2 父子物体

场景中的层级关系/父子关系,是由 Transform 维持的。

查看文档,找到 Transform 类的说明

https://docs.unity3d.com/cn/2023.2/ScriptReference/Transform.html

  1. 获取父级
1
2
3
4
//父级的Transform组件
Transform parent = transform.parent;
//父级的GameObject节点
GameObject parentNode = transform.parent.gameObject;
  1. 获取子级
  • foreach遍历
1
2
3
4
foreach (Transform child in transform)
{
Debug.Log("* 子物体: " + child.name); //child
}
  • GetChild(),按索引获取
1
2
Transform child = transform.GetChild(0); //第0个
Debug.Log("* 子物体: " + child.name); //child
  • transform.Find(),按名称查找子项,

    其中,二级子级应该制定路径,如 bb/cc

1
2
3
4
5
6
7
8
9
Transform child = transform.Find("bb/cc");
if(child != null)
{
Debug.Log("* 找到子物体: " + child.name); //child
}
else
{
Debug.Log("Not Found"); //child
}

17.3 物体的操作

  1. 设置新的父级
1
transform.SetParent(other);
  1. 设为一级节点
1
transform.SetParent(null);

其中,parent 为 null 表示一级节点(没有父级)

  1. gameObject.SetActive(false) 显示/隐藏
1
2
3
4
5
6
7
8
9
Transform child = transform.Find("aa");
if(child.gameObject.activeSelf)
{
child.gameObject.SetActive(false);
}
else
{
child.gameObject.SetActive(true);
}

要点与细节

transform.Find("/222"),其中/表示在根下查找物体

17.4 练习 俄罗斯方块

练习,3D 版的俄罗斯方块,按空格键切换形状。

PlayerLogic.cs

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

public class PlayerLogic : MonoBehaviour
{
int m_index = 0; // 表示显示的是哪一个形状

// Start is called before the first frame update
void Start()
{
}

// Update is called once per frame
void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
ChangeShape();
}

// 向前运动
float speed = 1;
transform.Translate(0, 0, speed * Time.deltaTime, Space.Self);
}

private void ChangeShape()
{
// 先把原来的形状,隐藏
Transform child = transform.GetChild(m_index);
child.gameObject.SetActive(false);

m_index += 1;
int count = transform.childCount;
if (m_index >= count)
m_index = 0;

// 显示新的形状
child = transform.GetChild(m_index);
child.gameObject.SetActive(true);
}
}

第 18 章 资源的访问

18.1 资源的使用

在脚本中,也可以引用一个资源。

AudioClip,音频文件

Texture,纹理贴图

Material,材质 ……

演示:

  1. 准备音效文件,预览

  2. 添加脚本MusicTest.cs

    • 添加变量public AudioClip audioSuccess;
  3. 引用音频资源

  4. 使用 API 播放音频

    • AudioSource.PlayOneShot(clip),用于播放音效

18.2 资源数组

在脚本中,也可以定义一个数组变量。

比如,一个音乐盒,存了多首歌曲。

1
public AudioClip[] songs;

练习,创建一个音乐盒,点鼠标随机切换

其中,

1
int index = Random.Range(min, max);

用于在[min,max)中随机抽取一个数,不含max

18.3 练习 三色球

练习,制作一个变换颜色的小球。

就是用脚本修改 Material 材质

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;

public class SimpleLogic : MonoBehaviour
{
public Material[] colors;

int m_index = 0;

// Start is called before the first frame update
void Start()
{
}

// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
ChangeColor();
}
}

private void ChangeColor()
{
m_index += 1;
if (m_index >= colors.Length)
m_index = 0;

Material selected = colors[m_index];

MeshRenderer rd = GetComponent<MeshRenderer>();
rd.material = selected;
}
}

第 19 章 定时调用

19.1 定时调用

定时调用Invoke*,即一般所谓的‘定时器’
继承自MonoBehaviour:

  • Invoke(func, delay),只调用一次

  • InvokeRepeating(func, delay, interval),循环调用

  • IsInvoking(func),是否正在调度中

  • CancelInvoke(func),取消调用、从调度队列中移除

https://docs.unity3d.com/cn/2023.2/ScriptReference/MonoBehaviour.html

其中,
func,函数名是一个字符串(反射机制)

19.2 定时与线程

InvokeRepeating 定时调用,并没有创建新的线程
Unity 引擎的核心是单线程的。

验证:

Start()Update()、以及定时调用,是在同一个线程。
获取当前线程号,

1
2
using System.Threading;
int threadld = Thread.CurrentThread.ManagedThreadld

https://docs.unity3d.com/cn/2023.2/Manual/ExecutionOrder.html

参考官方文档的说明。

  • 消息函数 Awake/Start/Update/OnEnable.
  • 定时调用 Invoke
  • 协程调用 Coroutine

19.3 几点细节

  1. 重复的调用
    每次InvokeRepeating,都会添加一个新的调度
    演示:连续调用两次InvokeRepeating

  2. 取消调用
    IsInvoking(func),判断 func 是否在 Invoke 队列
    CancelInvoke(func),取消 func 的 Invoke 调用
    CancelInvoke(),取消当前脚本的所有 Invoke 调用

1
2
3
4
if (!IsInvoking(func))
{
InvokeRepeating(func, delay, interval);
}

19.4 练习 红绿灯

练习,做一个可以自动切换的红绿灯。
红灯,4 秒
绿灯,4 秒
黄灯,1 秒

LightLogic.cs

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

public class LightLogic : MonoBehaviour
{
[Tooltip("红、绿、黄按顺序指定")]
public Material[] colors;

int m_index = 0; // 红灯开始

// Start is called before the first frame update
void Start()
{
ChangeColor();
}

// Update is called once per frame
void Update()
{
}

void ChangeColor()
{
// 当前材质
Material color = colors[m_index];
MeshRenderer renderer = GetComponent<MeshRenderer>();
renderer.material = color;
Debug.Log("* Change -> " + m_index + ", time=" + Time.time);

if (m_index == 0)
{
// 红 -> 绿,间隔3秒钟
Invoke("ChangeColor", 4);
}
else if (m_index == 1)
{
// 绿 -> 黄,间隔1秒钟
Invoke("ChangeColor", 4);
}
else if (m_index == 2)
{
// 黄 -> 红,间隔1秒钟
Invoke("ChangeColor", 1);
}

// 切换
m_index++;
if (m_index >= 3) m_index = 0;
}
}

19.5 练习 加速减速

按鼠标实现风扇加速减速

FanLogic.cs

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

public class FanLogic : MonoBehaviour
{
// 最大转速
public float maxRotateSpeed = 720;

float m_speed = 0; // 当前转速
bool m_speedUp = false; // true 加速 , false 减速

// Start is called before the first frame update
void Start()
{
InvokeRepeating("AdjustSpeed", 0.1f, 0.1f);
}

// Update is called once per frame
void Update()
{
// 点一下,加速。再点一下,减速。
if(Input.GetMouseButtonDown(0))
{
m_speedUp = !m_speedUp;
}

// 旋转
if(m_speed > 0)
{
transform.Rotate(0, m_speed * Time.deltaTime, 0, Space.Self);
}
}

// 速度调整
private void AdjustSpeed()
{
if(m_speedUp)
{
if (m_speed < maxRotateSpeed)
m_speed += 10;
}
else
{
m_speed -= 10;
if (m_speed < 0)
m_speed = 0;
}
}

}

第 20 章 向量

20.1 向量

  1. Vector3 三位向量

API: 向量的长度

1
2
Vector3 v = new Vector3(3, 0, 4);
float len = v.magnitude;
  1. 单位向量,即长度为 1 的向量。

例如,

1
2
Vector3 v1 = new Vector3(1, 0, 0);
Vector3 v2 = new Vector3(0.6f, 0.8f, 0);
  1. 标准化 Normalize

缩放一个向量,使其长度为 1, 例如,

(3, 4, 0) → (0.6, 0.8, 0)
(2, 2, 0) → (0.707, 0.707, 0)
(1, 0, 0) → (1, 0, 0)

API: 向量标准化

1
2
Vector3 v1 = new Vector3(2, 2, 0);
Vector3 v2 = v1.normalized;
  1. 几个常量

Vector3.zero 即(0, 0, 0)
Vector3.up 即(0, 1, 0)
Vector3.right 即(1, 0, 0)
Vector3.forward 即(0, 0, 1)

https://docs.unity3d.com/cn/2023.2/ScriptReference/Vector3.html

20.2 向量运算

  1. 加法

向量加法,即 x y z 三个分量分别相加
例如,

1
2
3
Vector3 a = new Vector3(1, 3, 0);
Vector3 b = new Vector3(4, 1, 3);
Vector3 c = a + b;

image-20231113183101275

  1. 减法

向量加法,即 x y z 三个分量分别相减
例如,

1
2
3
Vector3 m = new Vector3 (5, 4, 0);
Vector3 n = new Vector3(1, 3, 0);
Vector3 p = m - n;

image-20231113183251327

  1. 乘法

向量乘法,分为 3 种:

  • 标量乘法 b = a * 2

  • 点积 c = Vector3.Dot(a, b)

  • 差积 c = Vector3.Cross(a, b)

其中,只要求掌握标量乘法,即对每一个分量相乘

  1. 赋值

Vectors3 是值类型,可以直接赋值
例如,

1
2
Vector3 a = new Vector(1, 1, 0);
Vector3 b = a;

不能设为 null,

1
Vector3 speed = null; ??

20.3 向量测距

向量测距,用于求两物体间的距离
例如,

1
2
3
4
Vector3 p1 = transform.position; /自己位置
Vector3 p2 = target.transform.position; /目标位置
Vector3 direction = p2 - p1; /方向向量
float distance = direction.magnitude; /距离
1
Vector3.Distance(a, b) //也可以求距离

https://docs.unity3d.com/cn/2023.2/ScriptReference/Vector3.Distance.html

物体间的距离,确切的说是轴心点之间的距离
应检查确认物体的轴心点。

20.4 向量的使用

Vector3 可以直接作为脚本的参数

1
2
3
4
5
6
public Vector3 speed;

void Update()
{
transform.Translate(speed * Time.deltaTime, Space.Self);
}

第 21 章 预制体

21.1 预制体

预制体 Prefab,即预先制作好的物体
使用预制体,可以提高开发效率。

演示:导出 RacingCar 资源包。。

  • 在 Prefabs 目录下,是预制体资源,*.prefab
  • 用预制体来构造物体

使用预制体 Prefab,可以快速创建物体
在 Prefab 资源中,包含了的所有数据。

21.2 预制体的创建

预制体的创建:

  1. 先制作好一个样本节点
  2. 做好以后,直接拖到 Assets 窗口,则自动生成一个 *.prefab 资源
  3. 原始物体不再需要,可以删除。

要点与细节

在导出 prefab 资源时,应该将依赖文件一并导出。
prefab 只是记录了节点的信息。
prefab 文件中不包含材质、贴图数据,仅包含引用

21.3 预制体的实例

Prefab Instance, 由预制体创建得到的对象

特征:

  • 在 Hierarchy 中,节点图标不同
  • 在 Hierarchy 中,右键菜单 | Prefab
  • 在 Inspector 中,上下文工具 | Prefab

Prefab Instance 和原 Prefab 之间存在关联。
右键菜单 Prefab | Unpack,则解除关联,成为普通物体

21.4 预制体的编辑

*.prefab相当于是一个模板,可以再次编辑

第 1 种方式:单独编辑

  • 双击 Prefab,进入 单独编辑 模式
  • 编辑节点和组件
  • 退出,完成编辑

第 2 种方式:原位编辑

  • 选择 Prefab Instance
  • 在检查器中 Open
  • Context:显示:Normal / Gray / Hidden
  • 此时,仅选中的物体被编辑,其余物体是陪衬
  • 编辑节点
  • 退出,完成编辑

第 3 种方式:覆盖编辑

  • 选择 Prefab Instance
  • 直接在场景中编辑
  • 编辑完后
  • Overrides | Apply, 应用编辑
  • Overrides | Revert, 取消编辑

要点与细节

  1. 点 Unpack 之后,可断开与原始 Prefab 间的联系

21.5 多级节点

Prefab 中,多级节点 / 父子关系,也是常见的情况

第 22 章 动态创建实例

22.1 动态创建实例

创建 Prefab 之后,用 API 动态创建实例
API:

1
Object.Instantiate(original, parent)

https://docs.unity3d.com/cn/2023.2/ScriptReference/Object.Instantiate.html

演示:

  1. 准备子弹 prefab,参考制作演示

  2. 添加火控脚本 FireLogic

    • 添加变量 public GameObject bulletPrefab;

    • 克隆实例

      1
      2
      3
      GameObject node = Instantiate(bulletPrefab, null);
      node.transform.position = Vector3.zero;
      node.transform.localEulerAngles = Vector3.zero;
  3. 在检查器中,引用子弹的 prefab

  4. 运行游戏

在 3D 视图中,观察创建出来的实例。

22.2 实例的初始化

创建 Prefab Instance 之后,应做初始化:

  • parent, 父节点:子弹树 Bullut Folder
  • position / localPosition, 位置:fire point
  • eulerAngles / localEulerAngles, 旋转:与炮管一致
  • Script, 自带的控制脚本

要点与细节

(1)一般引用 Transform,而 GameObject 是没有存在感的
(2)可以使用空物体,标记一个空间坐标

22.3 实例的销毁

一般的,创建实例之后,也要负责销毁。

对于子弹来说,

  • 当飞出屏幕时,销毁
  • 按 射程 / 飞行时间
  • 当击中目标时,销毁

Object.Destroy(obj), 用于销毁一个实例

在本轮帧更新Update()之后,执行销毁

https://docs.unity3d.com/cn/2023.2/ScriptReference/Object.Destroy.html

要点与细节

  1. 区分以下两种写法:
    Destroy(this.gameObject)
    Destroy(this) 错误
  2. Destroy()不会立即执行,而是在本轮 Update 之后才执行

22.4 练习 火控参数

练习 火控参数的完善

MainLogic.cs

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 MainLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Application.targetFrameRate = 60;
}

// Update is called once per frame
void Update()
{
}
}

FireLogic.cs

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

public class FireLogic : MonoBehaviour
{
// 对子弹 prefab 资源的引用
public GameObject bulletPrefab;

[Tooltip("子弹节点的管理")]
public Transform bulletFolder;

[Tooltip("子弹出生点")]
public Transform firePoint;

[Tooltip("炮塔")]
public Transform cannon;

[Tooltip("子弹飞行速度")]
public float bulletSpeed;

[Tooltip("子弹飞行时长")]
public float bulletLifetime;

// Start is called before the first frame update
void Start()
{
}

// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
TestFire();
}
}

private void TestFire()
{
Debug.Log("* 创建子弹的实例 ..");
GameObject node = Object.Instantiate(bulletPrefab, bulletFolder); // 指定父节点
node.transform.position = this.firePoint.position; // 指定出生点
node.transform.eulerAngles = this.cannon.eulerAngles; // 与炮塔角度一致

// node.transform.rotation = this.cannon.rotation; // 这样写也可以,指定初始角度

// 指定初始飞行速度
BulletLogic script = node.GetComponent<BulletLogic>();
script.speed = this.bulletSpeed; // 0.5f;
script.maxDistance = script.speed * this.bulletLifetime; // 最大最行距离
}
}

BulletLogic.cs

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;

public class BulletLogic : MonoBehaviour
{
public float speed;

public float maxDistance;

// Start is called before the first frame update
void Start()
{
float lifetime = 1;
if(speed > 0)
{
lifetime = maxDistance / speed;
}

// 在 lifetime之后,调用 SelfDestroy 自毁
Invoke("SelfDestroy", lifetime);
}

// Update is called once per frame
void Update()
{
this.transform.Translate(0, 0, speed, Space.Self);
}

private void SelfDestroy()
{
Debug.Log("* 自毁 !");
// 销毁实例
Object.Destroy(this.gameObject);
}
}

22.5 练习 按键控制

练习,添加按键控制,旋转炮塔的方向。

FireLogic.cs

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FireLogic : MonoBehaviour
{
// 对子弹 prefab 资源的引用
public GameObject bulletPrefab;

//
[Tooltip("子弹节点的管理")]
public Transform bulletFolder;

[Tooltip("子弹出生点")]
public Transform firePoint;

[Tooltip("炮塔")]
public Transform cannon;

[Tooltip("子弹飞行速度")]
public float bulletSpeed;

[Tooltip("子弹飞行时长")]
public float bulletLifetime;

[Tooltip("子弹发射间隔")]
public float bulletInterval;

[Tooltip("炮塔转速")]
public float rotateSpeed;

// 当前转角
private Vector3 m_eulerAngles;

// Start is called before the first frame update
void Start()
{
StartFire();
}

// Update is called once per frame
void Update()
{
//if(Input.GetMouseButtonDown(0))
//{
// TestFire();
//}

float delta = rotateSpeed * Time.deltaTime;

if (Input.GetKey(KeyCode.A))
{
// 左转
if (m_eulerAngles.y > -75)
m_eulerAngles.y -= delta;
}
if (Input.GetKey(KeyCode.D))
{
// 右转
if (m_eulerAngles.y < 75)
m_eulerAngles.y += delta;
}
if (Input.GetKey(KeyCode.W))
{
// 上转
if (m_eulerAngles.x > -60)
m_eulerAngles.x -= delta;
}
if (Input.GetKey(KeyCode.S))
{
// 下转
if (m_eulerAngles.x < 10)
m_eulerAngles.x += delta;
}

// 多维度旋转时,不宜使用 Rotate
cannon.transform.localEulerAngles = m_eulerAngles;
}

private void TestFire()
{
//Debug.Log("* 创建子弹的实例 ..");
GameObject node = Instantiate(bulletPrefab, bulletFolder); // 指定父节点
node.transform.position = firePoint.position; // 指定出生点
node.transform.eulerAngles = cannon.eulerAngles; // 与炮塔角度一致

// node.transform.rotation = this.cannon.rotation; // 这样写也可以,指定初始角度

// 指定初始飞行速度
BulletLogic script = node.GetComponent<BulletLogic>();
script.speed = bulletSpeed; // 0.5f;
script.maxDistance = script.speed * bulletLifetime; // 最大最行距离
}

// 开火
public void StartFire()
{
if (! IsInvoking("TestFire") )
{
InvokeRepeating("TestFire", bulletInterval, bulletInterval);
}
}

public void StopFire()
{
CancelInvoke("TestFire");
}
}

第 23 章 物理系统

23.1 物理系统

物理系统 Physics,即由物理规律起作用的系统
确切地说,是 牛顿运动定律 (力,质量,速度)

刚体组件 Rigidbody,物理学中的物体

  1. 给‘苹果’添加 Rigidbody 组件
    Physics | Rigidbody
  2. 运行游戏
    此时,牛顿接管了这个物体,在重力作用下使其运动

23.2 物理碰撞

物理系统,不仅接管了刚体的运动,也接管了碰撞
演示:添加一个 小球 物体,添加 Rigidbody。

碰撞体 Collider,描述了物体的碰撞范围。
其中,
Box Collider,长方碰撞体
Sphere Collider,球形碰撞体

碰撞体的范围:

image-20231121073117251

23.3 反弹与摩擦

刚体的反弹与摩擦,也归物理系统负责。
演示:

  • 新建 Physici Material,添加给小球的 Collider
  • 设置 FrictionBounciness
  • 观察小球的反弹。

第 24 章 碰撞检测

24.1 运动学刚体

运动学刚体 Kinematic ,即质量为 0 的刚体

由于质量为 0,所以此刚体不受牛顿约束。

image-20231121073746148

此时,需要用脚本使其运动。
演示:添加 SimpleLogic 脚本,让小球运动。

24.2 碰撞检测

对于运动学刚体,也支持碰撞检测。

由 物理引擎 负责检测。

image-20231121074236048

演示:

  1. Rigidbody ☑ Is Kinematic
  2. Collider ☑ Is Trigger 触发器
  3. 挂一个脚本,添加消息函数
1
2
3
void OnTriggerEnter(Collider other)
{
}

其中,

Collider other ,表示对方的碰撞体

other.gameObject ,对方节点

other.name ,对方节点的名字

要点与细节

  1. 物理引擎只负责探测 ( Trigger ) ,不会阻止物体或者反弹
  2. 物体引擎计算的是 Collider 之间的碰撞,和物体自身形状无关
  3. 当检测到碰撞时,会调用当前节点 的脚本中的 OnTriggerEnter 消息

24.3 碰撞体的编辑

碰撞体 Collider 的形状,规定了碰撞的边界。

其形状是可以编辑的,

  • Box Collider ,盒形

  • Sphere Collider ,球形

Box Collider ,盒形

  • Center 中心位置,相当于物体的轴心点

  • Size 长宽高

点 Edit Collider ,可以直接编辑 绿色框。

Sphere Collider ,球形

  • Center 中心位置,相当于物体的轴心点

  • Radius 半径大小

点 Edit Collider ,可以直接编辑 绿色框。

练习:添加子弹物体 。。。

  1. 检查原先有没有碰撞体,如果有,则先移除

  2. 根据体型,选择合适形状的碰撞体

    此处,添加一个 Box Collider

  3. 编辑碰撞体,调整边界

    一般无需调整,自动创建合适的尺寸。

24.4 练习 击毁目标

练习,发射一发子弹,击毁目标。

  1. 运动学刚体 Rigidbody / Is Kinematic

  2. 触发器模式 Collider / Is Trigger

  3. 消息函数 void OnTriggerEnter()

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 BulletLogic : MonoBehaviour
{
public Vector3 speed;

// Start is called before the first frame update
void Start()
{
}

// Update is called once per frame
void Update()
{
transform.Translate(speed * Time.deltaTime, Space.Self);
}

private void OnTriggerEnter(Collider other)
{
if( other.name.Equals("目标"))
{
Debug.Log("* 子弹发生碰撞,other=" + other.name);
Destroy(other.gameObject);
Destroy(gameObject);
}
}
}

要点与细节

碰撞的双方,只需一方设置为运动学刚体即可

第 25 章 游戏项目实例

25.1 射击游戏

制作一个射击游戏。

  • 海空背景

  • 玩家

  • 子弹,无限数量

  • 怪兽,蛇皮走位,无限数量

  • 子弹特效,爆炸特效

  • 背景音乐

25.2 添加角色

添加两个角色。玩家,敌人。

25.3 天空盒

天空盒 Skybox ,即游戏的背景。

Window | Rendering | Lighting ,光照设置

Environment | Skybox Material ,天空盒材质

25.4 子弹和碰撞

添加子弹。

添加子弹脚本。

添加碰撞检测。

注意,怪兽是一个空物体,其碰撞体要手工编辑

25.5 连续发射

  1. 子弹
  • 增加自毁时间 lifetime

  • 把子弹做成 prefab

  1. 玩家
  • 定义发射点 fire point

  • 定义子弹目录 bullet folder

  • 使用定时器,子弹连发

25.6 按键控制

添加按键控制,让玩家左右移动。

25.7 怪兽的走位

给怪兽添加控制脚本 EnemyLogic

25.8 怪兽生成器

添加怪兽生成器,定时生成怪兽。

25.9 子弹特效

添加子弹特效。粒子特效 Particle System
注意,修改之后要应用 Prefab

25.10 爆炸特效

添加爆炸特效。粒子特效 Particle System
在击中目标时,创建特效节点。

MainLogic.cs

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

public class MainLogic : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Application.targetFrameRate = 60;

}

// Update is called once per frame
void Update()
{

}
}

BulletLogic.cs

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

public class BulletLogic : MonoBehaviour
{
[Tooltip("子弹飞行速度")]
public float speed = 1;

[Tooltip("子弹生命时长")]
public float lifetime = 3;

[Tooltip("爆炸的粒子特效的prefab")]
public GameObject explosionEffect;

// Start is called before the first frame update
void Start()
{
Invoke("SelfDestroy", lifetime);
}

// Update is called once per frame
void Update()
{
transform.Translate(0, 0, speed * Time.deltaTime, Space.Self);
}

private void OnTriggerEnter(Collider other)
{
Debug.Log("* 子弹碰撞, other=" + other.name);

if (!other.name.StartsWith("怪兽")) return;

// 销毁子弹
Destroy(this.gameObject);

// 销毁对方节点
Destroy(other.gameObject);

// 创建一个粒子特效,表现爆炸效果
GameObject effectNode = Instantiate(explosionEffect, null);
effectNode.transform.position = this.transform.position;
// 当粒子特效播放时,该节点会自毁
}

private void SelfDestroy()
{
Destroy(this.gameObject);
}

}

PlayerLogic.cs

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

public class PlayerLogic : MonoBehaviour
{
[Tooltip("子弹节点的预制体")]
public GameObject bulletPrefab;

[Tooltip("子弹节点的父节点")]
public Transform bulletFolder;

[Tooltip("子弹出生点")]
public Transform firePoint;

[Tooltip("开火间隔")]
public float fireInterval = 0.1f;

[Tooltip("平移速度")]
public float moveSpeed = 0.1f;

// Start is called before the first frame update
void Start()
{
InvokeRepeating("Fire", fireInterval, fireInterval);
}

// Update is called once per frame
void Update()
{
float dx = 0;

if (Input.GetKey(KeyCode.A))
{
dx = -moveSpeed;
}
if (Input.GetKey(KeyCode.D))
{
dx = moveSpeed;
}
this.transform.Translate(dx, 0, 0, Space.Self);
}

private void Fire()
{
// 实例化一个子弹节点
GameObject node = Instantiate(bulletPrefab, bulletFolder);

// 把子弹移动到出生点位置
node.transform.position = firePoint.position;
}

}

EnemyLogic.cs

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

public class EnemyLogic : MonoBehaviour
{
[Tooltip("前进速度")]
public float zSpeed = 10;

// 横移速度
float xSpeed = 0;

// Start is called before the first frame update
void Start()
{
// 每秒改变一次横移速度
InvokeRepeating("SnakeMove", 1f, 1f);
}

// Update is called once per frame
void Update()
{
float dz = zSpeed * Time.deltaTime;
float dx = xSpeed * Time.deltaTime;

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

// 蛇皮走位
void SnakeMove()
{
// 4 种速度选项
float[] options = { -10, -5, 5, 10 };

int sel = Random.Range(0, options.Length);

xSpeed = options[sel];
}
}

EnemyCreator.cs

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

public class EnemyCreator : MonoBehaviour
{
[Tooltip("怪兽的 Prefab")]
public GameObject enemyPrefab;

[Tooltip("定时创建新的怪兽")]
public float interval = 1;

// Start is called before the first frame update
void Start()
{
// CreateEnemy();
InvokeRepeating("CreateEnemy", 0.1f, interval);
}

// Update is called once per frame
void Update()
{

}

private void CreateEnemy()
{
GameObject node = Instantiate(enemyPrefab, this.transform);
node.transform.position = this.transform.position;

// 头转过来
node.transform.localEulerAngles = new Vector3(0, 180, 0);

// 添加一个随机的偏移量,避免被人堵住出生点
float dx = Random.Range(-20, 20);
node.transform.Translate(dx, 0, 0, Space.Self);

}
}