A lightweight, type-safe MVC framework for Unity, built for performance and team scalability.
- Unity 2021.3+
- .NET Standard 2.1, C# 9
- No third-party dependencies
Full docs, guides, and API reference: mvcexpress.org
Add via Window > Package Manager > Add package from git URL:
https://github.com/MindScriptAct/mvcExpress-framework-unity.git?path=Packages/org.mvcexpress.core
Or add directly to Packages/manifest.json:
{
"dependencies": {
"org.mvcexpress.core": "https://github.com/MindScriptAct/mvcExpress-framework-unity.git?path=Packages/org.mvcexpress.core"
}
}Pin to a specific version by appending a tag:
https://github.com/MindScriptAct/mvcExpress-framework-unity.git?path=Packages/org.mvcexpress.core#0.9.2
Unity projects tend to start clean and end up as a tangle of MonoBehaviours that read input, hold state, call APIs, and update the UI all in the same script. That's fine for a prototype, but it gets expensive as a team and a codebase grow: business logic is hard to test without a running scene, a change in one script silently ripples into others, and onboarding a new programmer means reading spaghetti to find out "where does X actually happen."
mvcExpress imposes a small, strict set of actor roles (Service, Proxy, Command, Mediator) that only communicate through typed messages and a dependency injection container - never through direct references to each other. The C# type system, not a wiki page, is what enforces the architecture: every actor is registered, resolved, and dispatched by its class type, so mistakes tend to surface as compile errors instead of runtime mysteries. Business logic (Commands, Proxies, Services) is plain C#, so it can be unit tested without a Unity scene or Play Mode.
The framework doesn't dictate everything beyond the four roles: it recommends good patterns through docs, editor warnings, and tooling, but doesn't force a single workflow. Pick whichever of the three registration styles (Inspector drag-drop, attributes, code) fits your team, and mix them per module.
MvcFacade (singleton, DontDestroyOnLoad)
+- Global DI container + global message bus, shared by all modules
|
+-- MvcModule composition root for one scene / subsystem / subApp
+-- Services stateless helpers, calculations, SDK adapters, utils
+-- Proxies state, data, persistence -- publish messages, never subscribe
+-- Commands business logic triggered by messages -- the only actors that should write to Proxies
+-- Mediators MonoBehaviour bridge to Unity views -- can subscribe/publish messages for communication
View input becomes a published message. A bound Command reads/writes a Proxy and calls Services. The Proxy publishes a state-change message. Subscribed Mediators update the view. No actor holds a direct reference to another actor.
- MVC actors - Service/ServiceBehaviour, Proxy/ProxyBehaviour, Command/CommandAsync, MediatorBehaviour, each with one job and an enforced dependency direction
- Type-safe dependency injection - constructor and
[Inject]property injection, module-scoped and global containers,TryResolve<T>for optional lookups, no reflection in hot paths - Typed message bus -
IMessage/IMessage<T1..T12>; preferreadonly structfor zero-GC publish; one bus shared across all modules - Async commands -
CommandAsync<T...>withasync/await, bound identically to sync commands - Command pooling - fixed-size pools remove per-dispatch allocation for high-frequency messages
- Three registration styles, mixable per module - Unity Inspector components,
[Register]/[Bind]/[Attach]attributes, or code overrides - Strict initialization order - Services -> Proxies -> Commands -> Mediators ->
OnInitialized(), validated in Editor and development builds - Dynamic mediators - attach/detach
MediatorBehaviourinstances at runtime viaMediatorHub, with a prefab catalog for view lookup - Global scope - promote a service or proxy to
MvcFacadeso any module can inject it - Shared proxy mapping - register one proxy instance under a write interface for Commands and a read-only interface for Mediators
- Automatic cleanup - subscriptions, bindings, and actor teardown are handled when a module is destroyed
- Editor tooling - code generators for every actor type, custom inspectors, composition-style enforcement (soft warning or hard compile error)
MvcDebuglogging - zero-allocation, compiled out of release builds unless explicitly enabled [Work in progress...]
- Business logic lives in plain C# (Commands, Proxies, Services), so it can be tested without a Unity scene or Play Mode
- No reflection in the dispatch/injection hot paths and no per-frame allocation from struct messages - friendly to IL2CPP/AOT builds and mobile performance budgets
- Compile-time wiring: renaming or deleting a type breaks the build, not a runtime lookup
- Scales from a single-scene prototype (one module, Inspector wiring) to a multi-module app (global proxies, runtime module spawning) without switching tools
- Read-only view interfaces make "who is allowed to write this state" a type-checked question, not a code-review reminder
- Zero third-party dependencies
- More ceremony than a raw MonoBehaviour for a one-off script: a simple action is a Message plus a Command plus a binding, not just a method call
- The message bus is application-wide with no per-module filtering;
CommandAsyncgets a module-scopedCancelTokenautomatically (cancelled when the module is destroyed), but Proxies and Services do not - they still need their ownCancellationTokenSourceif they run independent background work- Pre-1.0 (
0.9.x): the public API is close to settled but may still shift before1.0, and some tooling (composition-style hard enforcement, the debugging suite) is still in progress - Opinionated by design: teams that want unrestricted freedom in where logic lives may feel the actor boundaries as friction
// Message: zero-allocation struct payload
public readonly struct AddGoldMessage : IMessage<int> { }
public readonly struct GoldChangedMessage : IMessage<int> { }
// Proxy: owns state, publishes when it changes
public class WalletProxy : Proxy
{
public int Gold { get; private set; }
public void Add(int amount)
{
Gold += amount;
Messenger.Publish<GoldChangedMessage, int>(Gold);
}
}
// Command: the only actor that writes to WalletProxy, bound to AddGoldMessage
public class AddGoldCommand : Command<int>
{
[Inject] private WalletProxy _wallet;
public override void Execute(int amount) => _wallet.Add(amount);
}
// Module: wires it together
public class GameplayModule : MvcModule
{
protected override void RegisterProxies() =>
Container.Register(new WalletProxy()).ToLogic().AsPermanent();
protected override void BindCommands() =>
Commander.Bind<AddGoldCommand, AddGoldMessage, int>();
}
// Mediator: reacts to the state change, no business logic
public class WalletMediator : MediatorBehaviour
{
[SerializeField] private TMP_Text _label;
protected override void OnInitialized() =>
Messenger.Subscribe<GoldChangedMessage, int>(gold => _label.text = gold.ToString());
}Three importable sample projects (Window > Package Manager > mvcExpress > Samples) build the same score-counter app with a different registration style, so you can compare them directly:
| Sample | Registration style |
|---|---|
| 01 - Single Module Unity App | Unity Inspector drag-drop |
| 02 - Single Module Code App | Code-first |
| 03 - Single Module Attribute App | Attribute-based |
- Multi-module sample demonstrating cross-module messaging, global proxies, and runtime module spawning
- Debugging suite - message bus console, actor timelines, dependency graphs, live application state overview
- mvcExpress Live - an extension package for scheduled updates, timers, and reactive data streams
MIT - see LICENSE.md