On of my biggest issues with unity games is when scripts, usually MonoBehavior, reference other scripts, with SerializeField, GetComponent, Static Events or Singletons to get or set values within them. This ends up coupling code making it more likely to break during system changes, and makes it harder to test individual features without testing the entire game.
To combat this and assist in development I made a dependency injection framework/event system inspired by 2 talks given at Unite
The key design principles that I wanted to incorporate from these talks were:
Shared Values adds an abstraction layer to handle values in between namespaces and classes allowing them to only care about the values, not where they come from.
Shared Values makes heavy use of the Unity Inspector, The Listener Pattern, Object Oriented Programming, Generic Classes, and Polymorphism
The core of the tool is SharedValueReference which is essentially a pointer class. It tells the component referencing it, the ammount its value should be shared:
SharedValue (Scene independent, synced across objects)SharedValue (synced across all other references that use the same instancer)
The value can be set via a script, and for a SharedValue, via a UnityEvent:
Value = newValue Value and broadcasts the newValueSetValueWithoutNotify(newValue) Value, does not broadcastSetValueWithBroadcastIfChange(newValue) Value and broadcasts if newNalue is different from ValueListening can be done by using one of the provided components or in a script with AddListener(Funciton)
Unlike an event callback which will just broadcast a value, a SharedValueReference stores the value so it can be read, like in Update()
There are currently 5 modules to Shared Values, the following 3 have the same polymorphic structure using interfaces:
The 2 below are modules that use a slightly different structure, but use SharedValueReference, and follow the same design principles .
public class PlayerController : MonoBehavior
{
RigidBody2D rb;
PlayerInput input;
[SerializeField] float speed;
void Update()
{
Vector2 moveInput = input.GetValue<Vector2>("Move");
rb.velocity = moveInput * speed
}
} PlayerController requires PlayerInput, and a RigidBody2D and will break if it doesn’t have either.PlayerInput, and a RigidBody2D, but if you wanted to test something (like an AI Controller) or add feedback (Velocity Arrow) you would have to rewrite code and add even more coupling to account for it.public class PlayerController : MonoBehavior
{
[SerializeField] SharedVector2Reference moveInput;
[SerializeField] SharedVector2Reference velocity;
[SerializeField] float speed;
void Update()
{
//broadcast changes in velocity
velocity.SetValueWithBroadcastIfChange(moveInput.Value * speed)
}
} PlayerController doesn’t require any additional components, so you can test different inputs and outputs easier. Ex: AI Controller, Cutscene controller, etcVelocityController for the example above)SharedValueReference is less performant than SerializeField or GetComponent UnityEvents requires public functions decreasing encapsulation.