跳到主要内容

脚本基础

Unity 使用 C# 作为主要的脚本语言。本章将介绍 Unity 脚本的基础知识,包括 MonoBehaviour 生命周期、常用 API 和编程模式。

创建第一个脚本

在编辑器中创建

  1. 在 Project 窗口中右键点击
  2. 选择 Create > C# Script
  3. 输入脚本名称(如 PlayerController
  4. 双击脚本在 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();
}

最佳实践

  1. 缓存组件引用:在 Awake 或 Start 中缓存常用组件
  2. 使用 [SerializeField]:而非 public 字段暴露 Inspector 变量
  3. 避免在 Update 中查找:使用 Find 和 GetComponent 有性能开销
  4. 使用事件通信:组件间通过事件解耦,而非直接引用
  5. 合理使用生命周期:Awake 用于自身初始化,Start 用于依赖初始化

下一步

掌握脚本基础后,你可以:

  1. 学习 输入系统 处理玩家输入
  2. 了解 物理系统 实现碰撞检测
  3. 探索 动画系统 制作角色动画(即将推出)