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平台性能限制与指标