Coder Social home page Coder Social logo

syrinj's Introduction

Syrinj

Lightweight dependency injection & convenient attributes for Unity

Updated to work with Unity 2019.4


Table of Contents


Introduction

Examples

Convenience attributes:

public class SimpleBehaviour : MonoBehaviour
{
    [GetComponent]  private Rigidbody rigidbody;
    [Find("Music")] private AudioSource musicSource;
}

Simple dependency injection:

public class SceneProviders : MonoBehaviour 
{
    [Provides] 
    public Light SunProvider; // drag object in inspector to set

    [Provides]
    [FindObjectOfType]
    public Player PlayerProvider; // provides Player object from scene
}

// ...

public class SimpleBehaviour : MonoBehaviour
{
    [Inject] public Light Sun; 
    [Inject] public Player MyPlayer;
}

Set-up

  1. Add using Syrinj; to the top of files which use Syrinj.

  2. Annotate your classes with the attributes shown in the Documentation.

  3. Follow the steps below for your use case:

For injection on scene load:

Create a GameObject in your scene with the Component SceneInjector.

For providing MonoBehaviours:

Add [Provides] and the required convenience attributes to a field/property declaration.

For providing non-MonoBehaviours:

Add [Instance] or [Singleton] attributes to providers of non-MonoBehaviours. These will construct new instances or a shared single instance, respectively, at injection sites.

For injecting both MonoBehaviours and non-MonoBehaviours:

Add [Inject] to a field/property declaration.

For injection of MonoBehaviours while application is running:

Method 1:

Instead of using Instantiate() or CreateInstance() to create new instances of MonoBehaviours or ScriptableObjects, set parent class of the instantiator class to MonoClonable (DataClonable for ScriptableObjects) and use Clone() or use ObjectExtensions.Clone() directly.

For instance:

public class BulletFactory : MonoClonable
{
    public Bullet BulletPrefab; // drag from inspector

    private void Update()
    {
        Clone(BulletPrefab)
    }
}

public class Bullet : MonoBehaviour
{
    [Inject]
    private AudioController AudioController { get; set; } // this will be injected upon 'cloning'

    ...
}

Method 2:

Attach the InjectorComponent to any GameObject which contains providers and injectors.

Set the ShouldInjectChildren property in the inspector if you wish to inject children of the GameObject as well. DO NOT attach another InjectorComponent to those children. There should be only one root InjectorComponent for an object created with GameObject.Instantiate().

For injection of non-MonoBehaviours while application is running:

There are two kinds of non-MonoBehaviours.

For plain C-sharp objects that are instantiated with the new keyword (considered not serializable by Unity): Set parent class to InjectableObject and use as usual.

For non-MonoBehaviours that are instantiated by the Unity engine (considered serializable by Unity): Set parent class to InjectableSerializableObject and use as usual.

The difference between non-MonoBehaviours that are serializable and not serializable by Unity is the former is either a class that implements an interface with [SerializedReference] being used on the field, or a [Serializable] class that has its fields set in the inspector, while the latter is just a plain C-sharp object that needs to be instantiated with a new keyword (the former doesn't).

To inject instances of non-MonoBehaviours without a provider class:

Inject a Provider<T> if you wish to create your own injected objects. T is the object you wish to create, and must be a non-MonoBehaviour with a default constructor. Then call Get() on the provider for a new instance. This kind of injection does not need the object to be an InjectableObject.


What is this?

Syrinj is a small package to make creating objects simpler in Unity.

It provides convenient attributes, such as [GetComponent] which automatically tell your MonoBehaviours where to find their dependencies.

For more customizable or shared dependencies, Syrinj allows you to specify providers and injection sites. See the extended examples for how to do this. You can even mix attributes like [GetComponent] with a [Provider], so that the GetComponent() method only runs once!

Why use this?

If you're familiar with dependency injection and see how Syrinj could help your project, check out the set-up and documentation to see more. If not, read on:

Dependency injection is an intimidating word for a simple concept you're likely familiar with. It simply means if ObjectA creates ObjectB, then ObjectA resolves all of ObjectsB's dependencies (i.e. fields & properties).

Here's a more concrete example. Say your enemies have a RocketLauncher which can fire homing missiles. I will call these GoodHomingMissile to denote that this is a good way to do this. Here is how you might fire a missile at the player:

public class RocketLauncher {
    private Player player;

    public void Fire() {
        var target = player;
        var missile = new GoodHomingMissile(target);
    }
}

public class GoodHomingMissile {
    private Player target;

    public GoodHomingMissile(Player target) {
        this.target = target;
    }

    public void MoveTowardsTarget() {
        // ...
    }
}

Makes sense? That's dependency injection. The GoodHomingMissile has a dependency of a Player target, and the RocketLauncher tells it which target to move towards on construction! This is a good practice because you know that if a GoodHomingMissile is created, it must have had its target specified.

If you're a Unity developer, you may already notice a slight issue. In Unity, you don't instantiate objects with constructors! Instead, you call GameObject.Instantiate(). One workaround is to make an Initialize() method:

public class OkayHomingMissile : MonoBehaviour {
    private Player target;

    public void Initialize(Player target) {
        this.target = target;
    }

    // ...
}

This is okay, but you lose the guarantee that the OkayHomingMissile has its dependencies right when it's created. You have to remember to call Initialize() every time. Let's complicate it further, and imagine the OkayHomingMissile also creates an Explosion when it reaches the player! The Explosion needs to know who to damage, and so it receives a Player as well when it's created.

public class OkayHomingMissile : MonoBehaviour {
    // ...

    void Update() {
        if (distanceToTarget() < 0.1f) {
            var explosion = new Explosion();
            explosion.Initialize(target);
        }
    }
}

Now we've passed this Player object between three classes, and it's getting a bit difficult to keep track of. Plus, in reality your classes are going to have a lot more than one dependency. Can you imagine doing this?:

explosion.Initialize(target, damage, radius, audioManager, particleManager, camera);

At this point, most Unity developers will settle on using Unity's inspector to set dependencies, and the infamous Singleton. Neither of these solutions are inherently bad, but they can lead to code that's difficult to maintain. A homing missile in practice may end up looking like this:

public class BadHomingMissile : MonoBehaviour {
    public GameManager gameManager; // set in inspector
    public ParticleSystem particles; // set in inspector
    public AudioSource audio;
    private int damage;
    private Player target;
    private RocketLauncher launcher;

    void Start() {
        audio = this.GetComponent<AudioSource>();
        target = Player.Instance;
        launcher = RocketLauncher.Instance;
    }
}

There's a lot of issues with this:

  • Your dependencies ought to be specified in code. You hope that the GameManager and ParticleSystem are set, but you may have drag-n-dropped the wrong object. Or you may have forgotten to do it all together.

  • Homing missiles are now tightly coupled to Singletons (i.e. Player.Instance and RocketLauncher.Instance). How do you know the Singleton exists when BadHomingMissile calls Start()? What if later on you want more then one RocketLauncher or Player? Extending and maintaining your classes will take a lot more effort.

  • These fields don't need to be public (easy solution: expose private fields in the inspector with [SerializeField]).

  • If a dependency isn't met, you won't receive an informative error message about what happened.

I'd argue that the most common issue with Unity code is bad dependency management and overuse of Singletons. Syrinj addresses all of the problems mentioned in the above exmaple.

There are alternatives to Syrinj for Unity, such as Zenject and StrangeIoC. These are great at what they do and worth checking out if you're starting a new project. Unfortunately they can be bulky, difficult to implement in an existing project, and harder to approach. However, I'd highly recommend reading the authors' elaboration on dependency injection and inversion-of-control to become a better programmer.

Syrinj allows you to write fewer lines of code, not more. You can take advantage of as many or as few of Syrinj's features as you'd like. Check out the set-up section to see how easy it is to get started.


Documentation

Extended examples

public class ExampleProvider : MonoBehaviour
{
    [Provides]
    [FindObjectOfType(typeof(Canvas))]
    private Canvas UIRootProvider; // any convenience attribute can be combined with "Provides"
    
    [Provides]
    [FindResourceOfType(typeof(GameData))]
    private GameData _gameData; // Finds the first asset of type GameData in a 'Resources' folder within your project

    [Provides]
    public float RandomNumberProvider 
    {
        get {
            return Random.RandomRange(0f, 1f); 
        } // define custom provider properties, these evaluate each injection
    }

    [Provides]
    public AudioSource MusicSourceProvider; // manually set in inspector

    [Provides("Primary")] // specify optional tags for multiple bindings of the same type
    public Camera PrimaryCamera;

    [Provides("Secondary")]
    public Camera SecondaryCamera;
  
  	[Provides] [Instance]
  	public NPC NPCProvider; // creates a new NPC at each injection site
  
  	[Provides] [Singleton]
  	public Player PlayerProvider; // shares the same Player each injection
}

// ...

public class ExampleInjectee : MonoBehaviour
{
    [Inject] private Canvas UIRoot;
    [Inject] private float RandomNumber;
    [Inject] private AudioSource MusicSource;
  
  	[Inject] private Provider<Enemy> Spawner; // instantiates new injected NPC on Spawner.Get();

    [Inject("Primary")]     private Camera primaryCamera;
    [Inject("Secondary")]   private Camera secondaryCamera;

    [GetComponent] 
    private Rigidbody rigidbody; // automatically caches Rigidbody on this object

    [FindWithTag("Player")]
    private GameObject Player { get; set; } // works with properties, as long as they can be set
}
Convenience attributes:
Attribute Arguments Usage
[GetComponent] opt. System.Type ComponentType Gets a component attached to this GameObject.
[GetComponentInChildren] opt. System.Type ComponentType Gets a component attached to this GameObject or its children.
[Find] string GameObjectName Finds a GameObject in scene with a given name. Can also return a component.
[FindWithTag] string Tag Finds a GameObject in scene with a given tag. Can also return a component.
[FindObjectOfType] System.Type ComponentType Finds a component in the scene with a given type.
[FindResourceOfType] System.Type ComponentType Finds the first asset of a given type in a 'Resources' folder
Injection attributes:
Attribute Arguments Usage
[Provides] opt. string Tag Registers a provider for a given tag and type.
[Inject] opt. string Tag Injects a field/property for a given tag and type.
[Instance] none Attach to [Provides] to construct a new instance at every injection.
[Singleton] opt. SingletonOptions SingletonOptions Attach to [Provides] to construct a singleton instance shared across injections.
Classes
Class Usage
Provider<T> Inject this class if you want to construct instances of T and have them be injected. Use Get() to construct a new instance of T, where T has a default constructor and is not a MonoBehaviour.

Notes

  • None for now.

Troubleshooting

Q: My fields/properties aren't being injected (or) I'm getting an error about missing dependency/provider/resolver

A: Follow these steps in order:

  1. SceneInjector component must exist somewhere in the scene (if object exists in the scene initially).

  2. Make sure there the [Inject] attribute is on the proper injected field, and the [Provides] attribute is on the proper provider field. Ensure both are bound to the exact same Tag (or lack thereof) and Type.

  3. For runtime-injection, if you use Method 1:

    • Make sure you are using Clone() instead of Instantiate() for MonoBehaviours.
    • Make sure you have set the parent class of non-MonoBehaviour to either InjectableObject or InjectableSerializableObject, refer Set-up for more information.
  4. For runtime-injection, if you use Method 2:

    • Your injecting/providing GameObjects must have InjectorComponents attached (if created with GameObject.Instiantiate()).
    • For every object that you call GameObject.Instantiate() on, you should have at most ONE InjectorComponent. Place this component at the root GameObject, with ShouldInjectChildren set if necessary.
  5. Make sure the fields or property providers aren't null! Use Debug.Log() and/or double-check the inspector for the object.

  6. Verify the script execution order in Unity. Go to Edit -> Project Settings -> Script Execution Order and modify the Syrinj.InjectorComponent and Syrinj.SceneInjector scripts to execute before all other scripts. Put in a large negative number such that these two scripts before any others in the list.

  7. There might be some other problem. Create an issue on GitHub/message me/fix it yourself with a pull request!

syrinj's People

Contributors

coldino avatar maddoxbr avatar mattvr avatar tyrng avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.