unity-csharp

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Unity C# Skill

Unity C# 技能指南

Version: 2.0 Stack: Unity, C#
Patterns for writing clean, performant Unity C# code. Includes VR/mobile optimization.

Version: 2.0 Stack: Unity, C#
编写整洁、高性能Unity C#代码的设计模式,包含VR/移动端优化内容。

Scope and Boundaries

适用范围与边界

This skill covers:
  • MonoBehaviour lifecycle and component architecture
  • Unity-specific C# patterns (caching, events, coroutines, null safety)
  • Performance optimization (draw calls, batching, LODs, pooling)
  • VR/mobile performance targets and profiling
  • ScriptableObject usage
Defers to other skills:
  • vrc-udon
    : VRChat-specific scripting (UdonSharp)
  • vrc-worlds
    : VRChat world setup and limits
  • vrc-avatars
    : VRChat avatar setup and limits
Use this skill when: Writing C# scripts for Unity, or optimizing Unity performance for VR/mobile.

本技能涵盖:
  • MonoBehaviour生命周期与组件架构
  • Unity专属C#设计模式(缓存、事件、协程、空值安全)
  • 性能优化(绘制调用、批处理、LOD、对象池)
  • VR/移动端性能指标与性能分析
  • ScriptableObject的使用
以下内容请参考其他技能文档:
  • vrc-udon
    : VRChat专属脚本开发(UdonSharp)
  • vrc-worlds
    : VRChat世界搭建与限制
  • vrc-avatars
    : VRChat Avatar搭建与限制
适用场景: 编写Unity的C#脚本,或针对VR/移动端优化Unity性能时。

Core Principles

核心原则

  1. Composition Over Inheritance — Small, focused components.
  2. Avoid Update() When Possible — Event-driven or coroutines instead.
  3. Cache References — GetComponent is expensive; cache in Awake.
  4. ScriptableObjects for Data — Decouple data from behavior.
  5. Null-Safe Access — Unity objects can be destroyed at any time.
  6. Measure First — Profile before optimizing; gut feelings lie.
  7. Batch Aggressively — Same material = potential batch. Draw calls matter most in VR.

  1. 组合优于继承 — 使用小巧、职责单一的组件。
  2. 尽可能避免使用Update() — 优先采用事件驱动或协程。
  3. 缓存引用 — GetComponent开销较大,应在Awake中缓存。
  4. 使用ScriptableObject存储数据 — 实现数据与逻辑解耦。
  5. 空值安全访问 — Unity对象可能随时被销毁。
  6. 先测量再优化 — 优化前先进行性能分析,直觉不可靠。
  7. 积极批处理 — 相同材质可进行批处理,绘制调用对VR性能影响最大。

Patterns

设计模式示例

Reference Caching

引用缓存模式

csharp
public class PlayerController : MonoBehaviour
{
    private Rigidbody _rb;
    private Animator _animator;

    [SerializeField] private Transform _cameraTarget;

    private void Awake()
    {
        _rb = GetComponent<Rigidbody>();
        _animator = GetComponent<Animator>();
    }

    private void FixedUpdate()
    {
        _rb.AddForce(Vector3.up);
    }
}
csharp
public class PlayerController : MonoBehaviour
{
    private Rigidbody _rb;
    private Animator _animator;

    [SerializeField] private Transform _cameraTarget;

    private void Awake()
    {
        _rb = GetComponent<Rigidbody>();
        _animator = GetComponent<Animator>();
    }

    private void FixedUpdate()
    {
        _rb.AddForce(Vector3.up);
    }
}

Event System (ScriptableObject)

事件系统(基于ScriptableObject)

csharp
[CreateAssetMenu(menuName = "Events/Game Event")]
public class GameEvent : ScriptableObject
{
    private readonly List<GameEventListener> _listeners = new();

    public void Raise()
    {
        for (int i = _listeners.Count - 1; i >= 0; i--)
            _listeners[i].OnEventRaised();
    }

    public void RegisterListener(GameEventListener listener) =>
        _listeners.Add(listener);

    public void UnregisterListener(GameEventListener listener) =>
        _listeners.Remove(listener);
}

public class GameEventListener : MonoBehaviour
{
    [SerializeField] private GameEvent _event;
    [SerializeField] private UnityEvent _response;

    private void OnEnable() => _event.RegisterListener(this);
    private void OnDisable() => _event.UnregisterListener(this);
    public void OnEventRaised() => _response.Invoke();
}
csharp
[CreateAssetMenu(menuName = "Events/Game Event")]
public class GameEvent : ScriptableObject
{
    private readonly List<GameEventListener> _listeners = new();

    public void Raise()
    {
        for (int i = _listeners.Count - 1; i >= 0; i--)
            _listeners[i].OnEventRaised();
    }

    public void RegisterListener(GameEventListener listener) =>
        _listeners.Add(listener);

    public void UnregisterListener(GameEventListener listener) =>
        _listeners.Remove(listener);
}

public class GameEventListener : MonoBehaviour
{
    [SerializeField] private GameEvent _event;
    [SerializeField] private UnityEvent _response;

    private void OnEnable() => _event.RegisterListener(this);
    private void OnDisable() => _event.UnregisterListener(this);
    public void OnEventRaised() => _response.Invoke();
}

Null-Safe Pattern

空值安全模式

csharp
// Unity overloads == for destroyed objects
if (_target != null)
{
    _target.DoSomething();
}

// Best: explicit destroyed check
if (_target != null && !_target.Equals(null))
{
    _target.DoSomething();
}
csharp
// Unity为已销毁对象重载了==运算符
if (_target != null)
{
    _target.DoSomething();
}

// 最佳实践:显式检查销毁状态
if (_target != null && !_target.Equals(null))
{
    _target.DoSomething();
}

Coroutine Pattern

协程模式

csharp
private IEnumerator FadeOut(float duration)
{
    float elapsed = 0f;
    Color startColor = _renderer.material.color;

    while (elapsed < duration)
    {
        elapsed += Time.deltaTime;
        float t = elapsed / duration;
        _renderer.material.color = Color.Lerp(startColor, Color.clear, t);
        yield return null;
    }

    gameObject.SetActive(false);
}
csharp
private IEnumerator FadeOut(float duration)
{
    float elapsed = 0f;
    Color startColor = _renderer.material.color;

    while (elapsed < duration)
    {
        elapsed += Time.deltaTime;
        float t = elapsed / duration;
        _renderer.material.color = Color.Lerp(startColor, Color.clear, t);
        yield return null;
    }

    gameObject.SetActive(false);
}

Object Pooling

对象池模式

csharp
public class ObjectPool : MonoBehaviour
{
    [SerializeField] private GameObject _prefab;
    [SerializeField] private int _initialSize = 10;

    private Queue<GameObject> _pool = new();

    private void Awake()
    {
        for (int i = 0; i < _initialSize; i++)
        {
            var obj = Instantiate(_prefab);
            obj.SetActive(false);
            _pool.Enqueue(obj);
        }
    }

    public GameObject Get()
    {
        if (_pool.Count == 0)
            return Instantiate(_prefab);

        var pooled = _pool.Dequeue();
        pooled.SetActive(true);
        return pooled;
    }

    public void Return(GameObject obj)
    {
        obj.SetActive(false);
        _pool.Enqueue(obj);
    }
}
csharp
public class ObjectPool : MonoBehaviour
{
    [SerializeField] private GameObject _prefab;
    [SerializeField] private int _initialSize = 10;

    private Queue<GameObject> _pool = new();

    private void Awake()
    {
        for (int i = 0; i < _initialSize; i++)
        {
            var obj = Instantiate(_prefab);
            obj.SetActive(false);
            _pool.Enqueue(obj);
        }
    }

    public GameObject Get()
    {
        if (_pool.Count == 0)
            return Instantiate(_prefab);

        var pooled = _pool.Dequeue();
        pooled.SetActive(true);
        return pooled;
    }

    public void Return(GameObject obj)
    {
        obj.SetActive(false);
        _pool.Enqueue(obj);
    }
}

Static Batching

静态批处理

csharp
gameObject.isStatic = true;

// Or specific flags
GameObjectUtility.SetStaticEditorFlags(gameObject,
    StaticEditorFlags.BatchingStatic |
    StaticEditorFlags.OcclusionStatic);

csharp
gameObject.isStatic = true;

// 或设置特定标记
GameObjectUtility.SetStaticEditorFlags(gameObject,
    StaticEditorFlags.BatchingStatic |
    StaticEditorFlags.OcclusionStatic);

VR Performance Targets

VR性能指标

MetricQuest 2Quest 3PC VR
Draw Calls<100<150<200
Triangles<100K<150K<1M
Frame Time<14ms (72fps)<11ms (90fps)<11ms (90fps)
Texture Memory<200MB<500MB<1GB
指标Quest 2Quest 3PC VR
绘制调用<100<150<200
三角面数<100K<150K<1M
帧时间<14ms (72fps)<11ms (90fps)<11ms (90fps)
纹理内存<200MB<500MB<1GB

LOD Configuration

LOD配置

LOD 0: 100% triangles (0-10m)
LOD 1: 50% triangles (10-25m)
LOD 2: 25% triangles (25-50m)
Culled: 0 triangles (50m+)
LOD 0: 100%三角面数(0-10米)
LOD 1: 50%三角面数(10-25米)
LOD 2: 25%三角面数(25-50米)
剔除: 0三角面数(50米以外)

Material Atlasing

材质图集优化

Before: 20 objects x 20 materials = 20 draw calls (no batching)
After:  20 objects x 1 atlas material = 1 draw call (batched)

优化前:20个对象 × 20种材质 = 20次绘制调用(无法批处理)
优化后:20个对象 × 1种图集材质 = 1次绘制调用(可批处理)

Anti-Patterns

反模式

Anti-PatternProblemFix
GetComponent
in Update
Expensive every frameCache in Awake
Find
or
FindObjectOfType
Slow, fragileInject references or use events
Heavy Update loopsPerformance drainUse events, coroutines, or FixedUpdate
String comparisons for tagsTypo-prone, slowUse CompareTag or constants
Public fields for everythingNo encapsulationUse [SerializeField] private
Unique material per objectNo batching possibleShare materials, use atlases
No LODsFull detail at any distanceAdd LOD groups
Instantiate/Destroy in gameplayGC spikes, stuttersObject pooling
Realtime lights everywhereExpensive shadowsBake lighting, limit realtime
No occlusion cullingRender hidden objectsBake occlusion data

反模式问题修复方案
Update
中调用
GetComponent
每帧开销大在Awake中缓存引用
使用
Find
FindObjectOfType
速度慢、易出错注入引用或使用事件
繁重的Update循环性能消耗大使用事件、协程或FixedUpdate
使用字符串比较标签易拼写错误、速度慢使用CompareTag或常量
所有字段设为Public无封装性使用[SerializeField] private
每个对象使用唯一材质无法进行批处理共享材质、使用材质图集
未使用LOD任何距离都渲染全细节添加LOD组
游戏运行时调用Instantiate/DestroyGC突增、卡顿使用对象池
大量使用实时光照阴影开销大烘焙光照、限制实时光照数量
未启用遮挡剔除渲染隐藏对象烘焙遮挡数据

Checklist

检查清单

Code Quality

代码质量

  • References cached in Awake
  • No GetComponent in Update/FixedUpdate
  • No Find methods in runtime code
  • ScriptableObjects for shared data
  • Events for decoupled communication
  • Null checks for destroyable objects
  • 已在Awake中缓存引用
  • 未在Update/FixedUpdate中调用GetComponent
  • 运行时代码中未使用Find方法
  • 已使用ScriptableObject存储共享数据
  • 已使用事件实现解耦通信
  • 对可销毁对象进行了空值检查

Performance

性能优化

  • Static objects marked static
  • Materials shared where possible
  • Texture atlases for small props
  • LOD groups on significant meshes
  • Occlusion culling baked
  • Object pooling for spawned objects
  • Lighting baked (not all realtime)
  • 静态对象已标记为Static
  • 尽可能共享材质
  • 小道具已使用纹理图集
  • 重要网格已添加LOD组
  • 已烘焙遮挡剔除数据
  • 生成对象已使用对象池
  • 光照已烘焙(未全部使用实时光照)

Profiling

性能分析

  • Frame Debugger checked for draw calls
  • Profiler run for CPU spikes
  • Memory Profiler checked for leaks
  • Tested on target device (not just editor)

  • 已使用Frame Debugger检查绘制调用
  • 已使用Profiler排查CPU峰值
  • 已使用Memory Profiler检查内存泄漏
  • 已在目标设备测试(而非仅编辑器)

References

参考资料

  • references/lifecycle.md
    — MonoBehaviour lifecycle and execution order
  • references/profiling.md
    — Unity Profiler usage and interpretation
  • references/lifecycle.md
    — MonoBehaviour生命周期与执行顺序
  • references/profiling.md
    — Unity Profiler使用与解读

Assets

相关资源

  • assets/component-checklist.md
    — Unity component design checklist
  • assets/vr-performance-limits.md
    — VR platform performance limits and targets
  • assets/component-checklist.md
    — Unity组件设计检查清单
  • assets/vr-performance-limits.md
    — VR平台性能限制与指标