Unity3D入门教程 - 阿发

第 01 章 课程介绍

课程介绍

Unity ,一款 3D 游戏引擎。

常见的游戏引擎:

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

游戏引擎 VS 建模

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

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

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

资源下载

官网 https://afanihao.cn

第 02 章 开发环境

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

第 03 章 场景与视图

场景

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

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

游戏物体

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

3D 视图

了解 3D 视图的操作。

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

视图相关操作

平移:鼠标中键

头部旋转:鼠标右键

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

修改旋转中心:鼠标中键

缩放:鼠标滚轮

导航器相关操作

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

世界坐标系

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

image-20231115232145354

视野中心

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

透视与正交

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

第 04 章 游戏物体

3D 原生物体

Cube ,立方体

Sphere ,球体

Capsule ,胶囊体

Cylinder ,圆柱体

Plane ,平面

物体的移动

Move Tool ,移动工具

Rotate Tool ,旋转工具

Scale Tool ,缩放工具

旋转与缩放

Rotate Tool ,旋转工具

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

其中,

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

  • 可以在 Inspector 里精确指定

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

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

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

2 操作模式,保持默认

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

多选复制对齐

1 多选

  • 按 CTRL 或 SHIFT 键,点选

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

2 复制

  • 选中物体,CTRL + D 复制

  • 物体右键 Duplicate

3 聚焦

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

  • 或者 双击 物体

4 激活

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

5 对齐

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

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

练习

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

AF 扩展插件

AF 扩展插件 ,AfSimpleExtension.cs

相关功能:

  • 获取物体的尺寸

  • 置于视图中心,G键

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

第 05 章 模型

网格

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

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

材质

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

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

纹理

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

外部模型

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

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

更多细节

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

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

FBX 的使用

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

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

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

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

贴图,

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

材质,

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

FBX 的使用(二)

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

  • 选中*.fbx 资源文件

  • 在 Inspector 中切到 Materials 属性

    Use Embeded Materials

    On Demand Remap: 映射新的材质

  • Apply应用设置

FBX 的使用(三)

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

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

第 06 章 资源文件

资源文件

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

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

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

场景文件

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

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

资源包

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

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

Unity 资源商店

https://assetstore.unity.com/

第 07 章 父子关系

轴心

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

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

父子关系

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

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

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

空物体

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

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

空物体很常见,其作用:

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

要点与细节

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

Global 与 Local

  1. Global,即世界坐标系

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

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

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

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

Pivot 与 Center

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

第 08 章 组件

组件

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

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

添加删除组件

几个组件操作:

Add Component,添加一个组件

Remove Component,删除一个组件

练习:Mesh Filter 和 Mesh Renderer 组件。

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

AudioSource 组件

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

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

Transform 组件

Transform,称为变换组件。

物体的基本参数:

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

Transform 组件的特点:

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

摄像机

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

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

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

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

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

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

第 09 章 脚本

脚本

文件名即为类名 。如 SimpleLogic

当前物体

在 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;

物体的坐标

物体的坐标,

  • 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 结尾

播放模式

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

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

You must exit play mode to save the scene

第 10 章 帧更新

帧更新

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 的帧率更新游戏

移动物体

在 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 章 物体的运动

物体的运动

本章研究物体的运动。

  • 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

相对运动

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

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

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

要点与细节

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

运动的方向

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

  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,沿物体自身坐标系的轴向运动

小练习

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

向量测距:

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

第 12 章 物体的旋转

物体的旋转

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

  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;

相对旋转

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

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

相当于,

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

自转与公转

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

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

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

官方文档

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

第 13 章 脚本的运行

脚本的运行

场景的加载过程:

  1. 创建节点

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

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

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

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

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

消息函数

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

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

常见的消息函数:

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

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

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

脚本执行顺序

消息函数的调用顺序:
第 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,默认即可

主控脚本

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

1
Application.targetFrameRate = 60;

第 14 章 脚本的参数

脚本的参数

脚本的参数,用于控制脚本组件的功能。
修改 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;

参数的赋值

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

  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()

值类型

参数的类型,分为值类型、引用类型
值类型:如 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 类型

引用类型

参数也可以是引用类型。

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

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

运行时调试

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

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

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

第 15 章 鼠标输入

鼠标输入

鼠标输入 相关 API :

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

几个细节

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

鼠标事件,只触发一次

Input.GetMouseButtonDown()

Input.GetMouseButtonUp()

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

Input.GetMouseButton()

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

屏幕坐标

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);

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

键盘输入

获取键盘输入,相关 API :

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

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

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

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

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

KeyCode.A

KeyCode.Space

KeyCode.LeftArrow

第 16 章 组件的访问

组件的调用

组件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();
}
}

组件的参数

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

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

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

引用别的组件

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

第一种办法,

(1)添加一个变量,

1
public GameObject bgmNode;

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

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

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

第二种办法:(推荐)

直接添加一个变量,

1
public AudioSource bgm;

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

引用脚本组件

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

  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;
}

消息调用

不推荐使用这种方法

消息调用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

练习 简单飞控

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

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 章 物体的访问

获取物体

游戏物体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();
}

父子物体

场景中的层级关系/父子关系,是由 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
}

物体的操作

  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"),其中/表示在根下查找物体

练习 俄罗斯方块

练习,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 章 资源的访问

资源的使用

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

AudioClip,音频文件

Texture,纹理贴图

Material,材质 ……

演示:

  1. 准备音效文件,预览

  2. 添加脚本MusicTest.cs

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

  4. 使用 API 播放音频

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

资源数组

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

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

1
public AudioClip[] songs;

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

其中,

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

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

练习 三色球

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

就是用脚本修改 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 章 定时调用

定时调用

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

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

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

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

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

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

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

定时与线程

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

几点细节

  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);
}

练习 红绿灯

练习,做一个可以自动切换的红绿灯。
红灯,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;
}
}

练习 加速减速

按鼠标实现风扇加速减速

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 章 向量

向量

  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

向量运算

  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; ??

向量测距

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

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

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

向量的使用

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

1
2
3
4
5
6
public Vector3 speed;

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

第 21 章 预制体

预制体

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

演示:导出 RacingCar 资源包。。

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

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

预制体的创建

预制体的创建:

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

要点与细节

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

预制体的实例

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

特征:

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

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

预制体的编辑

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

第 1 种方式:单独编辑

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

第 2 种方式:原位编辑

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

第 3 种方式:覆盖编辑

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

要点与细节

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

多级节点

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

第 22 章 动态创建实例

动态创建实例

创建 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 视图中,观察创建出来的实例。

实例的初始化

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

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

要点与细节

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

实例的销毁

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

对于子弹来说,

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

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

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

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

要点与细节

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

练习 火控参数

练习 火控参数的完善

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);
}
}

练习 按键控制

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

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 章 物理系统

物理系统

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

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

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

物理碰撞

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

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

碰撞体的范围:

image-20231121073117251

反弹与摩擦

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

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

第 24 章 碰撞检测

运动学刚体

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

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

image-20231121073746148

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

碰撞检测

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

由 物理引擎 负责检测。

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 消息

碰撞体的编辑

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

其形状是可以编辑的,

  • Box Collider ,盒形

  • Sphere Collider ,球形

Box Collider ,盒形

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

  • Size 长宽高

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

Sphere Collider ,球形

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

  • Radius 半径大小

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

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

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

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

    此处,添加一个 Box Collider

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

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

练习 击毁目标

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

  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 章 游戏项目实例

射击游戏

制作一个射击游戏。

  • 海空背景

  • 玩家

  • 子弹,无限数量

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

  • 子弹特效,爆炸特效

  • 背景音乐

添加角色

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

天空盒

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

Window | Rendering | Lighting ,光照设置

Environment | Skybox Material ,天空盒材质

子弹和碰撞

添加子弹。

添加子弹脚本。

添加碰撞检测。

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

连续发射

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

  • 把子弹做成 prefab

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

  • 定义子弹目录 bullet folder

  • 使用定时器,子弹连发

按键控制

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

怪兽的走位

给怪兽添加控制脚本 EnemyLogic

怪兽生成器

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

子弹特效

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

爆炸特效

添加爆炸特效。粒子特效 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);

}
}