mcpField
U
Unity Architecture

Unity Architecture

github.com/cryptorabea开发3142 次安装45 周前更新
代码仓库

关于 Unity Architecture

介绍可维护 Unity 项目的架构模式,面向 Unity 游戏开发者,帮助组织系统、数据与通信,并保持组件易于测试、低耦合。

主要功能

  1. 01管理器与服务定位器模式
  2. 02基于 ScriptableObject 的数据设计
  3. 03事件驱动通信模式
  4. 04组件组合与状态机
  5. 05MVC/MVP 分层

适用场景

  1. 01组织游戏全局系统
  2. 02跨场景共享游戏数据
  3. 03解耦游戏逻辑与 UI 组件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/cryptorabea/claude_unity_dev_plugin --skill unity-architecture

安装后在 Claude Code 中输入/unity-architecture确认已加载。

附带文件references/manager-patterns.mdreferences/scriptableobject-architecture.md

详细介绍

Unity 游戏架构

适用于可扩展、易维护 Unity 项目的核心架构模式和设计原则。

概述

良好的架构能够分离关注点、降低耦合,并使代码易于测试和维护。本技能介绍经过验证的 Unity 游戏开发模式。

核心架构概念:

  • 管理器模式和全局系统
  • 基于 ScriptableObject 的数据架构
  • 事件驱动通信
  • 组件组合模式
  • 依赖管理

管理器模式

用于协调全局游戏功能的集中式系统。

单例管理器

最常见的全局管理器模式:

public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    public void StartGame() { }
    public void PauseGame() { }
    public void EndGame() { }
}

// Access from anywhere
public class Player : MonoBehaviour
{
    private void Start()
    {
        GameManager.Instance.StartGame();
    }
}

适用场景:

  • 游戏状态管理(GameManager)
  • 音频管理(AudioManager)
  • 输入管理(InputManager)
  • 存档/读档系统(SaveManager)
  • UI 管理(UIManager)

不适用场景:

  • 所有情况(避免“单例地狱”)
  • 临时系统
  • 需要多个实例的系统
泛型单例基类

可复用的单例模式:

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;

    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<T>();

                if (instance == null)
                {
                    GameObject singleton = new GameObject(typeof(T).Name);
                    instance = singleton.AddComponent<T>();
                    DontDestroyOnLoad(singleton);
                }
            }

            return instance;
        }
    }

    protected virtual void Awake()
    {
        if (instance == null)
        {
            instance = this as T;
            DontDestroyOnLoad(gameObject);
        }
        else if (instance != this)
        {
            Destroy(gameObject);
        }
    }
}

// Usage
public class GameManager : Singleton<GameManager>
{
    protected override void Awake()
    {
        base.Awake();
        // Additional initialization
    }
}
管理器初始化顺序

控制管理器初始化:

// Use Script Execution Order:
// Edit > Project Settings > Script Execution Order

// Or explicit initialization
public class GameBootstrap : MonoBehaviour
{
    private void Awake()
    {
        InitializeManagers();
    }

    private void InitializeManagers()
    {
        // Initialize in specific order
        var saveManager = SaveManager.Instance;
        var audioManager = AudioManager.Instance;
        var gameManager = GameManager.Instance;

        // Managers initialize in Awake, but access here ensures order
    }
}

最佳实践:使用显式初始化场景或引导程序。

服务定位器模式

用于依赖注入的单例替代方案:

public class ServiceLocator
{
    private static ServiceLocator instance;
    public static ServiceLocator Instance => instance ?? (instance = new ServiceLocator());

    private readonly Dictionary<Type, object> services = new Dictionary<Type, object>();

    public void RegisterService<T>(T service)
    {
        services[typeof(T)] = service;
    }

    public T GetService<T>()
    {
        if (services.TryGetValue(typeof(T), out var service))
        {
            return (T)service;
        }

        throw new Exception($"Service {typeof(T)} not found");
    }
}

// Register services
public class GameBootstrap : MonoBehaviour
{
    private void Awake()
    {
        var audioManager = new AudioManager();
        ServiceLocator.Instance.RegisterService(audioManager);

        var saveManager = new SaveManager();
        ServiceLocator.Instance.RegisterService(saveManager);
    }
}

// Access services
public class Player : MonoBehaviour
{
    private void Start()
    {
        var audio = ServiceLocator.Instance.GetService<AudioManager>();
        audio.PlaySound("Jump");
    }
}

相较于单例的优点:

  • 可测试(可注入模拟服务)
  • 无静态依赖
  • 依赖关系清晰

缺点:

  • 需要编写更多设置代码
  • 运行时字典查找
  • 不易发现

ScriptableObject 架构

使用 ScriptableObject 的数据驱动设计。

ScriptableObject 数据容器

将数据与行为分开存储:

[CreateAssetMenu(fileName = "WeaponData", menuName = "Game/Weapon Data")]
public class WeaponData : ScriptableObject
{
    public string weaponName;
    public int damage;
    public float fireRate;
    public Sprite icon;
    public GameObject projectilePrefab;
}

public class Weapon : MonoBehaviour
{
    [SerializeField] private WeaponData data;

    public void Fire()
    {
        Instantiate(data.projectilePrefab, firePoint.position, firePoint.rotation);
    }

    public int GetDamage() => data.damage;
}

优点:

  • 数据与代码分离
  • 在多个场景间共享数据
  • 无需更改代码即可编辑
  • 方便设计师使用

适用场景:

  • 物品/武器属性
  • 角色数据
  • 游戏平衡数值
  • 配置
ScriptableObject 事件

使用 ScriptableObject 的事件系统:

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

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

    public void RegisterListener(GameEventListener listener)
    {
        if (!listeners.Contains(listener))
            listeners.Add(listener);
    }

    public void UnregisterListener(GameEventListener listener)
    {
        listeners.Remove(listener);
    }
}

public class GameEventListener : MonoBehaviour
{
    [SerializeField] private GameEvent gameEvent;
    [SerializeField] private UnityEvent response;

    private void OnEnable()
    {
        gameEvent.RegisterListener(this);
    }

    private void OnDisable()
    {
        gameEvent.UnregisterListener(this);
    }

    public void OnEventRaised()
    {
        response.Invoke();
    }
}

用法:

Create GameEvent asset: "OnPlayerDeath"
Attach GameEventListener to UI
Configure response: Show death screen
Raise event when player dies

优点:

  • 设计师可直接使用
  • 可在 Inspector 中可视化连接
  • 系统间解耦
  • 事件可复用
ScriptableObject 变量

在多个场景间共享变量:

public abstract class ScriptableVariable<T> : ScriptableObject
{
    [SerializeField] private T value;

    public T Value
    {
        get => value;
        set
        {
            this.value = value;
            OnValueChanged?.Invoke(value);
        }
    }

    public event Action<T> OnValueChanged;
}

[CreateAssetMenu(fileName = "IntVariable", menuName = "Variables/Int")]
public class IntVariable : ScriptableVariable<int> { }

[CreateAssetMenu(fileName = "FloatVariable", menuName = "Variables/Float")]
public class FloatVariable : ScriptableVariable<float> { }

用法:

public class Player : MonoBehaviour
{
    [SerializeField] private IntVariable playerHealth;

    public void TakeDamage(int damage)
    {
        playerHealth.Value -= damage;  // Updates all subscribers
    }
}

public class HealthUI : MonoBehaviour
{
    [SerializeField] private IntVariable playerHealth;
    [SerializeField] private Text healthText;

    private void OnEnable()
    {
        playerHealth.OnValueChanged += UpdateUI;
        UpdateUI(playerHealth.Value);
    }

    private void OnDisable()
    {
        playerHealth.OnValueChanged -= UpdateUI;
    }

    private void UpdateUI(int health)
    {
        healthText.text = $"Health: {health}";
    }
}

优点:

  • 在多个场景间持久保存
  • 支持多个监听器
  • 可在 Inspector 中编辑
  • 运行时更改会反映到所有位置

事件系统

组件间的解耦通信。

C# 事件

标准 C# 事件模式:

public class Health : MonoBehaviour
{
    public event Action<int> OnHealthChanged;
    public event Action OnDeath;

    private int health = 100;

    public void TakeDamage(int damage)
    {
        health -= damage;
        OnHealthChanged?.Invoke(health);

        if (health <= 0)
        {
            OnDeath?.Invoke();
        }
    }
}

public class HealthUI : MonoBehaviour
{
    [SerializeField] private Health playerHealth;

    private void OnEnable()
    {
        playerHealth.OnHealthChanged += UpdateHealthBar;
        playerHealth.OnDeath += ShowDeathScreen;
    }

    private void OnDisable()
    {
        playerHealth.OnHealthChanged -= UpdateHealthBar;
        playerHealth.OnDeath -= ShowDeathScreen;
    }

    private void UpdateHealthBar(int health) { }
    private void ShowDeathScreen() { }
}

重要:务必在 OnDisable 中取消订阅,以防止内存泄漏。

UnityEvent

可在 Inspector 中分配的事件:

public class Interactable : MonoBehaviour
{
    [SerializeField] private UnityEvent onInteract;
    [SerializeField] private UnityEvent<int> onScoreChanged;

    public void Interact()
    {
        onInteract?.Invoke();
    }

    public void AddScore(int points)
    {
        onScoreChanged?.Invoke(points);
    }
}

优点:

  • 设计师可在 Inspector 中使用
  • 简单交互无需编写代码
  • 可视化连接

缺点:

  • 比 C# 事件慢
  • 无编译时检查
  • 更难调试

适用场景:

  • 简单交互
  • 由设计师驱动的事件
  • 原型开发
全局事件总线

集中式事件系统:

public static class EventBus
{
    private static readonly Dictionary<Type, Delegate> eventTable = new Dictionary<Type, Delegate>();

    public static void Subscribe<T>(Action<T> handler)
    {
        if (eventTable.TryGetValue(typeof(T), out var existingHandler))
        {
            eventTable[typeof(T)] = Delegate.Combine(existingHandler, handler);
        }
        else
        {
            eventTable[typeof(T)] = handler;
        }
    }

    public static void Unsubscribe<T>(Action<T> handler)
    {
        if (eventTable.TryGetValue(typeof(T), out var existingHandler))
        {
            var newHandler = Delegate.Remove(existingHandler, handler);
            if (newHandler == null)
                eventTable.Remove(typeof(T));
            else
                eventTable[typeof(T)] = newHandler;
        }
    }

    public static void Publish<T>(T eventData)
    {
        if (eventTable.TryGetValue(typeof(T), out var handler))
        {
            (handler as Action<T>)?.Invoke(eventData);
        }
    }
}

// Event data types
public struct PlayerDiedEvent
{
    public Vector3 position;
    public string killedBy;
}

// Subscribe
public class DeathUI : MonoBehaviour
{
    private void OnEnable()
    {
        EventBus.Subscribe<PlayerDiedEvent>(OnPlayerDied);
    }

    private void OnDisable()
    {
        EventBus.Unsubscribe<PlayerDiedEvent>(OnPlayerDied);
    }

    private void OnPlayerDied(PlayerDiedEvent data)
    {
        ShowDeathScreen(data.position, data.killedBy);
    }
}

// Publish
public class Player : MonoBehaviour
{
    private void Die()
    {
        EventBus.Publish(new PlayerDiedEvent
        {
            position = transform.position,
            killedBy = "Enemy"
        });
    }
}

优点:

  • 完全解耦
  • 无需直接引用
  • 使用结构体时具备类型安全

缺点:

  • 运行时开销(字典查找)
  • 更难追踪事件流
  • 忘记取消订阅时存在内存泄漏风险

组件组合模式

策略模式

可互换的行为:

public interface IMovementStrategy
{
    void Move(Transform transform, float speed);
}

public class GroundMovement : MonoBehaviour, IMovementStrategy
{
    public void Move(Transform transform, float speed)
    {
        // Ground-based movement
    }
}

public class FlyingMovement : MonoBehaviour, IMovementStrategy
{
    public void Move(Transform transform, float speed)
    {
        // Flying movement
    }
}

public class Character : MonoBehaviour
{
    [SerializeField] private float speed = 5f;
    private IMovementStrategy movementStrategy;

    private void Awake()
    {
        movementStrategy = GetComponent<IMovementStrategy>();
    }

    private void Update()
    {
        movementStrategy.Move(transform, speed);
    }
}

优点:

  • 可在运行时切换行为
  • 策略可复用
  • 开闭原则
状态机模式

管理对象状态:

public interface IState
{
    void Enter();
    void Execute();
    void Exit();
}

public class IdleState : MonoBehaviour, IState
{
    public void Enter() => Debug.Log("Entering Idle");
    public void Execute() { }
    public void Exit() => Debug.Log("Exiting Idle");
}

public class StateMachine : MonoBehaviour
{
    private IState currentState;

    public void ChangeState(IState newState)
    {
        currentState?.Exit();
        currentState = newState;
        currentState?.Enter();
    }

    private void Update()
    {
        currentState?.Execute();
    }
}

public class Enemy : MonoBehaviour
{
    private StateMachine stateMachine;
    private IdleState idleState;
    private ChaseState chaseState;

    private void Awake()
    {
        stateMachine = GetComponent<StateMachine>();
        idleState = GetComponent<IdleState>();
        chaseState = GetComponent<ChaseState>();

        stateMachine.ChangeState(idleState);
    }

    public void OnPlayerSpotted()
    {
        stateMachine.ChangeState(chaseState);
    }
}
观察者模式

一对多通知:

public interface IObserver
{
    void OnNotify(string eventType);
}

public class Subject : MonoBehaviour
{
    private readonly List<IObserver> observers = new List<IObserver>();

    public void Attach(IObserver observer)
    {
        if (!observers.Contains(observer))
            observers.Add(observer);
    }

    public void Detach(IObserver observer)
    {
        observers.Remove(observer);
    }

    protected void Notify(string eventType)
    {
        for (int i = observers.Count - 1; i >= 0; i--)
        {
            observers[i].OnNotify(eventType);
        }
    }
}

public class Player : Subject
{
    public void Jump()
    {
        Notify("PlayerJumped");
    }
}

public class AudioObserver : MonoBehaviour, IObserver
{
    [SerializeField] private Player player;

    private void OnEnable()
    {
        player.Attach(this);
    }

    private void OnDisable()
    {
        player.Detach(this);
    }

    public void OnNotify(string eventType)
    {
        if (eventType == "PlayerJumped")
            PlayJumpSound();
    }
}

Unity 中的 MVC/MVP

模型-视图-控制器

分离数据、表现和逻辑:

// Model - Data
public class PlayerModel
{
    public int Health { get; private set; } = 100;
    public int Score { get; private set; } = 0;

    public event Action<int> OnHealthChanged;
    public event Action<int> OnScoreChanged;

    public void TakeDamage(int damage)
    {
        Health -= damage;
        OnHealthChanged?.Invoke(Health);
    }

    public void AddScore(int points)
    {
        Score += points;
        OnScoreChanged?.Invoke(Score);
    }
}

// View - Presentation
public class PlayerView : MonoBehaviour
{
    [SerializeField] private Text healthText;
    [SerializeField] private Text scoreText;

    public void UpdateHealth(int health)
    {
        healthText.text = $"Health: {health}";
    }

    public void UpdateScore(int score)
    {
        scoreText.text = $"Score: {score}";
    }
}

// Controller - Logic
public class PlayerController : MonoBehaviour
{
    private PlayerModel model;
    private PlayerView view;

    private void Awake()
    {
        model = new PlayerModel();
        view = GetComponent<PlayerView>();

        model.OnHealthChanged += view.UpdateHealth;
        model.OnScoreChanged += view.UpdateScore;
    }

    private void OnDestroy()
    {
        model.OnHealthChanged -= view.UpdateHealth;
        model.OnScoreChanged -= view.UpdateScore;
    }

    public void TakeDamage(int damage)
    {
        model.TakeDamage(damage);
    }

    public void AddScore(int points)
    {
        model.AddScore(points);
    }
}

优点:

  • 可测试(可模拟模型/视图)
  • 组件可复用
  • 分工清晰

适用场景:

  • 复杂 UI
  • 需要可测试的代码
  • 同一数据存在多个视图

其他资源

参考文件

有关详细的架构模式,请参阅:

  • references/manager-patterns.md - Manager 的实现、初始化和通信
  • references/scriptableobject-architecture.md - 高级 SO 模式、运行时集合和变量
  • references/event-systems.md - 事件模式、消息总线和发布-订阅系统
  • references/design-patterns.md - Unity 中的工厂、对象池、命令和策略模式

最佳实践

✅ 应该:

  • 使用 Manager 管理全局系统(音频、输入、存档)
  • 利用 ScriptableObjects 存储数据
  • 实现事件驱动通信
  • 优先采用组合而非继承
  • 始终在 OnDisable 中取消订阅事件
  • 使用依赖注入以便于测试
  • 让 Manager 专注于单一职责

❌ 不应该:

  • 把所有东西都做成单例
  • 创建上帝 Manager(应只处理一个关注点)
  • 忘记取消订阅事件
  • 硬编码依赖项
  • 什么都用单例
  • 混合数据和呈现
  • 创建深层继承层次结构

黄金法则:为变化而设计。松耦合、高内聚的系统更易于维护、测试和扩展。


应用这些架构模式,构建可扩展、易维护且经得起时间考验的 Unity 项目。

评论

支持 Markdown