目录
前言
-
在前面的文章中,我简单写了一个在输入框写文字,点击按钮将文字显示在日志的功能:
-
虽然功能非常简单,但却涉及了 unity editor 界面中的诸多操作,以及脚本与 GameObject、组件 之间的配合。
-
如果不能很好地理解这些“元素”之间的关系,那么开发丰富的游戏内容就会变成一场痛苦的“死记硬背”。
-
因此本篇文章旨在介绍 unity 中,对象、组件、脚本 之间的关系,以便在后续的游戏开发中能够自己设计并实现预期功能。
题外话
得益于大语言模型的发展,我们可以借助 AI 来学习 unity,甚至写代码。
但是,如果没有打好基础、理解原理,我们就没法当好 AI 的 “老板”,保障产品的质量了。
因此,为了让 AI 成为一个 好助手、好员工,系统性地学习仍然十分有必要。
1. 前置知识
本文需要的前置知识主要包括:类、对象、派生、聚合、组合、UML 图、访问控制。
这里仅作简单介绍。
-
类
- 对一类事物的抽象。
- 举例:假设在一个简单场景里,我们需要计算顾客购买的苹果的花费,这里的苹果就是一个类,在这个场景里可以简单抽象出其属性:重量kg、单价 Yuan/kg。当然 重量是每个苹果都具有的属性,单价是某一类苹果公共的属性。具体的类如何设计还要视实际需要而定。
-
对象
- 实际的“物品”,也称:实例
- 举例:假设我们有了一个 “苹果” 类,它只是抽象的概念,在程序的世界里,我们可以用这个 类 来 实例化出一个对象,也就是一个真正的 “苹果”(在内存中的一段参与运算的数据)。
-
派生
- 描述的是 父类(也称基类)与子类(也称派生类)之间的关系,是 is-a 关系。
- 举例:界门纲目科属种 就是一套典型的类的派生,就好比 植物界 就是基类,某个具体种类的苹果树 就是最末端的 派生类。
-
聚合、组合
- 这是类之间的另外两种关系,是 has-a 关系
- 举例1:茶壶和杯子可以聚合成茶具套装,茶具套装与茶壶之间是整体与局部的关系。在这个关系中,脱离茶具套装,茶壶还是有意义的个体。
- 举例2:冰箱柜与冰箱门组合成冰箱,冰箱与冰箱门之间是整体与局部的关系。在这个关系中,脱离冰箱,冰箱门不再是一个有意义的个体。
-
UML 图
- UML 图是程序设计中的重要工具,可以用来表示程序中类之间的关系,比如前面介绍的 派生、聚合、组合,在 UML 图中都有各自的表示方法。
-
访问控制
- 访问控制是指,控制 类/对象 内部的 属性(成员变量)、方法(成员函数)可以被谁访问。
- private 修饰的,仅能被本类的对象访问;
- public 修饰的可以被所有对象访问;
- protected 修饰的可以被 本类以及子类对象 访问,不能被其他类型的对象访问
2. 游戏对象、组件、脚本 的关系 ★
2.1 要点
-
GameObject
GameObject 是用于承载各类功能的基础对象; -
GameObject & 组件
- 通过组装各种 组件 到 某个具体的 GameObject 上,可以实现具有复杂功能的游戏对象,这种关系是一种组合关系(组件必须依附在 GameObject 上);
- 一个 GameObject 下,同类的 组件 只能有一个;
-
子 GameObject
- GameObject 下面可以包含另一个 GameObject,一般称为子对象。不过这里并非“派生”,而是聚合关系;
- 子对象 的 位置(Transform组件中的属性),是相对于父对象的;
-
组件 的 派生
- 组件多种多样,以 Component 作为基类,派生出:MonoBehaviour (脚本基类)、Transform (位置、旋转、缩放)、Collider (碰撞检测)、Rigidbody (物理行为)、AudioSource (音频播放)、Animator (动画控制)、Canvas (UI管理) 等等 大量子类;
-
组件 & 脚本
脚本用来实现游戏对象的某些行为。通过 MonoBehaviour 组件,将脚本附加到 GameObject 上,可以说:脚本聚合到 MonoBehaviour 组件上,进一步聚合到 GameObject 中。
2.2 图解
下图是一个 GameObject、组件、脚本 关系的示例:
3. 以上一篇文章涉及的对象、组件、脚本为例,加深理解
3.1 GameObjects 结构
在 Hierarchy 界面 看到的是 GameObject 的结构
3.2 组件
- 在 Inspector 界面 看到的是组件
- 下图是 Button 这个 GameObject 中的前几个组件。
可以看到其中有一个名为 Button 的组件。
这个对象所包含的组件 是在创建这个对象时自动提供的“套装”。
- 下图是 Canvas 这个 GameObject 中的组件
与上面一个例子类似,创建 Canvas 对象的时候,已经带有了基础的几个组件(脚本组件是后来通过“拖拽”这个动作添加的)
3.3 脚本以及成员变量的显示
- 将脚本“拖拽”到 GameObject 上,会自动给对象添加脚本组件
- 在 Inspector 界面 显示成员变量
- 脚本中的类,其 公有成员变量 将在 Inspector 界面中显示
- [SerializeField]:用于将private或protected变量暴露到Inspector中。
- [HideInInspector]:用于隐藏public变量,使其不在Inspector中显示。
- 举例
- 代码
using UnityEngine;
using UnityEngine.UI;
using TMPro; // 引入TextMeshPro命名空间
public class InputHandler : MonoBehaviour
{
public TMP_InputField inputField; // 拖拽 InputField 到这个变量
public Button submitButton; // 拖拽 Button 到这个变量
public int testIntParam = 0;
[HideInInspector] public int testHideParam = 1;
private int testPrvParam = 2;
[SerializeField] private int testViewablePrvParam = 3;
void Start()
{
// 添加按钮点击事件
submitButton.onClick.AddListener(OnSubmit);
}
void OnSubmit()
{
// 获取输入框的内容
string inputText = inputField.text;
// 打印到 Debug 日志
Debug.Log("Input: " + inputText);
}
}
- Inspector 界面 效果
附录1 - 常见的组件以及作用
在Unity中,组件(Component)是附加到GameObject
上的功能模块,用于扩展其行为和属性。以下是一些常见的组件及其作用:
1. Transform
- 作用:定义
GameObject
的位置、旋转和缩放。 - 属性:
position
:对象在世界空间中的位置。rotation
:对象的旋转,通常用Quaternion
表示。scale
:对象的缩放。
- 使用场景:几乎所有需要位置、旋转和缩放的对象都需要
Transform
组件。
2. Rigidbody
- 作用:为
GameObject
提供物理行为,如运动、重力、碰撞等。 - 属性:
mass
:物体的质量。velocity
:物体的速度。drag
:物体的阻力。
- 使用场景:需要物理交互的对象,如角色、道具等。
3. Collider
- 作用:定义
GameObject
的碰撞检测。 - 类型:
BoxCollider
:立方体碰撞器。SphereCollider
:球体碰撞器。CapsuleCollider
:胶囊碰撞器。MeshCollider
:基于网格的碰撞器。
- 属性:
isTrigger
:是否作为触发器。
- 使用场景:需要碰撞检测的对象,如墙壁、地面、角色等。
4. Renderer
- 作用:定义
GameObject
的渲染属性,如材质、阴影等。 - 属性:
material
:对象的材质。enabled
:是否启用渲染。
- 使用场景:需要渲染的对象,如模型、UI元素等。
5. Animator
- 作用:控制
GameObject
的动画。 - 属性:
runtimeAnimatorController
:动画控制器。
- 使用场景:需要动画的对象,如角色、UI元素等。
6. AudioSource
- 作用:为
GameObject
提供音频播放功能。 - 属性:
clip
:音频剪辑。volume
:音量。loop
:是否循环播放。
- 使用场景:需要播放音频的对象,如背景音乐、音效等。
7. Canvas
- 作用:管理UI元素,是UI系统的根节点。
- 属性:
renderMode
:渲染模式(屏幕空间、世界空间等)。
- 使用场景:需要管理UI元素的场景。
8. RectTransform
- 作用:定义UI元素的布局和位置。
- 属性:
anchoredPosition
:锚点位置。sizeDelta
:大小偏移。
- 使用场景:需要布局的UI元素。
9. Button
- 作用:创建按钮,提供交互逻辑。
- 属性:
onClick
:点击事件。
- 使用场景:需要用户交互的UI元素,如菜单按钮。
10. InputField
- 作用:创建文本输入框,允许用户输入文本。
- 属性:
text
:输入的文本。placeholder
:占位符文本。
- 使用场景:需要用户输入的UI元素,如表单、对话框。
11. Image
- 作用:显示图像。
- 属性:
sprite
:图像资源。color
:图像颜色。
- 使用场景:需要显示图像的UI元素。
12. Text
- 作用:显示文本。
- 属性:
text
:显示的文本。fontSize
:字体大小。
- 使用场景:需要显示文本的UI元素。
13. ScriptableObject
- 作用:用于创建可持久化存储数据的脚本对象,通常用于配置和管理数据。
- 属性:
- 自定义属性,根据需要定义。
- 使用场景:需要管理配置数据的场景。
14. MonoBehaviour
- 作用:所有附加到
GameObject
的脚本的基类,提供生命周期方法(如Start
、Update
等)。 - 属性:
- 自定义属性,根据需要定义。
- 使用场景:需要实现游戏逻辑的
GameObject
。
总结
Transform
:定义位置、旋转和缩放。Rigidbody
:提供物理行为。Collider
:提供碰撞检测。Renderer
:定义渲染属性。Animator
:控制动画。AudioSource
:提供音频播放功能。Canvas
:管理UI元素。RectTransform
:定义UI元素的布局和位置。Button
:创建按钮,提供交互逻辑。InputField
:创建文本输入框,允许用户输入文本。Image
:显示图像。Text
:显示文本。ScriptableObject
:用于管理配置数据。MonoBehaviour
:提供生命周期方法。
通过合理使用这些组件,你可以实现复杂的游戏逻辑和功能。
附录2 - MonoBehaviour 常用接口
MonoBehaviour
是 Unity 中所有附加到 GameObject
的脚本的基类,它提供了一系列的生命周期方法和事件回调。这些方法和回调通常被称为“接口”,虽然它们在 C# 的严格意义上并不是接口(interface
),但它们的作用类似于接口,提供了特定的功能和行为。
以下是一些常用的 MonoBehaviour
生命周期方法及其作用:
1. Awake
- 作用:在脚本实例被加载时调用一次。
- 特点:
- 在
Start
之前调用。 - 即使脚本被禁用,
Awake
也会被调用。
- 在
- 使用场景:
- 初始化静态变量。
- 设置全局引用。
- 注册事件监听器。
- 示例:
void Awake() { Debug.Log("Awake is called when the script is loaded."); }
2. Start
- 作用:在脚本实例被激活后调用一次。
- 特点:
- 在第一次调用
Update
之前被调用。 - 如果脚本被禁用,则不会调用
Start
。
- 在第一次调用
- 使用场景:
- 初始化变量。
- 设置初始状态。
- 执行一次性的逻辑。
- 示例:
void Start() { Debug.Log("Start is called when the script is activated."); }
3. Update
- 作用:每帧调用一次。
- 特点:
- 在每一帧的渲染之前被调用。
- 适合处理需要每帧更新的逻辑。
- 使用场景:
- 处理用户输入(如键盘、鼠标、触摸等)。
- 更新游戏逻辑(如移动物体、碰撞检测等)。
- 动态更新 UI。
- 示例:
void Update() { if (Input.GetKeyDown(KeyCode.Space)) { Debug.Log("Space bar is pressed."); } }
4. FixedUpdate
- 作用:在固定的物理更新周期调用。
- 特点:
- 与物理系统同步,适合处理物理相关的逻辑。
- 调用频率与
Update
不同,通常用于处理物理碰撞、刚体运动等。
- 使用场景:
- 更新物理组件(如
Rigidbody
)。 - 处理物理碰撞。
- 更新物理组件(如
- 示例:
void FixedUpdate() { Debug.Log("FixedUpdate is called at a fixed interval."); }
5. LateUpdate
- 作用:在所有
Update
方法调用后调用。 - 特点:
- 每帧调用一次,但比
Update
稍晚。 - 适合处理需要在所有
Update
方法之后执行的逻辑。
- 每帧调用一次,但比
- 使用场景:
- 跟随相机或其他需要在所有更新后处理的对象。
- 示例:
void LateUpdate() { Debug.Log("LateUpdate is called after all Update methods."); }
6. OnEnable
- 作用:在脚本实例被激活时调用。
- 特点:
- 每次脚本被启用时都会调用。
- 如果脚本被禁用后再次启用,也会调用。
- 使用场景:
- 设置脚本被启用时的初始状态。
- 注册事件监听器。
- 示例:
void OnEnable() { Debug.Log("OnEnable is called when the script is enabled."); }
7. OnDisable
- 作用:在脚本实例被禁用时调用。
- 特点:
- 每次脚本被禁用时都会调用。
- 如果脚本被销毁,也会调用。
- 使用场景:
- 清理资源。
- 注销事件监听器。
- 示例:
void OnDisable() { Debug.Log("OnDisable is called when the script is disabled."); }
8. OnDestroy
- 作用:在脚本实例被销毁时调用。
- 特点:
- 只在脚本被销毁时调用一次。
- 使用场景:
- 清理资源。
- 释放内存。
- 示例:
void OnDestroy() { Debug.Log("OnDestroy is called when the script is destroyed."); }
9. OnApplicationQuit
- 作用:在应用程序退出时调用。
- 特点:
- 在 Unity 编辑器中,只有在播放模式下退出时才会调用。
- 在构建的游戏中,会在关闭游戏时调用。
- 使用场景:
- 保存数据。
- 清理资源。
- 示例:
void OnApplicationQuit() { Debug.Log("OnApplicationQuit is called when the application is about to quit."); }
10. OnTriggerEnter / OnTriggerExit / OnTriggerStay
- 作用:处理碰撞器的触发事件。
- 特点:
OnTriggerEnter
:当其他碰撞器进入触发器时调用。OnTriggerExit
:当其他碰撞器离开触发器时调用。OnTriggerStay
:当其他碰撞器在触发器内时每帧调用。
- 使用场景:
- 实现触发区域的逻辑(如进入区域时播放音效、显示提示等)。
- 示例:
void OnTriggerEnter(Collider other) { Debug.Log("Trigger entered by " + other.gameObject.name); } void OnTriggerExit(Collider other) { Debug.Log("Trigger exited by " + other.gameObject.name); } void OnTriggerStay(Collider other) { Debug.Log("Trigger stayed by " + other.gameObject.name); }
11. OnCollisionEnter / OnCollisionExit / OnCollisionStay
- 作用:处理碰撞事件。
- 特点:
OnCollisionEnter
:当发生碰撞时调用。OnCollisionExit
:当碰撞结束时调用。OnCollisionStay
:当碰撞持续时每帧调用。
- 使用场景:
- 实现碰撞逻辑(如反弹、播放音效等)。
- 示例:
void OnCollisionEnter(Collision collision) { Debug.Log("Collision detected with " + collision.gameObject.name); } void OnCollisionExit(Collision collision) { Debug.Log("Collision ended with " + collision.gameObject.name); } void OnCollisionStay(Collision collision) { Debug.Log("Collision staying with " + collision.gameObject.name); }
总结
MonoBehaviour
提供了一系列的生命周期方法和事件回调,这些方法在特定的时刻被 Unity 自动调用。通过合理使用这些方法,你可以实现复杂的游戏逻辑和行为。以下是一些常用的生命周期方法及其作用:
Awake
:初始化静态变量和全局引用。Start
:初始化变量和设置初始状态。Update
:每帧更新游戏逻辑。FixedUpdate
:处理物理更新。LateUpdate
:在所有Update
方法之后处理。OnEnable
:脚本被启用时调用。OnDisable
:脚本被禁用时调用。OnDestroy
:脚本被销毁时调用。OnApplicationQuit
:应用程序退出时调用。OnTriggerEnter/Exit/Stay
:处理触发器事件。OnCollisionEnter/Exit/Stay
:处理碰撞事件。
通过理解这些方法的作用和调用顺序,你可以更好地组织和管理你的脚本逻辑。