关于 unity-scripting
面向 Unity 开发者的 C# 脚本参考,涵盖游戏运行时与编辑器脚本。内容汇总生命周期、异步模式、序列化和常用 API,并说明常见错误的规避方法。
主要功能
- 01MonoBehaviour 生命周期与回调
- 02协程与 Unity 6 Awaitable
- 03事件和 ScriptableObject 模式
- 04序列化规则与特性
- 05核心 API 和常见反模式
适用场景
- 01为游戏逻辑选择回调
- 02实现跨帧异步操作
- 03共享配置与事件数据
- 04规避性能和生命周期问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-scripting安装后在 Claude Code 中输入/unity-scripting确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/coroutines-and-async.mdmonobehaviour-lifecycle.mdobject-pool.mdscriptableobjects.md详细介绍
Unity C# 脚本
脚本基础
C# 脚本(.cs 文件)存储在 Assets 文件夹中。脚本通过继承内置类型来获得 Unity 功能:
- UnityEngine.Object —— 使自定义类型可赋值给 Inspector 字段
- MonoBehaviour —— 作为组件附加到 GameObject,以控制场景中的行为
- ScriptableObject —— 不附加到 GameObject 的独立数据资源
脚本在两种上下文中运行:
- 运行时脚本 —— 在 Player 构建版本中执行(使用
UnityEngine命名空间) - 编辑器脚本 —— 仅在 Editor 中运行(使用
UnityEditor命名空间,放置在Editor文件夹中)
MonoBehaviour 生命周期
MonoBehaviour 始终作为 GameObject 的组件存在。生命周期事件函数会严格按顺序执行。除非通过 Script Execution Order 设置配置,否则不能依赖不同 GameObject 上同一事件函数的调用顺序。
执行顺序(ASCII 图)
INITIALIZATION
|
v
[Awake] ---------> Called when script instance loads (once per lifetime)
|
v
[OnEnable] ------> Called when object/component becomes enabled
|
v
(SceneManager.sceneLoaded fires here -- after OnEnable, before Start)
|
v
[Start] ---------> Called before first frame Update (once per lifetime)
|
|
| +===========================================+
| | PHYSICS LOOP (fixed timestep) |
| | |
+->| [FixedUpdate] --> Internal Physics ------>|
| | | |
| | [yield WaitForFixedUpdate resumes] |
| +===========================================+
|
v
[Update] --------> Called once per frame
|
v
[yield null / yield WaitForSeconds resumes]
|
v
(Internal Animation Update)
| [OnAnimatorMove]
| [OnAnimatorIK]
|
v
[LateUpdate] ----> Called after all Update functions complete
|
v
RENDERING
| [OnWillRenderObject]
| [OnPreCull] [OnBecameVisible/Invisible]
| [OnPreRender]
| [OnRenderObject]
| [OnPostRender]
| [OnRenderImage]
|
v
[OnGUI] ---------> Called for GUI rendering events
|
v
[yield WaitForEndOfFrame resumes]
|
v
DEACTIVATION / TEARDOWN
|
v
[OnDisable] -----> Called when component/object is disabled
|
v
[OnDestroy] -----> Called before object destruction
关键生命周期回调
| 回调 | 时机 | 用途 |
|---|---|---|
Awake() | 脚本实例加载时 | 一次性初始化、缓存引用 |
OnEnable() | 组件启用时 | 订阅事件 |
Start() | 首次调用 Update 之前 | 依赖其他 Awake() 调用的初始化 |
FixedUpdate() | 固定时间步(默认 0.02s) | 物理计算、Rigidbody 力 |
Update() | 每帧 | 输入、非物理游戏逻辑 |
LateUpdate() | 所有 Update 调用之后 | 摄像机跟随、Update 后的调整 |
OnDisable() | 组件禁用时 | 取消订阅事件 |
OnDestroy() | 销毁之前 | 最终清理 |
物理回调
// 3D Physics
void OnCollisionEnter(Collision collision) { }
void OnCollisionStay(Collision collision) { }
void OnCollisionExit(Collision collision) { }
void OnTriggerEnter(Collider other) { }
void OnTriggerStay(Collider other) { }
void OnTriggerExit(Collider other) { }
// 2D Physics
void OnCollisionEnter2D(Collision2D collision) { }
void OnTriggerEnter2D(Collider2D other) { }
MonoBehaviour 属性(Unity 6)
| 属性 | 用途 |
|---|---|
destroyCancellationToken | MonoBehaviour 销毁时触发的令牌(用于异步取消) |
didAwake | 是否已调用 Awake |
didStart | 是否已调用 Start |
runInEditMode | 允许脚本在编辑器中执行 |
完整的生命周期参考:references/monobehaviour-lifecycle.md
协程与 Async/Await
Unity 支持两种跨多帧执行操作的模式。
协程(IEnumerator)
通过 yield return 暂停并根据 yield 指令恢复执行的方法。
IEnumerator Fade()
{
Color c = renderer.material.color;
for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
{
c.a = alpha;
renderer.material.color = c;
yield return new WaitForSeconds(0.1f);
}
}
void Update()
{
if (Input.GetKeyDown("f"))
{
StartCoroutine(Fade());
}
}
Yield 指令:
yield return null—— 下一帧恢复(Update 之后)yield return new WaitForSeconds(t)—— t 秒后恢复yield return new WaitForFixedUpdate()—— FixedUpdate 后恢复yield return new WaitForEndOfFrame()—— 渲染后恢复yield return new WaitUntil(() => condition)—— condition 为 true 时恢复yield return StartCoroutine(other)—— 等待嵌套协程
**重要:**协程在主线程上运行。通过 enabled = false 禁用 MonoBehaviour 不会停止协程。停用 GameObject 或销毁 MonoBehaviour 则会停止协程。
Awaitable(Unity 6 Async/Await)
Awaitable 是 Unity 自定义的异步类型,通常比基于迭代器的协程更高效。它通过对象池进行管理,以限制内存分配。
async Awaitable SampleSchedulingJobsForNextFrame()
{
await Awaitable.EndOfFrameAsync();
var jobHandle = ScheduleSomethingWithJobSystem();
await Awaitable.NextFrameAsync();
jobHandle.Complete();
}
Awaitable 方法:
Awaitable.NextFrameAsync()-- 在下一帧恢复Awaitable.FixedUpdateAsync()-- 在下一次 FixedUpdate 时恢复Awaitable.EndOfFrameAsync()-- 在帧末恢复Awaitable.WaitForSecondsAsync(float)-- 延迟后恢复Awaitable.MainThreadAsync()-- 强制在主线程上继续执行Awaitable.BackgroundThreadAsync()-- 强制在后台线程上继续执行
重要限制: Awaitable 实例由对象池管理,同一个实例只能 await 一次。多次 await 会导致未定义行为。
| 特性 | Coroutine | Awaitable |
|---|---|---|
| 返回值 | 否 | 是(Awaitable<T>) |
| 线程切换 | 否 | 是 |
| 内存分配 | 每次 yield 都有开销 | 对象池管理,开销极小 |
| 取消 | 手动调用 StopCoroutine | 支持 CancellationToken |
| 错误处理 | 不支持 try/catch | 完整支持 try/catch/finally |
事件和通信模式
C# 事件和委托
public class Health : MonoBehaviour
{
public event System.Action<float> OnDamageTaken;
public event System.Action OnDeath;
private float _hp = 100f;
public void TakeDamage(float amount)
{
_hp -= amount;
OnDamageTaken?.Invoke(amount);
if (_hp <= 0f)
OnDeath?.Invoke();
}
}
public class UIHealthBar : MonoBehaviour
{
[SerializeField] private Health _health;
void OnEnable()
{
_health.OnDamageTaken += HandleDamage;
}
void OnDisable()
{
_health.OnDamageTaken -= HandleDamage;
}
private void HandleDamage(float amount)
{
// Update UI
}
}
UnityEvents(可在 Inspector 中分配)
using UnityEngine.Events;
public class GameManager : MonoBehaviour
{
public UnityEvent OnGameStart;
public UnityEvent<int> OnScoreChanged;
public void StartGame()
{
OnGameStart?.Invoke();
}
}
ScriptableObject 事件通道
[CreateAssetMenu(menuName = "Events/Void Event Channel")]
public class VoidEventChannel : ScriptableObject
{
private System.Action _onEventRaised;
public void RaiseEvent()
{
_onEventRaised?.Invoke();
}
public void Subscribe(System.Action listener) => _onEventRaised += listener;
public void Unsubscribe(System.Action listener) => _onEventRaised -= listener;
}
ScriptableObjects
ScriptableObject 是派生自 UnityEngine.Object 的可序列化 Unity 类型。它们作为独立的项目资源存在,不附加到 GameObject 上。可将其用于共享数据、配置和事件通道。
[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/SpawnManagerScriptableObject", order = 1)]
public class SpawnManagerScriptableObject : ScriptableObject
{
public string prefabName;
public int numberOfPrefabsToCreate;
public Vector3[] spawnPoints;
}
主要行为:
- 在 Edit 模式下,Inspector 中的修改会自动保存;脚本中的修改需要调用
EditorUtility.SetDirty() - 在运行时,玩家可以读取 ScriptableObject 数据,但无法将修改持久化到磁盘
- 内存效率高:多个对象引用同一个资源实例,而不是重复保存数据
完整的 ScriptableObject 参考:references/scriptableobjects.md
序列化速查
Unity 会序列化满足以下所有条件的字段:
- 是
public,或具有[SerializeField]属性 - 不是
static、const或readonly - 是可序列化类型
可序列化类型: 基元类型、枚举(32 位或更小)、Unity 内置类型(Vector2、Vector3、Rect、Color、AnimationCurve 等)、带有 [Serializable] 的自定义类/结构体、UnityEngine.Object 引用,以及上述类型的 List<T> 和数组。
主要属性:
[SerializeField] private float _speed = 5f; // Serialize private field
[field: SerializeField] public float Speed { get; private set; } // Auto-property
[NonSerialized] public float tempValue; // Exclude from serialization
[HideInInspector] public float hiddenValue; // Serialize but hide from Inspector
[SerializeReference] private IMyInterface _impl; // Polymorphic serialization
不支持: 多维数组、交错数组、字典、嵌套容器。对于不受支持类型的自定义序列化,请使用 ISerializationCallbackReceiver。
核心 API 速查
Vector3
// Static direction shortcuts
Vector3.zero; // (0, 0, 0)
Vector3.one; // (1, 1, 1)
Vector3.up; // (0, 1, 0)
Vector3.forward; // (0, 0, 1)
Vector3.right; // (1, 0, 0)
// Common operations
float dist = Vector3.Distance(a, b);
float dot = Vector3.Dot(a.normalized, b.normalized);
Vector3 cross = Vector3.Cross(a, b);
Vector3 smoothed = Vector3.Lerp(from, to, t);
Vector3 moved = Vector3.MoveTowards(current, target, maxDelta);
Vector3 projected = Vector3.ProjectOnPlane(velocity, groundNormal);
属性: magnitude、sqrMagnitude(用于比较,可避免开平方运算)和 normalized。
Quaternion
// Creation
Quaternion.identity; // No rotation
Quaternion.Euler(0f, 90f, 0f); // From Euler angles (degrees)
Quaternion.LookRotation(direction, Vector3.up); // Face a direction
Quaternion.AngleAxis(45f, Vector3.up); // Rotate around axis
Quaternion.FromToRotation(Vector3.up, normal); // Rotation between directions
// Interpolation
Quaternion.Slerp(from, to, t); // Spherical interpolation
Quaternion.Lerp(from, to, t); // Linear interpolation
// Operations
float angle = Quaternion.Angle(a, b); // Angle in degrees (0-180)
Vector3 rotatedPoint = rotation * point; // Rotate a vector
Quaternion combined = rotA * rotB; // Combine rotations
切勿直接修改 x、y、z、w。 请使用 Euler()、AngleAxis() 或 LookRotation()。
Time
Time.deltaTime // Seconds since last frame (use in Update)
Time.fixedDeltaTime // Fixed timestep interval (use in FixedUpdate)
Time.time // Time since game start
Time.timeScale // 0 = paused, 1 = normal, 2 = double speed
Time.unscaledDeltaTime // Ignores timeScale (for UI animations during pause)
Debug
Debug.Log("Message");
Debug.LogWarning("Warning");
Debug.LogError("Error");
Debug.DrawRay(origin, direction, Color.red, duration);
Debug.DrawLine(start, end, Color.green, duration);
常见模式
缓存组件引用
public class PlayerMovement : MonoBehaviour
{
private Rigidbody _rb;
private Transform _transform;
void Awake()
{
_rb = GetComponent<Rigidbody>();
_transform = transform; // Cache the transform property
}
void FixedUpdate()
{
// Use cached references -- never call GetComponent in Update/FixedUpdate
_rb.AddForce(Vector3.up * 10f);
}
}
单例模式
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
}
使用 Cancellation 进行异步操作(Unity 6)
public class AsyncExample : MonoBehaviour
{
async void Start()
{
try
{
await LoadAndProcessAsync(destroyCancellationToken);
}
catch (OperationCanceledException) { }
}
async Awaitable LoadAndProcessAsync(CancellationToken token)
{
await Awaitable.BackgroundThreadAsync();
// Heavy computation here (off main thread)
var result = ComputeExpensiveData();
await Awaitable.MainThreadAsync();
// Back on main thread -- safe to use Unity API
ApplyResult(result);
}
}
条件等待(WaitUntil 的 Awaitable 替代方案)
public static async Awaitable AwaitableUntil(Func<bool> condition, CancellationToken token)
{
while (!condition())
{
token.ThrowIfCancellationRequested();
await Awaitable.NextFrameAsync();
}
}
反模式
| 反模式 | 问题 | 修复方法 |
|---|---|---|
在 Update() 中使用 GetComponent<T>() | 每帧都会分配内存并进行搜索 | 在 Awake() 中缓存 |
在 Update() 中使用 GameObject.Find() | 每帧都要进行开销很大的字符串搜索 | 缓存引用或使用序列化字段 |
在性能关键路径中反复使用 new Vector3() | 每帧都会产生不必要的构造函数开销 | 使用 Vector3.zero、Vector3.one,或缓存可重复使用的值 |
直接修改 Quaternion.x/y/z/w | 会产生无效旋转 | 使用 Euler()、AngleAxis()、LookRotation() |
在 Update() 中处理物理逻辑 | 帧率变化时结果不一致 | 使用 FixedUpdate() 施加 Rigidbody 力 |
在 FixedUpdate() 中使用 Time.deltaTime | 可以正常工作(Unity 会隐式返回 fixedDeltaTime),但读者不易理解 | 为清晰起见,明确使用 Time.fixedDeltaTime |
忘记在 OnDisable 中取消订阅事件 | 内存泄漏、空引用错误 | 始终在 OnDisable() 中取消订阅 |
对同一个 Awaitable 使用两次 await | 行为未定义(实例会被放入池中) | 只等待一次,或使用 .AsTask() 包装 |
空的 Update() / FixedUpdate() 方法 | Unity 仍会调用它们(产生开销) | 移除空的事件函数 |
基于字符串的 Invoke("MethodName", t) | 无法在编译时进行安全检查,重命名时会失效 | 改用协程或 Awaitable |
相关技能
- unity-foundations -- GameObject、组件、变换、场景、预制体
- unity-physics -- Rigidbody、碰撞体、射线检测、物理材质
- unity-input -- Input System、InputActions、PlayerInput 组件
其他资源
- 脚本 | 执行顺序 | 协程 | 异步/等待
- ScriptableObjects | MonoBehaviour | Vector3
- 对象池(
ObjectPool<T>)相关内容,请参阅 references/object-pool.md