State: Work in progress, but fully usable; not unit-tested, but master should be stable.
Requirements: Tested in Unity 2018.3, requires scripting runtime set to '.Net 4.x equivalent' (uses C# 7 features).
Sadly, this repo, like all my Unity3D repos, is sort of abandoned at the moment. I had plans expanding and polishing this scaffolding project, to make it more feature-rich and intuitive to use for others, but the reality is that I had to start looking for an actual paying job, and Unity3D didn't seem like a viable option. But I'll most likely continue working on it a bit later.
Essentially this is an adapted/extended version of the ScriptableObject-based architectural approach introduced in the Unite talk linked above.
I will try to show with some example scenarios and pictures why I think this architecture works pretty well for a lot of common problems.
This is used as a Git submodule in my project; in 2-3 classes I still need to work out some solution to a few dependencies on my sound and animation manager (notably in Interactable.cs). However, you can actually find the SoundManager here on GitHub. The solution I'll implement is probably injecting these dependencies as interfaces wrapped into GameProperty<T>
(one of the classes this 'framework' uses).
(FYI the naming is a real struggle for me here. I spent literally like 2 days thinking about how to call this module, plus how to call the state holding class, etc. I didn't like the original GameVariable name, and I wanted to differentiate its use from the normal variables/fields we use (that's why I also went with Get()
and Set()
instead of property accessors). So it's entirely possible that I'll rename a bunch of things.)
- Powerful Editor-configurability for teams with non-programmer workers, e.g. artists and designers. Because obviously it would be much easier to just use for example a message bus / event aggregator to send payloads to listeners.
- Injecting pre-defined data or configuration into
MonoBehaviour
components. - Exchanging pre-defined types of data between
MonoBehaviour
components, either through polling or event subscription, without creating hard references between them. - Creating reactive, or event-driven, workflow between components with events and change notifications. (But for now don't expect real reactive features, like map, filter, etc. ;))
- Driving GUI behaviour and interactivity.
- Highly complex games, because the data types are really fine-grained, and if you need to create hundreds of them, that would probably get messy. However, you can easily extend this system with your own, less fine grained types.
- Scenarios where you need to create and propagate state dynamically, since this is all about using pre-defined
ScriptableObject
instances. Of course in a lot of cases what you actually need is to hook the components onto a communication channel, and these channels are usually pre-definable.
The common scenario of item pickups (e.g. coins in platformers), or projectile impacts in 3D games. This often requires triggering ParticleSystems
, AudioSources
, and updating game statistics / UI, especially in more polished projects.
Workflow:
- You add a
ParticleSystem
and anAudioSource
component directly to yourGameObject
. - You reference various other components via e.g. singletons, Editor-associations or
GetComponent()
, for directly calling methods on them. - On trigger/collision enter you hide the
GameObject
's renderer, execute all the necessary calls on the referenced components, and destroy/disable theGameObject
in a delayed manner (since theParticleSystem
andAudioSource
still need to finish).
Key characteristics:
- Very simple and easy to learn approach, which doesn't require any framework, or understanding of software architecture.
- The
GameObject
itself assumes responsibility for everything that needs to happen when it's triggered or collided into. - Many components, e.g.
ParticleSystems
andAudioSources
, are duplicated on eachGameObject
instance. - Even simple
GameObjects
and prefabs start to feel tangled and bloated as you add more polish to the game and include particle effects, sounds, UI updates, game statistics, etc.
Workflow:
- (Required only first time) You create a
struct
orclass
that will contain all the data relevant to your event (or skip this, and just use a primitive type, if that's enough). - (Required only first time) You create a
ScriptableObject
-based asset that will serve as an Editor-assignable send/receive channel for your event data. - You assign this created
ScriptableObject
-based communication asset to all yourGameObject
script (as invokable), and to all other components that want to listen and react (as readonly). - Your script simply invokes the event, which notifies all subscribers.
Key characteristics:
- Requires more work and understanding to set up first. But, after the initial setup it's easy to use for non-programmers, because many aspects of game logic are Editor-declarable and -configurable.
- The
GameObject
has a single responsibility, and the other components which subscribe to this event take care of their own relevant responsibility. - The number of components on each
GameObject
instances can be minimized; often evenParticleSystems
andAudioSources
can be removed and handled in a separate single component which is responsible for reacting to events at world coordinates. - Your
GameObjects
and prefabs can remain very simple, even in a game that is highly polished with dozens of various audio/visual/UI reactions to events. - Easy to create general, reusable components. For example you can create a
ParticleSystem
trigger component to which you can associate any event in the Editor. Or aText
updater component that displays the content of the event payload, counts and displays the number of event invocations, etc. These simple, reusable components are easy to understand, and anybody can use and combine them to add simpler game features.
- You can create read-only and writeable instances of the data-holding classes.
- You can associate your writeable data classes (and invokable event classes) with readonly fields in the Editor.
- This means you can express clearly the intent that an event or data is an input of your component, and have automaticly enforced write-protection.
- Essentially you can avoid the situation of creating mutable shared state in your architecture. Which almost always leads to problems.
- My recommendation is that for each writeable instance you should have a single component that writes to it, and the rest should only listen.
- Not just the event classes, but the data-holding classes have an event too that notifies of value changes.
- Works sort of similarly to the
INotifyPropertyChanged
interface in .Net, in the sense that you can listen to changes related to a unit of data, and react in an event-driven manner.
- The common types, e.g.
float
,int
,Vector3
,Bounds
already have built-in concrete classes, but you can also create your own classes, including ones based on your custom types. Basically this is all you need to create a concrete type that you can use in the Editor (as we know, the Editor doesn't support generics, so you need to create non-generic derived classes):
[CreateAssetMenu]
public class BoundsProperty : GameProperty<Bounds>
{ }
- If you derive your components from
SubscriptionHelperMonoBehaviour
instead ofMonoBehaviour
, you can add subscriptions easily by invoking theAddSubscription()
method. - This automatically handles all subscription-related responsibilities, i.e. unsubscribing in
OnDisable()
, resubscribing inOnEnable()
, and again unsubscribing inOnDestroy()
. - Supports all data types automatically, including your own custom made types which derive from
GameEvent<T>
orGameProperty<T>
. - (Still need to refactor this, because it uses closing/allocating lambdas currently.)
- I included plenty of
MonoBehaviour
components I made over the last few weeks for myself; generally these are:- Component triggers and event counters
- GUI interactivity helpers for replacing the inflexible
Button
component - GUI skinning helpers for defining and associating colors which synchronize automatically (even in Edit mode).
- Includes color transformation capability, for defining H, S, V, A transformation on the received color.
- Based And a generic base class you can use for making color setters.
- Audio playback modulation for creating dynamic car engine, etc. sounds based on a float (e.g. speed)
- Has a 2-3 levels deep inheritance hierarchy here and there, but generally it's not overstructured and overcomplicated. I'd be glad to rely more on interfaces, composition and abstractions, but sadly it seems nearly impossible in Unity (if you want to keep things Editor-compatible).