Search code examples
c#unity-game-enginedependency-injectioninversion-of-control

How to use VContainer to instantiate a prefab and register attached monobehaviours


I've been working on introducing IoC containers into my project early on and am currently trying to use VContainer (https://vcontainer.hadashikick.jp/). I'm having trouble understanding the documentation for it though, and I can't figure out how exactly to instantiate a gameobject prefab and then register the components on it. VContainer has a function in its builder called RegisterComponentInNewPrefab but this doesn't seem to be used for this purpose.

The below code snippet lives on an empty gameobject in the scene, and runs on boot. I am trying to instantiate the player gameobject from a prefab, and then register the monobehaviours attached to it so they can be injected into other scripts when needed.

public class SimulationLifetimeScope : LifetimeScope
{
    [SerializeField] private GameObject prefab;

    protected override void Configure(IContainerBuilder builder)
    {
        InstallGameObject(builder);
        InstallPlayerControllers(builder)
    }

    private void InstallGameObject(IContainerBuilder builder)
    {
        var playerObject = Object.Instantiate(prefab);
        Container.InjectGameObject(playerObject);       
    }

    private void InstallPlayerControllers(IContainerBuilder builder)
    {
        builder.Register<PlayerController>(Lifetime.Singleton);
        builder.Register<PlayerFacade>(Lifetime.Singleton);
        builder.Register<Collectable>(Lifetime.Singleton);
    }
}

Container is an IObjectResolver type, and should be set up in the base class LifetimeScope, but when called here in code it's null and so unity errors out at that stage. How would I do this correctly?

Edit: Here's the PlayerController script, as well as the current PlayerFacade script that's currently just used to test if the player controller is injected correctly (both sit on the same game object prefab, which is instantiated on game start by VContainer).

public class PlayerController : MonoBehaviour
{
    [SerializeField] private PlayerData playerData;
    [SerializeField] private Transform collectableParent;

    private Transform cameraTransform;
    private Rigidbody rb;
    private InputControl playerInputActions;
    private Vector2 leftInputVector= Vector2.zero;
    private float size;

    private void Start()
    {        
        cameraTransform = Camera.main.transform;        

        //Input
        playerInputActions = new InputControl();
        playerInputActions.PlayerBallMovement.Enable();
        size = playerData.StartingSize;
    } 

    void Update()
    {
        leftInputVector = playerInputActions.PlayerBallMovement.LeftStickDirection.ReadValue<Vector2>();
    }

    private void FixedUpdate()
    {
        Move();
    }

    private void Move()
    {
        var dragForce = -rb.velocity * playerData.PlayerDrag;
        dragForce.y = 0f;
        var force = new Vector3(leftInputVector.x, 0, leftInputVector.y);
        
        //camera stuff
        //todo move this elsewhere and make nicer
        var cameraForward = cameraTransform.forward;
        var cameraRight = cameraTransform.right;
        cameraForward.y = 0f;
        cameraRight.y = 0f;
        cameraForward = cameraForward.normalized;
        cameraRight = cameraRight.normalized;
        
        var totalMovementVector = (force.z * cameraForward) + (force.x * cameraRight) + dragForce;

        rb.AddForce(totalMovementVector * playerData.PlayerSpeed, ForceMode.Acceleration);
    }

    private void OnCollisionEnter(Collision collision)
    {
        collision.gameObject.TryGetComponent<Collectable>(out var collectable);
        var closestHitPoint = collision.collider.ClosestPointOnBounds(gameObject.transform.position);
        
        if (collectable == null || !collectable.TryPickedUp(this, closestHitPoint)) 
            return;

        size += collectable.Size;
    }
}
public class PlayerFacade : MonoBehaviour
{
    [Inject] private PlayerController PlayerController;

    private void Start()
    {
        if (PlayerController == null)
           Debug.Log("player controller inject is null on start");
        else
            Debug.Log("player controller inject is not null on start");
    }

    private void Update()
    {
        if (PlayerController == null)
            Debug.Log("player controller null in update loop");
        else
            Debug.Log("player controller not null in update loop");
    }
}

Solution

  • You must pass Component to RegisterComponentInNewPrefab. Since most objects are GameObjects, I think it is not possible to specify this to avoid confusion.

    Below is the test code for the minimum configuration. BTW: The parent class of MonoBehaviour is Component.

    SimulationLifetimeScope.cs

    using VContainer;
    using VContainer.Unity;
    using UnityEngine;
    
    public sealed class SimulationLifetimeScope : LifetimeScope
    {
        [SerializeField] Player prefab;
    
        protected override void Configure(IContainerBuilder builder)
        {
            builder.RegisterComponentInNewPrefab<Player>(prefab, Lifetime.Singleton);
            builder.RegisterEntryPoint<RequiresPlayerEntryPoint>();
        }
    }
    

    Player.cs

    public sealed class Player : MonoBehaviour {}
    

    RequiresPlayerEntryPoint.cs

    public sealed class RequiresPlayerEntryPoint : IInitializable
    {
        public RequiresPlayerEntryPoint(Player player)
             => Debug.Log(player); // Player(Clone) (Player)
    
        public void Initialize() {}
    }
    

    Edit: If the scripts are attached to the same object

    It may be necessary to generate and register components on your own. The reason is that each API is only generated at registration.

    Player Inspector example scripts usage

    SimulationLifetimeScope.cs

    using VContainer;
    using VContainer.Unity;
    using UnityEngine;
    
    public sealed class SimulationLifetimeScope : LifetimeScope
    {
        [SerializeField] Player prefab;
    
        protected override void Configure(IContainerBuilder builder)
        {
            var obj = Instantiate<Player>(prefab);
            builder.RegisterComponent<Player>(obj);
            builder.RegisterComponent<PlayerController>(obj.GetComponent<PlayerController>());
            builder.RegisterComponent<PlayerFacade>(obj.GetComponent<PlayerFacade>());
            builder.RegisterEntryPoint<RequiresPlayerEntryPoint>();
        }
    }
    
    

    RequiresPlayerEntryPoint.cs

    public sealed class RequiresPlayerEntryPoint : IInitializable
    {
        public RequiresPlayerEntryPoint(Player player, PlayerController controller, PlayerFacade facade)
        {
            Debug.Log(player);
            Debug.Log(controller); 
            Debug.Log(facade); 
        }
    
        public void Initialize() {}
    }