在 Unity 里写脚本,最常打交道的就是游戏对象(GameObject)和挂在它上面的组件(Component)。不管是控制角色移动、处理碰撞还是管理 UI,归根结底都是:找到某个物体,拿到它身上的某个组件,然后调用方法或修改属性。这篇笔记帮我梳理思路并作为笔记使用。
一、先认识 GameObject 和 Transform
每个场景里的东西,小到一个立方体,大到整个角色,都是 GameObject。但 GameObject 本身是个空壳,它的位置、旋转、缩放这些基础属性其实是由 Transform 组件提供的。所以操作一个物体,第一步往往就是拿到它的 Transform。
1. 在脚本中获取自己
我们写的脚本通常继承 MonoBehaviour,然后挂到某个物体上。在脚本内部,可以用 gameObject 和 transform 直接访问“自己”。
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 最常用的三个属性是 position、rotation 和 localScale。注意区分世界坐标和本地坐标。
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>();
同样,这些方法适合在 Awake 或 Start 中调用一次,然后保存结果,不要在 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;
六、一些性能建议
- 缓存组件引用:在
Awake或Start里调用GetComponent并保存到成员变量,不要在Update里反复调用。 - 少用 Find 系列方法:这些方法遍历场景,开销大。最好在初始化时查找一次并缓存,或者通过 Inspector 拖拽引用、单例等方式传递。
- 用对象池代替频繁 Instantiate/Destroy:对于子弹、特效等大量重复生成销毁的对象,频繁调用会产生 GC 压力,可以考虑对象池。
七、总结
这篇笔记把游戏对象和组件访问的常用操作都过了一遍:获取对象、修改 Transform、父子关系、获取和添加组件、查找场景对象、实例化和销毁、预制体使用。这些是 Unity 脚本最基础也最核心的部分,之后写任何功能都离不开它们。建议自己动手写几个小例子,熟悉每个 API 的用法和返回值,尤其是空引用的问题。

2467

被折叠的 条评论
为什么被折叠?



