mcpField
U
unity-game-architecture

unity-game-architecture

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

关于 unity-game-architecture

面向 Unity 项目的游戏系统架构决策技能,帮助开发者根据项目规模和需求选择设计模式。内容涵盖服务访问、类职责、组件组合、事件通信和启动流程,并指出常见架构反模式。

主要功能

  1. 01全局服务访问方案比较
  2. 02MonoBehaviour 与普通 C# 类的取舍
  3. 03组件组合与继承决策
  4. 04事件架构选择
  5. 05管理器启动顺序方案
  6. 06架构反模式与替代方式

适用场景

  1. 01为系统依赖选择服务定位器或依赖注入
  2. 02将纯逻辑与 Unity 生命周期代码分离
  3. 03设计跨场景事件通信
  4. 04规划多个管理器的启动流程

安装

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

$npx skills add https://github.com/nice-wolf-studio/unity-claude-skills --skill unity-game-architecture

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

附带文件references/architecture-patterns.md

详细介绍

游戏系统架构——决策模式

前置技能: unity-scripting(MonoBehaviour、ScriptableObjects、事件)、unity-lifecycle(RuntimeInitializeOnLoadMethod、DontDestroyOnLoad、SubsystemRegistration)、unity-foundations(组件、GameObject)

这些模式针对最常见的架构失败问题:Claude 会默认使用臃肿的 MonoBehaviour、直接引用,并到处使用单例。这适用于原型,但在系统规模下会崩溃。


模式:全局服务访问

适用场景:系统需要在运行时查找其他系统(AudioManager、InputManager、SaveManager 等)

决策:

  • 延迟单例——小型项目,只有 1-3 个管理器,无需测试。static Instance + DontDestroyOnLoad。编写速度快,但无法模拟。
  • 服务定位器——中型项目,希望能为测试替换实现(模拟音频、存档桩)。使用带有 Register<T>/Get<T> 的中央注册表。依赖关系是隐式的,但可以替换。
  • 构造函数/方法 DI——大型项目或库代码,追求最高可测试性。显式传递依赖。使用 VContainer 或 Zenject 为 MonoBehaviour 注入依赖。

脚手架(服务定位器):

public static class Services
{
    private static readonly Dictionary<Type, object> _services = new();

    public static void Register<T>(T service) where T : class
    {
        _services[typeof(T)] = service;
    }

    public static T Get<T>() where T : class
    {
        if (_services.TryGetValue(typeof(T), out var service))
            return (T)service;
        throw new InvalidOperationException($"Service {typeof(T).Name} not registered");
    }

    public static bool TryGet<T>(out T service) where T : class
    {
        if (_services.TryGetValue(typeof(T), out var obj))
        {
            service = (T)obj;
            return true;
        }
        service = null;
        return false;
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void Reset() => _services.Clear();
}

// Registration (in a bootstrap MonoBehaviour or RuntimeInitializeOnLoadMethod):
Services.Register<IAudioService>(new AudioService());
Services.Register<ISaveService>(new SaveService());

// Usage (anywhere):
Services.Get<IAudioService>().PlaySFX("explosion");

注意:服务定位器会隐藏依赖——无法通过查看类的构造函数来了解它需要什么。仅将其用于真正的基础设施服务(音频、存档、分析),不要用于游戏玩法依赖。始终通过接口(IAudioService,而非 AudioManager)提供服务,以便测试可以注册模拟对象。对于禁用域重载的 Enter Play Mode Options,SubsystemRegistration 重置至关重要。


模式:MonoBehaviour 与普通 C# 类

适用场景:决定一个新类是否应继承 MonoBehaviour

决策:

  • MonoBehaviour——需要以下任一功能:通过 [SerializeField] 在 Inspector 中序列化、Unity 回调(Update、OnTriggerEnter)、访问 Transform/GameObject、协程/带有 destroyCancellationToken 的 Awaitable,或作为 GameObject 上的组件。
  • 普通 C# 类——纯逻辑:状态机、寻路算法、数据处理、物品栏逻辑、伤害计算、存档/加载 DTO。由 MonoBehaviour 持有并驱动。

脚手架(由 MonoBehaviour 持有的普通 C# 类):

// Pure logic class -- testable without Unity
public class HealthSystem
{
    public int Current { get; private set; }
    public int Max { get; }
    public bool IsDead => Current <= 0;
    public event Action OnDied;
    public event Action<int, int> OnChanged; // current, max

    public HealthSystem(int maxHealth)
    {
        Max = maxHealth;
        Current = maxHealth;
    }

    public void TakeDamage(int amount)
    {
        if (IsDead) return;
        Current = Mathf.Max(0, Current - amount);
        OnChanged?.Invoke(Current, Max);
        if (IsDead) OnDied?.Invoke();
    }

    public void Heal(int amount)
    {
        if (IsDead) return;
        Current = Mathf.Min(Max, Current + amount);
        OnChanged?.Invoke(Current, Max);
    }
}

// Thin MonoBehaviour wrapper -- bridges Unity and logic
public class HealthComponent : MonoBehaviour
{
    [SerializeField] private int maxHealth = 100;

    public HealthSystem Health { get; private set; }

    void Awake()
    {
        Health = new HealthSystem(maxHealth);
    }

    // Optional: expose events for Unity-side wiring
    void OnEnable() => Health.OnDied += HandleDeath;
    void OnDisable() => Health.OnDied -= HandleDeath;

    void HandleDeath()
    {
        // Unity-specific: play VFX, disable collider, etc.
        Destroy(gameObject, 2f);
    }
}

注意:普通 C# 类不能使用 [SerializeField]。使用 [System.Serializable] 在 Inspector 中显示嵌套内容。它们没有 destroyCancellationToken——如果需要执行异步工作,请从持有它的 MonoBehaviour 传入一个。普通 C# 类引用 UnityEngine 时,应使用 Mathf(而非 System.Math),以确保数学运算与 Unity 兼容。


模式:组件组合与继承

适用场景:多个 GameObject 共享某些行为,但细节有所不同(具有不同攻击方式的敌人、可交互对象)

决策:

  • 通过接口进行组合(默认)——将能力拆分为专用组件(Health、Mover、DamageDealer)。使用者通过 GetComponent<IDamageable>() 查询。灵活性最高,易于混搭。
  • 抽象基类——仅在确实存在共享状态和共享实现的“是一个”关系时使用(而非仅共享接口)。示例:Projectile 基类及其 BulletProjectile 和 RocketProjectile,它们共享速度/生命周期逻辑。

脚手架(接口 + 组合):

// Capability interface
public interface IDamageable
{
    void TakeDamage(float amount, Vector3 hitPoint, Vector3 hitNormal);
    bool IsAlive { get; }
}

// Focused component implementing the interface
[RequireComponent(typeof(Collider))]
public class DamageReceiver : MonoBehaviour, IDamageable
{
    [SerializeField] private float maxHealth = 100f;
    private float _currentHealth;

    public bool IsAlive => _currentHealth > 0;
    public event Action<float> OnDamaged;

    void Awake() => _currentHealth = maxHealth;

    public void TakeDamage(float amount, Vector3 hitPoint, Vector3 hitNormal)
    {
        if (!IsAlive) return;
        _currentHealth -= amount;
        OnDamaged?.Invoke(amount);
    }
}

// Consumer queries the interface, not the concrete type
void OnTriggerEnter(Collider other)
{
    if (other.TryGetComponent(out IDamageable target) && target.IsAlive)
    {
        target.TakeDamage(damage, transform.position, transform.forward);
    }
}

注意:GetComponent<IInterface>() 在 Unity 中可用——可以查询接口。RequireComponent 会在添加时强制要求组件依赖。优先使用 TryGetComponent,而不是对 GetComponent 结果进行空值检查(在较旧的 Unity 版本中可避免分配空包装器)。深层 MonoBehaviour 继承层次(3 层以上)是 Unity 架构反模式中的头号问题。


模式:事件架构选择

适用场景:两个或更多系统需要通信,但不希望存在直接引用

决策:

  • C# 事件/Action -- 在同一个类或同一 GameObject 上紧密耦合的组件之间通信。最简单、强类型、零开销。
  • ScriptableObject 事件通道 -- 跨场景通信,可由设计师配置,可在 Inspector 中通过拖放连接。最适合游戏玩法事件(玩家死亡、关卡完成)。完整实现请参见 unity-scripting/references/scriptableobjects.md。
  • 静态事件总线 -- 项目范围内的类型化事件,仅通过代码配置(无需管理资源)。原型开发速度快,适合系统级事件(场景已加载、设置已更改)。比 SO 通道更难调试。

脚手架代码(静态类型化事件总线):

// Event definitions
public struct PlayerDiedEvent { public Vector3 Position; public string CauseOfDeath; }
public struct ScoreChangedEvent { public int NewScore; public int Delta; }

// Bus implementation
public static class EventBus
{
    private static readonly Dictionary<Type, Delegate> _handlers = new();

    public static void Subscribe<T>(Action<T> handler) where T : struct
    {
        var type = typeof(T);
        if (_handlers.TryGetValue(type, out var existing))
            _handlers[type] = Delegate.Combine(existing, handler);
        else
            _handlers[type] = handler;
    }

    public static void Unsubscribe<T>(Action<T> handler) where T : struct
    {
        var type = typeof(T);
        if (_handlers.TryGetValue(type, out var existing))
        {
            var result = Delegate.Remove(existing, handler);
            if (result == null) _handlers.Remove(type);
            else _handlers[type] = result;
        }
    }

    public static void Publish<T>(T evt) where T : struct
    {
        if (_handlers.TryGetValue(typeof(T), out var handler))
            ((Action<T>)handler)?.Invoke(evt);
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void Reset() => _handlers.Clear();
}

// Usage
EventBus.Subscribe<PlayerDiedEvent>(OnPlayerDied);
EventBus.Publish(new PlayerDiedEvent { Position = pos, CauseOfDeath = "lava" });

注意事项:始终在 OnDisable(而非 OnDestroy)中取消订阅,以匹配 OnEnable 中的订阅。静态事件总线会在场景加载期间保持存活——如果处理程序所属对象被销毁时未取消订阅,就会触发 MissingReferenceException。使用 struct 事件以避免分配内存。若设计师需要通过可视化方式连接事件,优先使用 SO 事件通道。


模式:管理器引导顺序

适用场景:多个管理器系统在启动时相互依赖

决策:

  • 引导场景 -- 首先加载专用的“Boot”场景,创建所有管理器,然后以叠加方式加载游戏场景。可视化、支持异步操作,并支持加载画面。最适合用于正式项目。
  • RuntimeInitializeOnLoadMethod -- 纯代码引导,不依赖场景。通过 BeforeSceneLoad 创建管理器。最适合插件、程序包,或任何场景都可能作为入口点的项目。

脚手架代码(引导场景):

public class Bootstrapper : MonoBehaviour
{
    [SerializeField] private string firstGameplayScene = "MainMenu";

    async Awaitable Start()
    {
        // Create persistent managers
        DontDestroyOnLoad(gameObject);

        // Initialize services in dependency order
        var audio = gameObject.AddComponent<AudioService>();
        var save = gameObject.AddComponent<SaveService>();

        Services.Register<IAudioService>(audio);
        Services.Register<ISaveService>(save);

        // Load saved settings before gameplay
        await save.LoadSettingsAsync(destroyCancellationToken);

        // Load the first gameplay scene (replaces Boot scene)
        await SceneManager.LoadSceneAsync(firstGameplayScene);
    }
}

脚手架代码(RuntimeInitializeOnLoadMethod):

public static class GameBootstrap
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void Bootstrap()
    {
        // Create a persistent GameObject for managers
        var go = new GameObject("[Services]");
        Object.DontDestroyOnLoad(go);

        var audio = go.AddComponent<AudioService>();
        Services.Register<IAudioService>(audio);

        // NOTE: Cannot use async/await here -- this is a static method
        // For async init, have a manager use async Awaitable Start()
    }
}

注意事项:RuntimeInitializeOnLoadMethod 不能使用 await(它是静态同步方法)。如果引导过程需要异步操作(加载配置、进行身份验证),请使用 BeforeSceneLoad 创建一个引导 MonoBehaviour,然后在其 Start() 中执行异步操作。采用引导场景方案时,必须将 Boot 场景作为场景 0 添加到 Build Settings。交叉参考:unity-lifecycle 会详细介绍 RuntimeInitializeOnLoadMethod 的时机。


架构反模式

反模式问题替代方案
巨型 MonoBehaviour(1000+ 行)无法测试、难以修改、容易发生合并冲突拆分为职责明确的组件和纯 C# 逻辑类
所有内容都使用单例耦合紧密、无法测试、依赖关系隐蔽基础设施使用 Service Locator,游戏玩法使用 DI
在 Update 中使用 FindObjectOfType每帧执行 O(n) 搜索,速度慢在 Start/Awake 中缓存引用,或使用事件/SO
管理器的管理器集中式瓶颈、循环依赖每个系统都独立向 Service Locator 注册
系统之间直接交叉引用任一系统被移除或替换时都会出错使用事件通道,或搭配接口使用 Service Locator
静态状态未在域重新加载时重置编辑器中不同运行会话之间会残留过期数据[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]

相关技能

  • unity-scripting/references/scriptableobjects.md -- SO 事件通道、运行时集合、变量引用(完整实现)
  • unity-lifecycle -- RuntimeInitializeOnLoadMethod 时机、SubsystemRegistration、DontDestroyOnLoad
  • unity-state-machines -- 遵循 MonoBehaviour 包装模式的纯 C# 类 FSM/BT
  • unity-testing -- Service Locator 如何支持在 Edit Mode 测试中使用模拟对象

其他资源

评论

支持 Markdown