3、游戏对象与组件:GameObject概念、Transform组件、Mesh Renderer、添加/删除组件、组件生命周期

好,咱们今天聊点实在的。游戏对象和组件,说白了就是Unity世界的「积木」和「功能卡」。你想想看,一个空荡荡的游戏场景,总得放点东西进去吧?那个「东西」就是GameObject。而组件呢,就是给这个「东西」赋予各种能力的插件。

我个人习惯把GameObject想象成一个空盒子。盒子本身啥也干不了,但你往里面塞进不同的组件,它就能发光、能移动、能碰撞、能发出声音。嗯,就是这么个道理。

3.1 GameObject:一切的基础

在Unity里,场景中每一个可见或不可见的实体,都是GameObject。角色、灯光、摄像机、粒子特效……统统都是。你可以通过代码动态创建它,也可以在编辑器里直接拖拽。

创建一个空GameObject很简单:

// 创建一个名为"MyObject"的空游戏对象
GameObject myObj = new GameObject("MyObject");

我曾经在做一个AR项目时,需要动态生成上百个标记点。一开始我手动在编辑器里摆,后来发现用代码生成效率高得多。记住,GameObject本身只是一个容器,它的灵魂在于挂载的组件。

核心要点:GameObject = 空容器 + 组件列表。没有组件的GameObject,就是个「透明人」。

3.2 Transform组件:位置、旋转、缩放

每个GameObject都自带一个Transform组件。你删不掉它,也加不了第二个。为什么?因为Unity需要知道这个物体「在哪、面朝哪、多大」。

Transform有三个核心属性:

  • Position:世界坐标下的位置 (x, y, z)
  • Rotation:欧拉角旋转 (x, y, z)
  • Scale:缩放比例 (x, y, z)

代码里操作Transform非常频繁:

// 获取当前对象的Transform
Transform t = gameObject.transform;

// 移动位置
t.position = new Vector3(0, 5, 0);

// 绕Y轴旋转45度
t.Rotate(0, 45, 0);

// 设置缩放
t.localScale = new Vector3(2, 2, 2);

我记得有一次,团队里新来的同事直接修改了transform.position的x值,结果整个物体飞到了奇怪的地方。为什么?因为position返回的是结构体,不是引用。你改不了单个分量,必须整体赋值。这是个经典坑,我踩过。

小技巧:如果你只想修改位置的x值,可以这样写:transform.position = new Vector3(10, transform.position.y, transform.position.z);

3.3 Mesh Renderer:让物体「看得见」

光有Transform还不够,物体得有个「皮囊」吧?Mesh Renderer就是干这个的。它负责把网格数据(Mesh)和材质(Material)渲染到屏幕上。

一个完整的可见物体,通常需要三个组件配合:

组件 作用
Mesh Filter 指定使用哪个网格模型
Mesh Renderer 控制渲染方式、材质、阴影等
Material 定义表面颜色、纹理、光泽

你可以在代码里动态切换材质:

// 获取Mesh Renderer组件
MeshRenderer renderer = GetComponent<MeshRenderer>();

// 更换材质
renderer.material = Resources.Load<Material>("Materials/RedMat");

我曾经做过一个换装系统,就是靠动态替换Mesh Renderer的材质数组来实现的。说白了,就是给角色换「皮肤」。

注意:如果GameObject没有Mesh Filter和Mesh Renderer,它就是个「隐形人」。你可以在场景里选中它,但游戏运行时看不到。很多新手忘了加这两个组件,然后跑来问我「为什么我的物体不显示?」——嗯,先检查组件齐不齐。

3.4 添加/删除组件:动态控制能力

组件不一定要在编辑器里提前挂好。你可以在运行时动态添加或移除,这给了游戏极大的灵活性。

添加组件:

// 给当前对象添加一个Rigidbody(刚体)组件
Rigidbody rb = gameObject.AddComponent<Rigidbody>();

删除组件:

// 获取并销毁刚体组件
Rigidbody rb = GetComponent<Rigidbody>();
if (rb != null)
{
    Destroy(rb);
}

我个人习惯在制作道具系统时,用这种方式动态赋予物体物理特性。比如一个箱子,玩家捡起来时添加Rigidbody让它能掉落,放下时再移除——嗯,效果很自然。

但要注意,Transform组件不能删除。你试试看,Unity会直接报错。这是引擎的硬性规定。

避坑指南:我曾经在循环里频繁AddComponent和Destroy,结果发现内存碎片越来越多。后来改用对象池,提前创建好组件,需要时启用/禁用,性能提升了一大截。

3.5 组件生命周期:谁先执行?

组件不是挂上去就完事了。它有自己的一套「生老病死」流程。理解这个,你才能写出靠谱的逻辑。

核心生命周期方法按顺序如下:

  1. Awake():组件被加载时调用,无论脚本是否启用。适合做初始化,比如获取组件引用。
  2. OnEnable():组件变为启用状态时调用。每次启用都会触发。
  3. Start():第一次Update之前调用,且只调用一次。适合做依赖其他对象的初始化。
  4. Update():每帧调用。游戏主循环就在这里。
  5. OnDisable():组件变为禁用状态时调用。
  6. OnDestroy():组件被销毁时调用。

代码示例:

public class LifecycleDemo : MonoBehaviour
{
    private void Awake()
    {
        Debug.Log("Awake: 组件被加载");
    }

    private void OnEnable()
    {
        Debug.Log("OnEnable: 组件启用");
    }

    private void Start()
    {
        Debug.Log("Start: 第一次Update前执行");
    }

    private void Update()
    {
        Debug.Log("Update: 每帧执行");
    }

    private void OnDisable()
    {
        Debug.Log("OnDisable: 组件禁用");
    }

    private void OnDestroy()
    {
        Debug.Log("OnDestroy: 组件销毁");
    }
}

你想想看,如果两个脚本都用了Start,谁先执行?答案是:不保证顺序。除非你在Project Settings里调整执行顺序,或者用Awake做依赖注入。

我的习惯:Awake里只做「获取自身组件」的事,比如GetComponent<Rigidbody>()。Start里才做「需要其他对象已准备好」的事。这样能避免很多空引用错误。

好了,这一章的内容就这些。GameObject是骨架,组件是血肉,生命周期是心跳。把这三样搞明白,你就能在Unity世界里自由搭建了。下一章咱们聊聊输入系统和玩家控制——到时候你会看到,这些组件怎么跟玩家的操作互动起来。