Unity C# 脚本学习笔记:游戏对象与组件访问

在 Unity 里写脚本,最常打交道的就是游戏对象(GameObject)和挂在它上面的组件(Component)。不管是控制角色移动、处理碰撞还是管理 UI,归根结底都是:找到某个物体,拿到它身上的某个组件,然后调用方法或修改属性。这篇笔记帮我梳理思路并作为笔记使用。

一、先认识 GameObject 和 Transform

每个场景里的东西,小到一个立方体,大到整个角色,都是 GameObject。但 GameObject 本身是个空壳,它的位置、旋转、缩放这些基础属性其实是由 Transform 组件提供的。所以操作一个物体,第一步往往就是拿到它的 Transform。

1. 在脚本中获取自己

我们写的脚本通常继承 MonoBehaviour,然后挂到某个物体上。在脚本内部,可以用 gameObjecttransform 直接访问“自己”。

using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        // 获取当前脚本所在的游戏物体
        GameObject self = gameObject;
        Debug.Log("物体名字:" + self.name);

        // 获取自己的 Transform
        Transform trans = transform;
        Debug.Log("当前位置:" + trans.position);
    }
}

这两个属性是 MonoBehaviour 自带的,不用手动赋值,非常方便。

2. 改位置、旋转、缩放

Transform 最常用的三个属性是 positionrotationlocalScale。注意区分世界坐标和本地坐标。

  • position:世界坐标,相对于整个场景的原点。
  • localPosition:本地坐标,相对于父物体的位置。
  • rotation:世界旋转(用四元数表示)。
  • localRotation:本地旋转。
  • localScale:本地缩放(通常世界缩放和本地缩放一样,除非父物体有缩放)。
// 把物体放到世界坐标 (0, 5, 0)
transform.position = new Vector3(0, 5, 0);

// 相对父物体移动
transform.localPosition = new Vector3(1, 0, 0);

// 旋转:Euler 角度转四元数
transform.rotation = Quaternion.Euler(0, 90, 0);

// 缩放
transform.localScale = new Vector3(2, 2, 2);

// 每帧向前移动(世界坐标系)
transform.Translate(Vector3.forward * Time.deltaTime);

// 每帧绕自己的 Y 轴旋转
transform.Rotate(Vector3.up, 90 * Time.deltaTime);

注意 Translate 默认在本地坐标系下移动,如果传 Space.World 就在世界坐标系下移动。Rotate 同理。

3. 父子关系

层级结构是 Unity 管理物体的重要方式。父物体动,子物体跟着动;子物体的本地坐标是相对于父物体的。

// 设置父物体
transform.SetParent(otherTransform);

// 设置父物体,但保持世界坐标不变(有时不希望坐标跟着父物体变)
transform.SetParent(otherTransform, true);

// 取消父物体
transform.SetParent(null);

// 查找子物体(按名字)
Transform child = transform.Find("Weapon");
if (child != null)
{
    Debug.Log("找到子物体:" + child.name);
}

// 遍历所有直接子物体
foreach (Transform t in transform)
{
    Debug.Log(t.name);
}

Find 只能找直接子物体,不能找孙物体。如果要找深层子物体,可以递归或者用路径名 "A/B/C"

二、组件的获取与添加

GameObject 的功能都是由组件拼起来的。比如 Rigidbody 负责物理,Collider 负责碰撞,Renderer 负责渲染。脚本要控制这些功能,就得先拿到对应组件。

1. GetComponent:最常用的方法

GetComponent<T>() 返回当前物体上指定类型的组件。如果没有这个组件,返回 null。

Rigidbody rb = GetComponent<Rigidbody>();
if (rb != null)
{
    rb.AddForce(Vector3.up * 10f, ForceMode.Impulse);
}

因为可能返回 null,所以使用前最好判断一下,避免空引用报错。

2. 获取子物体或父物体上的组件

有时候组件不在自己身上,而在子物体或父物体上,可以用:

  • GetComponentInChildren<T>():查找自身及所有后代物体中的第一个匹配组件。
  • GetComponentInParent<T>():查找自身及所有祖先物体中的第一个匹配组件。
// 获取子物体上的碰撞器(比如从角色身上找武器的碰撞盒)
Collider col = GetComponentInChildren<Collider>();

// 获取父物体上的玩家控制脚本
PlayerController controller = GetComponentInParent<PlayerController>();

注意这两个方法也会把自身包含进去,所以如果自身有该组件,优先返回自身的。

3. 获取多个组件

  • GetComponents<T>():返回当前物体上所有该类型的组件数组。
  • GetComponentsInChildren<T>()GetComponentsInParent<T>():同理,返回数组。
Collider[] colliders = GetComponents<Collider>();
foreach (Collider c in colliders)
{
    c.isTrigger = false;
}

4. 动态添加组件

运行时也可以给物体添加组件,用 AddComponent<T>()

// 添加一个刚体
Rigidbody rb = gameObject.AddComponent<Rigidbody>();
rb.mass = 2f;

// 添加自定义脚本
MyScript script = gameObject.AddComponent<MyScript>();

不过要注意,某些组件不能重复添加,比如一个物体只能有一个 Rigidbody;但自定义脚本可以添加多个。

三、查找场景中的其他物体

除了自己,脚本常常需要引用场景中的其他物体。Unity 提供了几种查找方法,但性能开销较大,只适合在初始化时用,不要每帧调用。

1. GameObject.Find(string name)

按名字查找,返回场景中第一个名字匹配的活动物体。性能很差,因为它遍历整个场景。

GameObject player = GameObject.Find("Player");
if (player != null)
{
    Debug.Log("找到玩家");
}

建议只用一次,然后缓存引用。

2. GameObject.FindWithTag(string tag)

按标签查找,需要提前在 Inspector 里给物体设置 Tag。性能比 Find 好一点,但仍不建议高频使用。

GameObject enemy = GameObject.FindWithTag("Enemy");

3. FindObjectOfType() / FindObjectsOfType()

查找场景中第一个或所有带有指定组件的物体。这两个方法在新版本 Unity 中逐渐被 Object.FindFirstObjectByType<T>()Object.FindObjectsByType<T>() 替代,但旧版依然可用。

// 找到第一个 PlayerController 组件
PlayerController playerCtrl = FindObjectOfType<PlayerController>();

// 找到所有 Enemy 组件
Enemy[] enemies = FindObjectsOfType<Enemy>();

同样,这些方法适合在 AwakeStart 中调用一次,然后保存结果,不要在 Update 中用。

四、实例化与销毁

运行时动态创建和销毁物体是游戏逻辑的基础,比如发射子弹、生成敌人、销毁特效等。

1. Instantiate 创建物体

Instantiate 可以克隆一个现有物体或预制体,并设置位置和旋转。

public GameObject bulletPrefab;   // 在 Inspector 拖入预制体
public Transform firePoint;

void Shoot()
{
    GameObject bullet = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
    // 可选:把新物体设为当前物体的子物体
    bullet.transform.SetParent(transform);
}

也可以只传一个物体,表示克隆该物体(包括其所有组件和子物体)。

2. Destroy 销毁物体

// 立即销毁当前物体
Destroy(gameObject);

// 延迟 3 秒销毁
Destroy(gameObject, 3f);

// 销毁组件(比如移除 Rigidbody)
Destroy(GetComponent<Rigidbody>());

注意:Destroy 是在当前帧结束后才真正销毁物体,所以调用后物体还会存在到帧末。

3. DontDestroyOnLoad 跨场景保留

默认加载新场景时,旧场景的物体会被销毁。如果想让某个物体一直存在(比如游戏管理器、背景音乐),可以在 Awake 中调用 DontDestroyOnLoad

void Awake()
{
    DontDestroyOnLoad(gameObject);
}

通常配合单例模式使用,避免重复创建。

五、预制体的动态使用

预制体(Prefab)是提前配置好的物体模板,在脚本中用 public GameObject 字段引用,然后 Instantiate 创建实例。

public class Spawner : MonoBehaviour
{
    public GameObject enemyPrefab;  // 在 Inspector 拖入预制体

    void Start()
    {
        // 每 2 秒生成一个敌人
        InvokeRepeating("SpawnEnemy", 1f, 2f);
    }

    void SpawnEnemy()
    {
        Vector3 pos = new Vector3(Random.Range(-5f, 5f), 0, Random.Range(-5f, 5f));
        Instantiate(enemyPrefab, pos, Quaternion.identity);
    }
}

脚本实例化的物体和场景中手动摆放的物体没有区别,修改实例不会影响预制体资源。如果要批量修改所有实例,应该修改预制体本身。

另外,实例化时传父物体可以让实例自动成为其子物体,并继承父物体的变换:

GameObject instance = Instantiate(enemyPrefab, transform);
instance.transform.localPosition = Vector3.zero;

六、一些性能建议

  • 缓存组件引用:在 AwakeStart 里调用 GetComponent 并保存到成员变量,不要在 Update 里反复调用。
  • 少用 Find 系列方法:这些方法遍历场景,开销大。最好在初始化时查找一次并缓存,或者通过 Inspector 拖拽引用、单例等方式传递。
  • 用对象池代替频繁 Instantiate/Destroy:对于子弹、特效等大量重复生成销毁的对象,频繁调用会产生 GC 压力,可以考虑对象池。

七、总结

这篇笔记把游戏对象和组件访问的常用操作都过了一遍:获取对象、修改 Transform、父子关系、获取和添加组件、查找场景对象、实例化和销毁、预制体使用。这些是 Unity 脚本最基础也最核心的部分,之后写任何功能都离不开它们。建议自己动手写几个小例子,熟悉每个 API 的用法和返回值,尤其是空引用的问题。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值