Coming from a software engineering background and with some WPF on my resume, I’ve long preferred the MVVM pattern to Unity’s more laissez-faire component approach. Don’t get me wrong, components are great. The trouble comes when trying to organize one’s code with some semblance of separation of concerns, since Unity mixes business and presentation at nearly every level.
There have surely been countless pixels spilled on this paradox. My impression is that for many, the choice seems to boil down to capitulating to bad practice, or completely beating Unity into submission: some combination of physics proxy, startup sequencer, singleton, “MyMonobehaviour” with “MyUpdate” called by “MyUpdateManager,” tool-prescribed workflow e.g. define a model and generate codebehind, etc.
With BuildDown, I aimed squarely for a happy medium. Build an event-driven, model/presentation-distinct architecture where the cost was low and return on investment was high, but leverage Unity where it made sense. For one, it made implementation that much simpler. And realistically, I’m not about to jump ship for another engine anytime soon, so saving myself some minor future refactoring was hardly worth the headache or heartache of fully platform-agnostic intermediary layers and proxies muddying up the codebase.
I used the MVVM pattern as a guide, splitting the code into model (pure C# classes with atomic operations and no presentation knowledge), view model (Unity scripts that translate Unity I/O into model operations and vice versa), and view (sometimes simply represented by Unity Transform or Renderer components, sometimes a UI-interfacing specialist component).
The most important objects to look at are the table and the blocks. To begin, TableViewModel handles touch input, listening for pointer down/move/up messages and determining which block is being manipulated. This gives me power and flexibility in interpreting inputs, and is much more lightweight than subscribing the possibly dozens of BlockViewModels to touch inputs individually.
Based on those inputs, TableViewModel will call methods on its model, like Move() and TryMerge(). The Table model is a purely mathematical model of the playspace; its function is to manipulate Block models (mostly adding or removing blocks or modifying their positions). Actually, the Table model requests new Blocks to add from the IBlockProvider, the implementation of which (UnityPooledBlockProvider) handles Unity-specific instantiation and pairing block models and view models.
The Block model simply contains the individual block’s state – color, position, etc. The BlockViewModel is bound to the mutable state variables and relays changes to the relevant Unity components, for example, responding to a BlockPosition change event by updating the Transform position. This relationship is powered by observable values (née bindable values, renamed to more accurately represent the observer-observable relationship, and to free up the “binding” term for a future implementation of true data binding).
From the touch event received by TableViewModel, to the method call updating Table, to the property change on a Block raising an event that BlockViewModel interprets to update its Transform, the entire end-to-end process is extremely event-driven. In fact, only about a dozen scripts in the whole project even implement an Update() method.
This centrist approach to blending MVVM best-practices with Unity’s expectations and demands served me well on BuildDown, and has been an important foundation for my approach to subsequent projects. Of course, not all game models are so simple and platform-independent, and on a future update I will detail my struggles reconciling this philosophy with a much messier, physics-based model.