脚本基础
Unity 使用 C# 作为主要的脚本语言。本章将介绍 Unity 脚本的基础知识,包括 MonoBehaviour 生命周期、常用 API 和编程模式。
创建第一个脚本
在编辑器中创建
- 在 Project 窗口中右键点击
- 选择 Create > C# Script
- 输入脚本名称(如
PlayerController) - 双击脚本在 Visual Studio 或 VS Code 中打开
脚本基本结构
using UnityEngine;
// 脚本类必须继承自 MonoBehaviour
public class PlayerController : MonoBehaviour
{
// Start 在脚本启用时调用一次
void Start()
{
Debug.Log("游戏开始!");
}
// Update 每帧调用一次
void Update()
{
// 每帧执行的代码
}
}
MonoBehaviour 生命周期
理解 MonoBehaviour 的生命周期方法是编写 Unity 脚本的基础。
生命周期流程图
脚本加载
│
▼
┌─────────────┐
│ Awake() │ ◄── 脚本实例化时调用(仅一次)
└─────────────┘ 用于初始化自身,不依赖其他对象
│
▼
┌─────────────┐
│ OnEnable() │ ◄── 对象启用时调用
└─────────────┘ 对象被激活或脚本被启用时
│
▼
┌─────────────┐
│ Start() │ ◄── 第一帧更新前调用(仅一次)
└─────────────┘ 用于初始化依赖其他对象的逻辑
│
▼
┌─────────────┐
│ FixedUpdate│ ◄── 固定时间间隔调用(物理更新)
└─────────────┘ 默认 0.02 秒(50次/秒)
│
▼
┌─────────────┐
│ Update() │ ◄── 每帧调用一次(主要游戏逻辑)
└─────────────┘
│
▼
┌─────────────┐
│ LateUpdate()│ ◄── 所有 Update 后调用
└─────────────┘ 适合相机跟随等逻辑
│
▼
┌─────────────┐
│ OnDisable()│ ◄── 对象禁用时调用
└─────────────┘
│
▼
┌─────────────┐
│ OnDestroy()│ ◄── 对象销毁时调用
└─────────────┘
核心生命周期方法详解
Awake()
void Awake()
{
// 脚本实例化时调用,仅一次
// 用于初始化自身,不依赖其他对象
// 即使脚本未启用也会调用
// 典型用途:
// - 缓存自身组件引用
// - 初始化变量
// - 注册事件
myRigidbody = GetComponent<Rigidbody>();
currentHealth = maxHealth;
}
Start()
void Start()
{
// 第一帧更新前调用,仅一次
// 用于初始化依赖其他对象的逻辑
// 只有脚本启用时才会调用
// 典型用途:
// - 查找其他对象
// - 建立对象间引用
// - 初始化游戏状态
player = GameObject.FindWithTag("Player").transform;
gameManager = FindObjectOfType<GameManager>();
}
Update()
void Update()
{
// 每帧调用一次
// 用于处理输入、游戏逻辑
// 帧率不固定,不要用于物理计算
// 典型用途:
// - 处理玩家输入
// - 非物理移动
// - 动画控制
// - UI 更新
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(horizontal, 0, vertical);
transform.Translate(movement * speed * Time.deltaTime);
}
FixedUpdate()
void FixedUpdate()
{
// 固定时间间隔调用
// 默认 0.02 秒(50Hz),可在 Time 设置中调整
// 用于物理相关的更新
// 典型用途:
// - 刚体力学操作
// - 物理移动
// - 碰撞检测相关的逻辑
Vector3 force = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
myRigidbody.AddForce(force * moveForce);
}
LateUpdate()
void LateUpdate()
{
// 所有 Update 调用完成后执行
// 确保在其他对象更新后再执行
// 典型用途:
// - 相机跟随(确保角色先移动)
// - 追踪目标位置
if (target != null)
{
transform.position = target.position + offset;
}
}
生命周期执行顺序对比
| 方法 | 调用频率 | 用途 | 注意 |
|---|---|---|---|
| Awake | 一次 | 自身初始化 | 不依赖其他对象 |
| Start | 一次 | 依赖初始化 | 在 Awake 之后 |
| FixedUpdate | 固定间隔 | 物理更新 | 默认 50次/秒 |
| Update | 每帧 | 游戏逻辑 | 帧率不固定 |
| LateUpdate | 每帧 | 后续处理 | 在 Update 后 |
常用属性和方法
访问游戏对象和组件
public class ComponentAccess : MonoBehaviour
{
void Start()
{
// ========== 访问当前对象 ==========
GameObject go = gameObject; // 当前游戏对象
Transform trans = transform; // Transform 组件
// ========== 获取组件 ==========
// 获取自身组件
Rigidbody rb = GetComponent<Rigidbody>();
Collider col = GetComponent<Collider>();
// 获取子对象组件
Renderer childRenderer = GetComponentInChildren<Renderer>();
// 获取父对象组件
Rigidbody parentRb = GetComponentInParent<Rigidbody>();
// 获取所有组件
Component[] allComponents = GetComponents<Component>();
// ========== 添加/移除组件 ==========
Rigidbody newRb = gameObject.AddComponent<Rigidbody>();
Destroy(GetComponent<Collider>()); // 移除组件
// ========== 启用/禁用组件 ==========
rb.enabled = false; // 禁用组件
rb.enabled = true; // 启用组件
}
}
游戏对象操作
public class GameObjectOperations : MonoBehaviour
{
void Start()
{
// ========== 激活状态 ==========
gameObject.SetActive(true); // 激活对象
gameObject.SetActive(false); // 禁用对象
bool isActive = gameObject.activeSelf; // 自身激活状态
bool inHierarchy = gameObject.activeInHierarchy; // 层级激活状态
// ========== 标签和层 ==========
gameObject.tag = "Player";
gameObject.layer = LayerMask.NameToLayer("Enemy");
// ========== 销毁对象 ==========
Destroy(gameObject); // 销毁对象
Destroy(gameObject, 2f); // 2秒后销毁
Destroy(this); // 销毁脚本组件
}
}
序列化字段
使用 [SerializeField] 可以在 Inspector 窗口中显示和编辑私有字段。
public class PlayerStats : MonoBehaviour
{
// 公有字段(自动序列化)
public int maxHealth = 100;
public float moveSpeed = 5f;
// 私有字段但可在 Inspector 中编辑
[SerializeField]
private int currentHealth;
[SerializeField]
private float jumpForce = 10f;
// 其他常用特性
[Range(0, 100)] // 显示为滑动条
[SerializeField]
private int armor = 50;
[Tooltip("玩家的初始等级")] // 鼠标悬停提示
[SerializeField]
private int startLevel = 1;
[Header("战斗属性")] // 分组标题
[SerializeField]
private float attackPower = 10f;
[Space(10)] // 添加间距
[SerializeField]
private float defense = 5f;
// 引用其他对象
[SerializeField]
private Transform target; // 可拖拽对象到此处
[SerializeField]
private GameObject bulletPrefab; // 预制体引用
}
Inspector 特性汇总
| 特性 | 作用 |
|---|---|
[SerializeField] | 序列化私有字段 |
[Range(min, max)] | 数值范围限制,显示滑动条 |
[Tooltip("文本")] | 鼠标悬停提示 |
[Header("标题")] | 在 Inspector 中显示分组标题 |
[Space(高度)] | 添加垂直间距 |
[HideInInspector] | 隐藏公有字段 |
[RequireComponent(typeof(X))] | 自动添加依赖组件 |
时间控制
Time 类
public class TimeControl : MonoBehaviour
{
void Update()
{
// ========== 时间缩放 ==========
Time.timeScale = 1f; // 正常速度
Time.timeScale = 0f; // 暂停
Time.timeScale = 0.5f; // 半速(慢动作)
Time.timeScale = 2f; // 双倍速度
// ========== 时间相关属性 ==========
float deltaTime = Time.deltaTime; // 上一帧耗时(秒)
float fixedDeltaTime = Time.fixedDeltaTime; // FixedUpdate 间隔
float time = Time.time; // 游戏开始后的总时间
float unscaledTime = Time.unscaledTime; // 不受 timeScale 影响的时间
int frameCount = Time.frameCount; // 总帧数
// ========== 帧率独立移动 ==========
// 错误:每帧移动固定距离,帧率不同速度不同
transform.Translate(Vector3.forward * speed);
// 正确:根据时间调整,确保每秒移动距离一致
transform.Translate(Vector3.forward * speed * Time.deltaTime);
}
}
延时调用
public class DelayedActions : MonoBehaviour
{
void Start()
{
// ========== Invoke ==========
Invoke("DoSomething", 2f); // 2秒后调用方法
InvokeRepeating("SpawnEnemy", 1f, 5f); // 1秒后开始,每5秒重复
// 取消调用
CancelInvoke("DoSomething"); // 取消特定方法
CancelInvoke(); // 取消所有 Invoke
bool isInvoked = IsInvoking("DoSomething"); // 检查是否已调度
// ========== Coroutine(协程)==========
StartCoroutine(MyCoroutine());
}
void DoSomething()
{
Debug.Log("执行延时任务");
}
// 协程示例
IEnumerator MyCoroutine()
{
Debug.Log("协程开始");
yield return new WaitForSeconds(2f); // 等待2秒
Debug.Log("2秒后");
yield return new WaitForEndOfFrame(); // 等待到帧结束
Debug.Log("帧结束");
yield return new WaitForFixedUpdate(); // 等待下一次 FixedUpdate
Debug.Log("FixedUpdate 后");
yield return null; // 等待下一帧
Debug.Log("下一帧");
yield return new WaitUntil(() => condition == true); // 等待条件满足
yield return new WaitWhile(() => condition == true); // 等待条件不满足
}
}
随机数
public class RandomExamples : MonoBehaviour
{
void Start()
{
// ========== 基本随机数 ==========
float random01 = Random.value; // 0.0 到 1.0
float randomRange = Random.Range(0f, 10f); // 0.0 到 10.0
int randomInt = Random.Range(0, 10); // 0 到 9(整数上限不包含)
// ========== 随机方向 ==========
Vector3 randomDir = Random.insideUnitSphere; // 单位球内随机点
Vector3 randomCircle = Random.insideUnitCircle; // 单位圆内随机点(XY平面)
Vector3 onUnitSphere = Random.onUnitSphere; // 单位球表面随机点
// ========== 随机颜色 ==========
Color randomColor = Random.ColorHSV(); // 随机颜色
Color randomColorRange = Random.ColorHSV(0f, 1f, 0.5f, 1f, 0.5f, 1f);
// ========== 初始化随机种子 ==========
Random.InitState(12345); // 设置种子,使随机可重复
// ========== 从数组随机选择 ==========
string[] items = { "剑", "盾", "药水", "金币" };
string randomItem = items[Random.Range(0, items.Length)];
// ========== 随机打乱数组 ==========
int[] numbers = { 1, 2, 3, 4, 5 };
ShuffleArray(numbers);
}
void ShuffleArray<T>(T[] array)
{
for (int i = array.Length - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
(array[i], array[j]) = (array[j], array[i]);
}
}
}
数学工具
Mathf 类
public class MathExamples : MonoBehaviour
{
void Start()
{
// ========== 常用常量 ==========
float pi = Mathf.PI; // 3.14159...
float infinity = Mathf.Infinity;
float negativeInfinity = Mathf.NegativeInfinity;
float epsilon = Mathf.Epsilon; // 最小正值
// ========== 限制和钳制 ==========
float clamped = Mathf.Clamp(150f, 0f, 100f); // 返回 100
int clampedInt = Mathf.Clamp(150, 0, 100); // 返回 100
float clamp01 = Mathf.Clamp01(1.5f); // 返回 1
// ========== 插值 ==========
float lerp = Mathf.Lerp(0f, 100f, 0.5f); // 返回 50
float lerpAngle = Mathf.LerpAngle(350f, 10f, 0.5f); // 角度插值
float smoothStep = Mathf.SmoothStep(0f, 100f, 0.5f); // 平滑插值
// ========== 移动平滑 ==========
float current = 0f;
float target = 100f;
float smooth = Mathf.MoveTowards(current, target, 10f); // 每步最多移动10
float smoothAngle = Mathf.MoveTowardsAngle(current, target, 10f);
// ========== 重复和乒乓 ==========
float repeat = Mathf.Repeat(5.5f, 2f); // 返回 1.5 (5.5 % 2)
float pingPong = Mathf.PingPong(3f, 2f); // 返回 1 (0->2->0 循环)
// ========== 舍入 ==========
float ceil = Mathf.Ceil(2.3f); // 3 (向上取整)
float floor = Mathf.Floor(2.7f); // 2 (向下取整)
float round = Mathf.Round(2.5f); // 2 (四舍五入)
int roundToInt = Mathf.RoundToInt(2.5f);
// ========== 三角函数 ==========
float sin = Mathf.Sin(Mathf.PI / 2); // 1
float cos = Mathf.Cos(0); // 1
float tan = Mathf.Tan(Mathf.PI / 4); // 1
float asin = Mathf.Asin(1); // PI/2
// 角度和弧度转换
float rad = Mathf.Deg2Rad * 90f; // 角度转弧度
float deg = Mathf.Rad2Deg * Mathf.PI; // 弧度转角度
// ========== 其他实用函数 ==========
float abs = Mathf.Abs(-5f); // 5
float sqrt = Mathf.Sqrt(16f); // 4
float pow = Mathf.Pow(2f, 3f); // 8
float exp = Mathf.Exp(2f); // e^2
float log = Mathf.Log(100f, 10f); // 2
float log10 = Mathf.Log10(100f); // 2
float max = Mathf.Max(1f, 5f, 3f); // 5
float min = Mathf.Min(1f, 5f, 3f); // 1
float sign = Mathf.Sign(-10f); // -1
bool isPowerOfTwo = Mathf.IsPowerOfTwo(16); // true
float closestPowerOfTwo = Mathf.ClosestPowerOfTwo(17); // 16
float nextPowerOfTwo = Mathf.NextPowerOfTwo(17); // 32
}
}
向量数学
public class VectorMath : MonoBehaviour
{
void Start()
{
Vector3 a = new Vector3(1, 2, 3);
Vector3 b = new Vector3(4, 5, 6);
// ========== 基本运算 ==========
Vector3 sum = a + b; // (5, 7, 9)
Vector3 diff = a - b; // (-3, -3, -3)
Vector3 scaled = a * 2f; // (2, 4, 6)
Vector3 divided = a / 2f; // (0.5, 1, 1.5)
// ========== 向量属性 ==========
float magnitude = a.magnitude; // 长度(模)
float sqrMagnitude = a.sqrMagnitude; // 长度平方(计算更快)
Vector3 normalized = a.normalized; // 单位向量(长度为1)
// ========== 向量操作 ==========
a.Normalize(); // 归一化自身
float distance = Vector3.Distance(a, b);
float dot = Vector3.Dot(a, b); // 点积
Vector3 cross = Vector3.Cross(a, b); // 叉积
Vector3 lerp = Vector3.Lerp(a, b, 0.5f); // 线性插值
Vector3 slerp = Vector3.Slerp(a, b, 0.5f); // 球面插值
Vector3 project = Vector3.Project(a, b); // 投影
Vector3 reflect = Vector3.Reflect(a, normal); // 反射
// ========== 常用方向向量 ==========
Vector3 zero = Vector3.zero; // (0, 0, 0)
Vector3 one = Vector3.one; // (1, 1, 1)
Vector3 up = Vector3.up; // (0, 1, 0)
Vector3 down = Vector3.down; // (0, -1, 0)
Vector3 right = Vector3.right; // (1, 0, 0)
Vector3 left = Vector3.left; // (-1, 0, 0)
Vector3 forward = Vector3.forward; // (0, 0, 1)
Vector3 back = Vector3.back; // (0, 0, -1)
// ========== 角度计算 ==========
float angle = Vector3.Angle(a, b); // 两向量夹角
float signedAngle = Vector3.SignedAngle(a, b, Vector3.up); // 带符号角度
// ========== 判断方向 ==========
bool isForward = Vector3.Dot(transform.forward, direction) > 0;
bool isRight = Vector3.Dot(transform.right, direction) > 0;
}
}
实践示例
示例1:简单的生命值系统
public class HealthSystem : MonoBehaviour
{
[Header("生命设置")]
[SerializeField] private float maxHealth = 100f;
[SerializeField] private float regenerationRate = 5f; // 每秒恢复
[SerializeField] private float regenerationDelay = 3f; // 受伤后多久开始恢复
private float currentHealth;
private float lastDamageTime;
private bool isDead = false;
// 事件
public event Action OnDeath;
public event Action<float> OnHealthChanged;
public event Action OnTakeDamage;
public float CurrentHealth => currentHealth;
public float MaxHealth => maxHealth;
public float HealthPercent => currentHealth / maxHealth;
public bool IsDead => isDead;
void Awake()
{
currentHealth = maxHealth;
}
void Update()
{
// 生命恢复
if (!isDead && currentHealth < maxHealth)
{
if (Time.time >= lastDamageTime + regenerationDelay)
{
RegenerateHealth();
}
}
}
public void TakeDamage(float damage)
{
if (isDead || damage <= 0) return;
currentHealth = Mathf.Max(0, currentHealth - damage);
lastDamageTime = Time.time;
OnHealthChanged?.Invoke(currentHealth);
OnTakeDamage?.Invoke();
if (currentHealth <= 0)
{
Die();
}
}
public void Heal(float amount)
{
if (isDead || amount <= 0) return;
currentHealth = Mathf.Min(maxHealth, currentHealth + amount);
OnHealthChanged?.Invoke(currentHealth);
}
void RegenerateHealth()
{
float healAmount = regenerationRate * Time.deltaTime;
currentHealth = Mathf.Min(maxHealth, currentHealth + healAmount);
OnHealthChanged?.Invoke(currentHealth);
}
void Die()
{
isDead = true;
OnDeath?.Invoke();
Debug.Log($"{gameObject.name} 已死亡");
}
}
示例2:对象池模式
public class ObjectPool : MonoBehaviour
{
[SerializeField] private GameObject prefab;
[SerializeField] private int initialSize = 10;
[SerializeField] private bool canExpand = true;
private Queue<GameObject> pool = new Queue<GameObject>();
private List<GameObject> activeObjects = new List<GameObject>();
void Start()
{
InitializePool();
}
void InitializePool()
{
for (int i = 0; i < initialSize; i++)
{
CreateNewObject();
}
}
GameObject CreateNewObject()
{
GameObject obj = Instantiate(prefab, transform);
obj.SetActive(false);
pool.Enqueue(obj);
return obj;
}
public GameObject GetFromPool(Vector3 position, Quaternion rotation)
{
GameObject obj;
if (pool.Count > 0)
{
obj = pool.Dequeue();
}
else if (canExpand)
{
obj = CreateNewObject();
pool.Dequeue(); // 移除刚创建的
}
else
{
return null; // 池已满且不允许扩展
}
obj.transform.position = position;
obj.transform.rotation = rotation;
obj.SetActive(true);
activeObjects.Add(obj);
// 调用初始化方法(如果对象有 IPoolable 接口)
var poolable = obj.GetComponent<IPoolable>();
poolable?.OnSpawnFromPool();
return obj;
}
public void ReturnToPool(GameObject obj)
{
if (!activeObjects.Contains(obj)) return;
var poolable = obj.GetComponent<IPoolable>();
poolable?.OnReturnToPool();
obj.SetActive(false);
activeObjects.Remove(obj);
pool.Enqueue(obj);
}
public void ReturnAllToPool()
{
foreach (var obj in activeObjects.ToArray())
{
ReturnToPool(obj);
}
}
}
// 可池化对象接口
public interface IPoolable
{
void OnSpawnFromPool();
void OnReturnToPool();
}
最佳实践
- 缓存组件引用:在 Awake 或 Start 中缓存常用组件
- 使用 [SerializeField]:而非 public 字段暴露 Inspector 变量
- 避免在 Update 中查找:使用 Find 和 GetComponent 有性能开销
- 使用事件通信:组件间通过事件解耦,而非直接引用
- 合理使用生命周期:Awake 用于自身初始化,Start 用于依赖初始化
下一步
掌握脚本基础后,你可以: