unity-csharp-scripting

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Unity C# Scripting (MonoBehaviour)

Unity C# 脚本编写(MonoBehaviour)

Write correct, idiomatic gameplay scripts in Unity 6. Get the lifecycle, component access, serialization, and coroutines right so behaviour is deterministic and the Inspector stays useful. Targets Unity 6 (6000.0 LTS), C# / .NET Standard 2.1.
编写符合规范的Unity 6游戏玩法脚本。正确掌握生命周期、组件访问、序列化和协程,确保行为可预测且Inspector面板保持实用。目标版本为Unity 6 (6000.0 LTS),使用C# / .NET Standard 2.1。

When to use

使用场景

  • Use when authoring or fixing a
    MonoBehaviour
    : choosing the right lifecycle callback, reading/caching components, exposing fields to the Inspector, or running timed logic with coroutines.
  • Use when the project has
    *.cs
    files, an
    Assembly-CSharp
    or
    *.asmdef
    , and a
    ProjectSettings/
    folder.
When not to use: moving rigidbodies / collision response →
unity-physics
; reading player input →
unity-input-system
; shared data assets / config →
unity-scriptableobjects
; Animator parameters →
unity-animation
. This skill owns the script lifecycle and C# plumbing, not those subsystems.
  • 当编写或修复
    MonoBehaviour
    脚本时使用:选择合适的生命周期回调、读取/缓存组件、向Inspector面板暴露字段,或使用协程执行定时逻辑。
  • 当项目包含
    *.cs
    文件、
    Assembly-CSharp
    *.asmdef
    文件,以及
    ProjectSettings/
    文件夹时使用。
不适用场景:移动刚体/碰撞响应 → 请使用
unity-physics
;读取玩家输入 → 请使用
unity-input-system
;共享数据资源/配置 → 请使用
unity-scriptableobjects
;Animator参数 → 请使用
unity-animation
。本技能负责处理脚本生命周期与C#基础架构,而非上述子系统。

Core workflow

核心工作流程

  1. Pick the callback by purpose, not habit.
    Awake
    (cache references, runs once on load),
    OnEnable
    (subscribe to events),
    Start
    (init that depends on other objects'
    Awake
    ),
    Update
    (per-frame logic/input polling),
    FixedUpdate
    (physics),
    LateUpdate
    (camera follow after movement),
    OnDisable
    /
    OnDestroy
    (unsubscribe/cleanup).
  2. Cache component lookups in
    Awake
    — never call
    GetComponent
    every frame.
  3. Expose tunables with
    [SerializeField] private
    , not public fields, so other code can't mutate them but designers can edit them in the Inspector.
  4. Scale per-frame values by
    Time.deltaTime
    in
    Update
    (and
    Time.fixedDeltaTime
    semantics are automatic in
    FixedUpdate
    ).
  5. Use coroutines for time-sequenced logic (delays, tweens, "do X then wait then Y"); start them with
    StartCoroutine
    and stop them deterministically.
  6. Verify in Play mode: check the Console for null-reference exceptions, confirm values in the Inspector update as expected, and watch the Profiler if
    Update
    is hot.
  1. 根据用途选择回调,而非习惯
    Awake
    (缓存引用,加载时运行一次)、
    OnEnable
    (订阅事件)、
    Start
    (依赖其他对象
    Awake
    完成的初始化)、
    Update
    (每帧逻辑/输入轮询)、
    FixedUpdate
    (物理相关)、
    LateUpdate
    (移动后跟随相机)、
    OnDisable
    /
    OnDestroy
    (取消订阅/清理)。
  2. Awake
    中缓存组件查找结果
    —— 绝不要每帧调用
    GetComponent
  3. 使用
    [SerializeField] private
    暴露可调参数
    ,而非公共字段,这样其他代码无法修改它们,但设计师可以在Inspector面板中编辑。
  4. Update
    中按
    Time.deltaTime
    缩放每帧数值
    FixedUpdate
    Time.fixedDeltaTime
    的语义是自动处理的)。
  5. 使用协程处理时间序列逻辑(延迟、补间、“执行X然后等待再执行Y”);通过
    StartCoroutine
    启动协程,并以可预测的方式停止它们。
  6. 在Play模式中验证:检查控制台是否有空引用异常,确认Inspector面板中的值如预期更新,如果
    Update
    占用过高性能则查看Profiler。

Patterns

模式示例

1. Lifecycle + cached components (the canonical skeleton)

1. 生命周期 + 缓存组件(标准框架)

csharp
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]      // auto-adds the dependency, prevents null refs
public class PlayerController : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 6f;   // editable in Inspector, private in code
    private Rigidbody _rb;                            // cached, not fetched per frame

    private void Awake() => _rb = GetComponent<Rigidbody>();  // cache once on load

    private void Update()
    {
        // Per-frame, non-physics work. Scale by deltaTime so it is frame-rate independent.
        transform.Rotate(0f, 90f * Time.deltaTime, 0f);
    }

    private void FixedUpdate()
    {
        // Physics work belongs here (fixed timestep). See the unity-physics skill.
        _rb.MovePosition(_rb.position + transform.forward * moveSpeed * Time.fixedDeltaTime);
    }
}
csharp
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]      // 自动添加依赖,防止空引用
public class PlayerController : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 6f;   // 可在Inspector中编辑,代码中为私有
    private Rigidbody _rb;                            // 缓存,不每帧获取

    private void Awake() => _rb = GetComponent<Rigidbody>();  // 加载时缓存一次

    private void Update()
    {
        // 每帧执行的非物理操作。按deltaTime缩放以确保帧率无关。
        transform.Rotate(0f, 90f * Time.deltaTime, 0f);
    }

    private void FixedUpdate()
    {
        // 物理操作应在此处执行(固定时间步长)。请查看unity-physics技能。
        _rb.MovePosition(_rb.position + transform.forward * moveSpeed * Time.fixedDeltaTime);
    }
}

2. Safe component access with
TryGetComponent

2. 使用
TryGetComponent
安全访问组件

csharp
// Avoids allocating a null and is clearer than GetComponent + null check.
if (other.TryGetComponent<Health>(out var health))
    health.Apply(-10);
csharp
// 避免分配空值,比GetComponent + 空值检查更清晰。
if (other.TryGetComponent<Health>(out var health))
    health.Apply(-10);

3. Serialization that shows up correctly in the Inspector

3. 在Inspector中正确显示的序列化

csharp
[SerializeField, Range(0f, 1f)] private float volume = 0.8f;  // slider
[SerializeField] private string playerName = "Hero";          // private but serialized

[System.Serializable]                 // REQUIRED for a plain class to serialize/show
public class Stats { public int hp = 100; public int mana = 50; }

[SerializeField] private Stats stats = new();  // nested struct-like data in the Inspector
csharp
[SerializeField, Range(0f, 1f)] private float volume = 0.8f;  // 滑块
[SerializeField] private string playerName = "Hero";          // 私有但可序列化

[System.Serializable]                 // 普通类要序列化/显示必须添加此特性
public class Stats { public int hp = 100; public int mana = 50; }

[SerializeField] private Stats stats = new();  // Inspector中的嵌套类数据

4. Coroutines for time-sequenced logic

4. 协程处理时间序列逻辑

csharp
private void Start() => StartCoroutine(FlashThenHide());

private System.Collections.IEnumerator FlashThenHide()
{
    yield return new WaitForSeconds(0.5f);   // wait half a second of game time
    GetComponent<Renderer>().enabled = false;
    yield return null;                       // resume next frame
}
csharp
private void Start() => StartCoroutine(FlashThenHide());

private System.Collections.IEnumerator FlashThenHide()
{
    yield return new WaitForSeconds(0.5f);   // 等待0.5秒游戏时间
    GetComponent<Renderer>().enabled = false;
    yield return null;                       // 下一帧恢复执行
}

Pitfalls

常见陷阱

  • GetComponent
    in
    Update
    — it searches every frame and tanks performance. Cache the reference in
    Awake
    /
    Start
    .
  • Physics in
    Update
    — moving a
    Rigidbody
    with forces or
    MovePosition
    outside
    FixedUpdate
    causes jitter and timestep-dependent behaviour. Read input in
    Update
    , apply physics in
    FixedUpdate
    .
  • Relying on
    Start
    order across objects
    Start
    runs after all
    Awake
    s, but order among
    Start
    s is undefined. Do cross-object wiring in
    Start
    , self-setup in
    Awake
    .
  • public
    fields just to show them in the Inspector
    — that also lets any script mutate them. Use
    [SerializeField] private
    instead.
  • gameObject.tag == "Enemy"
    allocates a string and is slower; use
    gameObject.CompareTag("Enemy")
    .
  • Coroutines stop when the GameObject is disabled — a disabled object's coroutines are killed; re-
    StartCoroutine
    in
    OnEnable
    if it must survive toggling.
  • Update
    never runs before
    Start
    , but the first
    Update
    can run on the same frame as
    Start
    — guard against not-yet-initialised fields if you split setup oddly.
  • Update
    中调用
    GetComponent
    —— 每帧都会搜索,严重影响性能。在
    Awake
    /
    Start
    中缓存引用。
  • Update
    中处理物理
    —— 在
    FixedUpdate
    之外使用力或
    MovePosition
    移动
    Rigidbody
    会导致抖动和时间步长相关的异常行为。在
    Update
    中读取输入,在
    FixedUpdate
    中应用物理。
  • 依赖多个对象的
    Start
    执行顺序
    ——
    Start
    在所有
    Awake
    执行后运行,但多个对象的
    Start
    执行顺序未定义。跨对象连接在
    Start
    中完成,自设置在
    Awake
    中完成。
  • 仅为了在Inspector中显示而使用
    public
    字段
    —— 这会让任何脚本都能修改它们。请改用
    [SerializeField] private
  • 使用
    gameObject.tag == "Enemy"
    —— 会分配字符串,速度较慢;请使用
    gameObject.CompareTag("Enemy")
  • GameObject禁用时协程会停止 —— 禁用对象的协程会被终止;如果需要在切换启用状态时继续运行,请在
    OnEnable
    中重新调用
    StartCoroutine
  • Update
    永远不会在
    Start
    之前运行,但第一次
    Update
    可能与
    Start
    在同一帧运行
    —— 如果拆分设置逻辑不当,请防范未初始化的字段。

References

参考资料

  • For the full event-execution-order table and advanced coroutine patterns (custom
    CustomYieldInstruction
    , stopping by handle,
    WaitUntil
    /
    WaitWhile
    ), read
    references/lifecycle-and-coroutines.md
    .
  • Primary docs: Unity Manual "Event function execution order" (
    https://docs.unity3d.com/Manual/execution-order.html
    ) and
    ScriptReference/MonoBehaviour
    .
  • 完整事件执行顺序表和高级协程模式(自定义
    CustomYieldInstruction
    、通过句柄停止、
    WaitUntil
    /
    WaitWhile
    ),请阅读
    references/lifecycle-and-coroutines.md
  • 官方文档:Unity手册“事件函数执行顺序”(
    https://docs.unity3d.com/Manual/execution-order.html
    )和
    ScriptReference/MonoBehaviour

Related skills

相关技能

  • unity-physics
    Rigidbody
    , collisions, and
    FixedUpdate
    motion.
  • unity-input-system
    — reading player input into these scripts.
  • unity-scriptableobjects
    — sharing data/config between scripts without singletons.
  • unity-physics
    ——
    Rigidbody
    、碰撞和
    FixedUpdate
    运动。
  • unity-input-system
    —— 在这些脚本中读取玩家输入。
  • unity-scriptableobjects
    —— 在脚本之间共享数据/配置,无需单例。