agentsclimarketplace

Unity game architecture

Skill IdoCohen560/claude-unity-game-studio/unity-knowledge-skills/skills/unity-game-architecture

AI-powered Unity game development studio for Claude Code - 49 agents, 120+ skills, and a full studio hierarchy that turns a single AI session into a coordinated game dev team. View more about me in my portfolio.

Install
npx -y skills add IdoCohen560/claude-unity-game-studio --skill unity-game-architecture

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 8 stars8 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Unity game architecture decision patterns. Service Locator vs Singleton vs DI, Event Bus vs ScriptableObject channels, MonoBehaviour vs plain C#, component composition, manager bootstrap sequences. DECISION format: WHEN/DECISION/SCAFFOLD/GOTCHA. Based on Unity 6.3 LTS.

SKILL.md

13.7 KB, ~2.9k tokens by cl100k_base, as published. Nobody here has run it

Game Systems Architecture -- Decision Patterns

Prerequisite skills: unity-scripting (MonoBehaviour, ScriptableObjects, events), unity-lifecycle (RuntimeInitializeOnLoadMethod, DontDestroyOnLoad, SubsystemRegistration), unity-foundations (components, GameObjects)

These patterns address the most common architectural failure: Claude defaults to fat MonoBehaviours with direct references and singletons everywhere. This works for prototypes but collapses at system scale.


PATTERN: Global Service Access

WHEN: Systems need to find other systems at runtime (AudioManager, InputManager, SaveManager, etc.)

DECISION:

  • Lazy Singleton -- Tiny project, 1-3 managers, no testing needed. static Instance + DontDestroyOnLoad. Fast to write, impossible to mock.
  • Service Locator -- Medium project, want to swap implementations for testing (mock audio, stub save). Central registry with Register<T>/Get<T>. Dependencies are implicit but swappable.
  • Constructor/Method DI -- Large project or library code, maximum testability. Pass dependencies explicitly. Use VContainer or Zenject for MonoBehaviour injection.

SCAFFOLD (Service Locator):

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");

GOTCHA: Service Locator hides dependencies -- you cannot see what a class needs by looking at its constructor. Use it for true infrastructure services only (audio, save, analytics), not for gameplay dependencies. Always back services with interfaces (IAudioService, not AudioManager) so tests can register mocks. The SubsystemRegistration reset is critical for Enter Play Mode Options with domain reload disabled.


PATTERN: MonoBehaviour vs Plain C# Class

WHEN: Deciding whether a new class should inherit from MonoBehaviour

DECISION:

  • MonoBehaviour -- Needs any of: Inspector serialization via [SerializeField], Unity callbacks (Update, OnTriggerEnter), Transform/GameObject access, coroutines/Awaitable with destroyCancellationToken, or being a component on a GameObject.
  • Plain C# class -- Pure logic: state machines, pathfinding algorithms, data processing, inventory logic, damage calculation, save/load DTOs. Owned and driven by a MonoBehaviour.

SCAFFOLD (Plain C# class owned by MonoBehaviour):

// 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);
    }
}

GOTCHA: Plain C# classes cannot use [SerializeField]. Use [System.Serializable] for nested display in the Inspector. They have no destroyCancellationToken -- pass one from the owning MonoBehaviour if they do async work. Use Mathf (not System.Math) for Unity-compatible math in plain C# classes that reference UnityEngine.


PATTERN: Component Composition vs Inheritance

WHEN: Multiple GameObjects share some behavior but differ in specifics (enemies with different attacks, interactable objects)

DECISION:

  • Composition with interfaces (default) -- Separate capabilities into focused components (Health, Mover, DamageDealer). Consumers query via GetComponent<IDamageable>(). Maximum flexibility, easy to mix-and-match.
  • Abstract base class -- Only when there is genuine IS-A relationship with shared STATE and shared IMPLEMENTATION (not just shared interface). Example: Projectile base with BulletProjectile and RocketProjectile that share velocity/lifetime logic.

SCAFFOLD (Interface + Composition):

// 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);
    }
}

GOTCHA: GetComponent<IInterface>() works in Unity -- interfaces are queryable. RequireComponent enforces component dependencies at add-time. Prefer TryGetComponent over null-checking GetComponent (avoids allocation of a null wrapper in older Unity versions). Deep MonoBehaviour inheritance hierarchies (3+ levels) are the #1 Unity architecture anti-pattern.


PATTERN: Event Architecture Selection

WHEN: Two or more systems need to communicate without direct references

DECISION:

  • C# events/Actions -- Communication within a single class or tightly-coupled components on the same GameObject. Simplest, strongly typed, zero overhead.
  • ScriptableObject Event Channels -- Cross-scene communication, designer-configurable, drag-and-drop wiring in Inspector. Best for gameplay events (player died, level complete). See unity-scripting/references/scriptableobjects.md for full implementation.
  • Static Event Bus -- Project-wide typed events, code-only (no assets to manage). Fast to prototype, good for system-level events (scene loaded, settings changed). Harder to debug than SO channels.

SCAFFOLD (Static Typed Event Bus):

// 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" });

GOTCHA: Always unsubscribe in OnDisable (not OnDestroy) to match the OnEnable subscription. Static Event Bus survives scene loads -- if a handler's object is destroyed without unsubscribing, you get MissingReferenceException. Use struct events to avoid allocation. SO Event Channels are preferred when designers need to wire events visually.


PATTERN: Manager Bootstrap Sequence

WHEN: Multiple manager systems depend on each other at startup

DECISION:

  • Boot Scene -- A dedicated "Boot" scene loads first, creates all managers, then loads the gameplay scene additively. Visual, async-friendly, supports loading screens. Best for production projects.
  • RuntimeInitializeOnLoadMethod -- Code-only bootstrap, no scene dependency. Creates managers via BeforeSceneLoad. Best for plugins, packages, or projects where any scene can be the entry point.

SCAFFOLD (Boot Scene):

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);
    }
}

SCAFFOLD (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()
    }
}

GOTCHA: RuntimeInitializeOnLoadMethod cannot use await (it's a static synchronous method). If your bootstrap needs async operations (loading configs, authenticating), use BeforeSceneLoad to create a bootstrap MonoBehaviour, then do async work in its Start(). Boot Scene approach requires adding the Boot scene to Build Settings as scene 0. Cross-ref: unity-lifecycle covers RuntimeInitializeOnLoadMethod timing in detail.


Architecture Anti-Patterns

Anti-PatternProblemAlternative
God MonoBehaviour (1000+ lines)Untestable, hard to modify, merge conflictsSplit into focused components + plain C# logic classes
Singleton for everythingTight coupling, untestable, hidden dependenciesService Locator for infra, DI for gameplay
FindObjectOfType in UpdateO(n) search every frame, slowCache reference in Start/Awake, or use events/SO
Manager-of-ManagersCentralized bottleneck, circular dependenciesEach system registers with Service Locator independently
Direct cross-references between systemsBreaks if either system is removed/replacedEvent channels or Service Locator with interfaces
Static state without domain-reload resetStale data between play sessions in Editor[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]

Related Skills

  • unity-scripting/references/scriptableobjects.md -- SO Event Channels, Runtime Sets, Variable References (full implementations)
  • unity-lifecycle -- RuntimeInitializeOnLoadMethod timing, SubsystemRegistration, DontDestroyOnLoad
  • unity-state-machines -- FSM/BT as plain C# classes following the MonoBehaviour wrapper pattern
  • unity-testing -- How Service Locator enables mocking in Edit Mode tests

Additional Resources

What ships with it: 1 file

8.7 KB alongside SKILL.md

references/

Gives 0 of the 12 instructions most project setup skills give in ~2.9k tokens

Counted across 999 of the 1,637 authors here whose files we hold, read 2026-08-07

  • ask one question at a timein 29 of 999, across 28 files
  • detect the package manager from lockfilesin 28 of 999, across 9 files
  • present findings to the userin 26 of 999, across 5 files
  • explore current repo statein 24 of 999, across 3 files
  • update the agent skills block in place if it existsin 24 of 999, across 3 files
  • install husky lint-staged and prettierin 23 of 999, across 4 files
  • create the lintstagedrc filein 22 of 999, across 3 files
  • commit all changed filesin 22 of 999, across 3 files
  • run lint-staged to verify it worksin 22 of 999, across 3 files
  • create the husky pre-commit filein 21 of 999, across 2 files
  • create a prettierrc file if missingin 21 of 999, across 2 files
  • initialize huskyin 21 of 999, across 2 files

Said here and by no other author read

  • Choose architecture based on project scale and testability needs.
  • Use interfaces for queryable component capabilities.
  • Separate pure logic into plain C# classes owned by MonoBehaviours.
  • Use composition over deep MonoBehaviour inheritance hierarchies.
  • Back services with interfaces so tests can register mocks.
  • Prefer TryGetComponent over null-checking GetComponent.

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.