Zenject Documentation

repository·master·Indexed 25 days ago

https://github.com/modesttree/zenject

A high-performance dependency injection framework for Unity 3D and C# (also referred to as Extenject). It enables the creation of loosely coupled applications through various injection patterns (Constructor, Field, Property, and Method), container management, and binding strategies. Supports multiple platforms including PC, Mac, Linux, iOS, Android, WebGL, and PS4, with specific support for IL2CPP and AOT platforms.

Tokens
38.4K
Snippets
72
Records
183
Agent score
84%

What's inside Zenject

  1. Overview of Zenject

    master

    Zenject is a lightweight, high-performance dependency injection (DI) framework designed specifically for Unity 3D, though it can be used in standard C# environments. It enables developers to create loosely coupled applications by segmenting responsibilities and using a DI container to glue parts together. This facilitates easier refactoring, reuse, and testing.

    Supported Platforms in Unity:

    • PC/Mac/Linux
    • iOS
    • Android
    • WebGL
    • PS4 (with IL2CPP backend)
    • Windows Store (including 8.1, Phone 8.1, Universal 8.1, and Universal 10 - both .NET and IL2CPP backends)

    Note: IL2CPP is supported, but there are specific considerations for AOT (Ahead-of-Time) platforms.

  2. Analyze and modify .NET binaries with Cecil

    master

    Cecil (Mono.Cecil) is a library used to generate and inspect programs and libraries in ECMA CIL form. It allows you to:

    • Analyze .NET binaries: Use a simple and powerful object model to inspect assemblies without the overhead of loading them via Reflection.
    • Modify .NET binaries: Add new metadata structures and alter the IL (Intermediate Language) code directly.

    For practical implementation details and learning how to manipulate IL, refer to the Cecil.Samples repository.

  3. Key Features of Zenject

    master

    Zenject provides a comprehensive suite of dependency injection features:

    • Injection Types: Supports Constructor, Field, Property, and Method injection for both normal C# classes and MonoBehaviours.
    • Binding Capabilities: Conditional binding (by type, name, etc.), optional dependencies, convention-based binding, and decorator pattern support.
    • Container Management: Supports Nested Containers (Sub-Containers), global project-wide bindings, and injection across different Unity scenes.
    • Object Lifecycle & Creation: Factories for post-initialization object creation, built-in memory pools, and LazyInject<> for just-in-time injection.
    • Unity Integration: Automatic injection via ZenjectBinding components, ZenAutoInjecter for game objects, and scene parenting.
    • Testing & Validation: Object graph validation at editor time, auto-mocking with Moq, and built-in support for unit, integration, and scene tests.
    • Performance: Support for multiple threads for resolving/instantiating and 'reflection baking' to eliminate reflection overhead by modifying generated assemblies.
  4. View games and libraries using Zenject/Extenject

    master
    This document provides a list of notable games and software libraries that utilize Zenject/Extenject for dependency injection. This serves as a reference for real-world applications of the framework across various genres (Rhythm, AR, Strategy, Simulation, etc.) and platforms (Oculus VR, iOS, Android, WebGL, PlayStation VR, and more).
  5. Identify libraries using Zenject/Extenject

    master

    The following libraries utilize Zenject/Extenject for dependency injection or architectural patterns:

    • EcsRx: A reactive Entity Component System (ECS) pattern that adheres to Inversion of Control (IoC). It is designed for .NET applications and games, with a specific Unity version available.
    • Karma: An MVCP (Model/View/Controller/Presenter) framework for Unity3D. It is built on top of Zenject to provide Dependency Injection (DI), which is used to route the application to desired views and enable composable, testable systems.
  6. Use ScriptableObjectInstaller for persistent settings

    master

    Derive from ScriptableObjectInstaller instead of MonoInstaller to create installers that persist changes made in the Unity Inspector during Play Mode. This is ideal for tweaking runtime parameters like game settings.

    Warning: Changes made to these settings via code will also be saved persistently. Treat settings objects as read-only in code to avoid unintended permanent changes.

    public class GameSettings : ScriptableObjectInstaller
    {
        public Player.Settings Player;
        public SomethingElse.Settings SomethingElse;
    
        public override void InstallBindings()
        {
            Container.BindInstances(Player, SomethingElse);
        }
    }
    
    public class Player : ITickable
    {
        readonly Settings _settings;
        public Player(Settings settings) => _settings = settings;
    
        public void Tick()
        {
            // Use _settings.Speed etc.
        }
    
        [Serializable]
        public class Settings
        {
            public float Speed;
        }
    }
  7. Queue instances for injection

    master

    When dealing with objects that exist at startup but are not created by Zenject, use QueueForInject instead of calling Inject immediately. This ensures objects are injected during the initial object graph construction phase, preventing issues where dependencies aren't yet bound, and guarantees correct injection order between dependent classes.

    var foo = new Foo();
    
    // Bind the existing instance
    Container.BindInstance(foo);
    
    // Queue it so it is injected during the graph construction phase
    Container.QueueForInject(foo);
  8. Enable lifecycle events in ByInstaller/ByMethod subcontainers

    master

    When using ByInstaller or ByMethod to create subcontainers, lifecycle interfaces like IInitializable, ITickable, and IDisposable are not automatically forwarded to the subcontainer by default. To ensure these events are triggered, you have two primary options:

    1. Derive the Facade from Kernel: Make your facade class inherit from Kernel and use BindInterfacesAndSelfTo<T>() in the parent container. This allows the parent container to forward lifecycle calls to the subcontainer.
    2. Use .WithKernel(): Add the .WithKernel() method to your binding statement. This automatically handles the forwarding of lifecycle events without requiring your facade class to inherit from Kernel.

    Note that for dynamically created subcontainers (e.g., via a BindFactory), you must explicitly call the lifecycle methods (like _greeter.Initialize()) on the created instance if you are not using a Kernel-based approach.

    // Option 1: Using WithKernel() to enable lifecycle events
    public class Greeter
    {
        public Greeter()
        {
            Debug.Log("Created Greeter");
        }
    }
    
    public class TestInstaller : MonoInstaller
    {
        public override void InstallBindings()
        {
            Container.Bind<Greeter>()
                .FromSubContainerResolve()
                .ByMethod(InstallGreeter)
                .WithKernel() // This enables IInitializable, ITickable, and IDisposable
                .AsSingle();
        }
    
        void InstallGreeter(DiContainer subContainer)
        {
            subContainer.Bind<Greeter>().AsSingle();
            subContainer.BindInterfacesTo<GoodbyeHandler>().AsSingle();
            subContainer.BindInterfacesTo<HelloHandler>().AsSingle();
        }
    }
  9. Implement Scene Decorators

    master

    Scene Decorators allow you to add behavior to a scene without modifying its original installers. Unlike scene parenting, all scenes in a decorator setup share the same Container, meaning they can access each other's bindings.

    To set up a Scene Decorator:

    1. Open your main production scene.
    2. In the Scene Hierarchy, right-click the menu beside the scene name and select Add New Scene.
    3. Drag the new scene so it is positioned above the main scene.
    4. Right-click inside the new scene and select Zenject -> Decorator Context.
    5. On the SceneDecoratorContext component, set the Decorated Contract Name to match the contract name used in the main scene's SceneContext.
    6. Create a MonoInstaller script to define the new behaviors.
    7. Add a GameObject to your decorator scene with this installer and drag it into the Installers property of the SceneDecoratorContext.

    Important Notes:

    • Decorator scenes must be loaded before the scenes they decorate.
    • Use the Validate command (CTRL+ALT+V) to verify multi-scene setups.
    public class ExampleDecoratorInstaller : MonoInstaller
    {
        public override void InstallBindings()
        {
            Container.Bind<ITickable>().To<TestHotKeysAdder>().AsSingle();
        }
    }
    
    public class TestHotKeysAdder : ITickable
    {
        public void Tick()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                Debug.Log("Hotkey triggered!");
            }
        }
    }
  10. Dynamically create objects with Factories

    master
    In Zenject, you should use Factories to create new object instances dynamically after the application or game has started (e.g., spawning enemies in a game). Using Factories ensures that newly created instances receive their required dependencies via injection, maintaining the integrity of the dependency graph.
  11. Set up Integration Tests with ZenjectIntegrationTestFixture

    master

    Integration tests run in a PlayMode environment involving SceneContext and ProjectContext. They execute bindings for IInitializable, ITickable, and IDisposable just like a normal game run.

    To set them up:

    1. Open Unity's Test Runner and go to the PlayMode tab.
    2. Click "Create PlayMode Test Assembly Folder".
    3. Add a reference to Zenject-TestFramework to the new .asmdef file.
    4. Right-click in the folder and select Create -> Zenject -> Integration Test.
    5. Inherit from ZenjectIntegrationTestFixture.

    Integration tests follow a three-phase lifecycle:

    • Before PreInstall(): Set up the initial scene (load prefabs, create GameObjects).
    • After PreInstall(): Call Container.Bind methods to configure the container.
    • After PostInstall(): All non-lazy objects are instantiated, injected, and IInitializable.Initialize has been called.

    Note: You must yield return null immediately after PostInstall() if you want MonoBehaviour.Start() methods to run.

    public class SpaceShipTests : ZenjectIntegrationTestFixture
    {
        [UnityTest]
        public IEnumerator TestVelocity()
        {
            PreInstall();
    
            Container.Bind<SpaceShip>().FromNewComponentOnNewGameObject()
                .AsSingle().WithArguments(new Vector3(1, 0, 0));
    
            PostInstall();
    
            var spaceShip = Container.Resolve<SpaceShip>();
            Assert.IsEqual(spaceShip.transform.position, Vector3.zero);
    
            yield return null; // Wait for Update/Start
    
            Assert.That(spaceShip.transform.position.x > 0);
        }
    }
  12. Enable UniRx integration

    master

    Zenject integration with UniRx is disabled by default. To enable it:

    1. Add the scripting define symbol ZEN_SIGNALS_ADD_UNIRX in Edit -> Project Settings -> Player -> Scripting Define Symbols.
    2. For Zenject version 7.0.0 and above, update your Zenject.asmdef file to include UniRx in the references:
    {
        "name": "Zenject",
        "references": [
            "UniRx"
        ]
    }

    Once enabled, you can observe Zenject signals via UniRx streams and observe Zenject events (like Tick, LateTick, FixedTick, etc.) on the TickableManager class.

    {
        "name": "Zenject",
        "references": [
            "UniRx"
        ]
    }