mcpField
U
unity-scripting

unity-scripting

github.com/nice-wolf-studio开发4074 次安装6 周前更新
代码仓库

关于 unity-scripting

面向 Unity 开发者的 C# 脚本参考,涵盖游戏运行时与编辑器脚本。内容汇总生命周期、异步模式、序列化和常用 API,并说明常见错误的规避方法。

主要功能

  1. 01MonoBehaviour 生命周期与回调
  2. 02协程与 Unity 6 Awaitable
  3. 03事件和 ScriptableObject 模式
  4. 04序列化规则与特性
  5. 05核心 API 和常见反模式

适用场景

  1. 01为游戏逻辑选择回调
  2. 02实现跨帧异步操作
  3. 03共享配置与事件数据
  4. 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.md
monobehaviour-lifecycle.md
object-pool.md
scriptableobjects.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)
属性用途
destroyCancellationTokenMonoBehaviour 销毁时触发的令牌(用于异步取消)
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 会导致未定义行为。

特性CoroutineAwaitable
返回值否是(Awaitable<T>)
线程切换否是
内存分配每次 yield 都有开销对象池管理,开销极小
取消手动调用 StopCoroutine支持 CancellationToken
错误处理不支持 try/catch完整支持 try/catch/finally

完整的异步参考:references/coroutines-and-async.md

事件和通信模式

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 会序列化满足以下所有条件的字段:

  1. 是 public,或具有 [SerializeField] 属性
  2. 不是 static、const 或 readonly
  3. 是可序列化类型

可序列化类型: 基元类型、枚举(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 组件

其他资源

评论

支持 Markdown