Unity/VContainer

유니티를 위한 DI 프레임워크, VContainer이란?

VR하는소년 2023. 3. 9. 09:00

[VContainer이란?]

  • VContainer는 Unity를 위한 가볍고 빠른 DI(Dependency Injection) 프레임워크로, 종속성 및 개체 생성을 쉽게 관리할 수 있도록 도와줍니다.
  • DI는 종속성의 생성 및 관리를 분리하는 프로그래밍 디자인 패턴입니다. 
  • VContainer의 주요 기능 중 하나는 성능입니다. 다른 종속성 주입 프레임워크와 달리 VContainer는 런타임이 아닌 컴파일 타임에 코드를 생성합니다. 
  • 전반적으로 VContainer는 Unity 프로젝트의 아키텍처를 단순화하고 개선하는 데 도움이 되는 강력하고 가벼운 DI 프레임워크입니다.

 

 

유니티(Unity) 인터페이스(Interface) 완전 쉽게 설명

먼저 인터페이스가 무엇인지 알고 시작해야합니다 인터페이스를 구글링하면 뭐 USB를 예시로 컴퓨터 USB구멍에 어떤걸 꽂아도 다 작동잘되죠~이게 인터페이스입니다~ 이렇게...설명해줘서 솔직

wlsdn629.tistory.com

DI에 대해 이해가 안되시는 분들은 이전 글을 읽고 오시는 것을 추천드립니다!


[VContainer 설치 방법]

프로젝트의 폴더에 manifest.json 파일을 열고 다음 한줄을 dependencies block안에 추가해주면 됩니다!

"com.unity.nuget.mono-cecil": "1.10.1",

 

 

그 후, VContainer설치는 아래 링크가서 하시면 됩니다.

 

Releases · hadashiA/VContainer

The extra fast, minimum code size, GC-free DI (Dependency Injection) library running on Unity Game Engine. - hadashiA/VContainer

github.com


manifest에 한줄 추가 안하면 위와 같이 오류가 잔뜩 뜹니다!

 

 

 

 

 

 


[VContainer 사용법 - 기초]

  • 씬 안에 LifetimeScope를 상속받는 컴포넌트를 하나 만듭니다. 그 컴포넌트 안에는 하나의 컨테이너 하나의 스코프를 가지고 있습니다.
  • LifetimeScope의 서브클래스 안에 있는 C#코드에 종속성을 등록합니다. 이것은 Composition Root입니다.
  • 씬을 플레이할 때, LifetimeScope는 자동적으로 컨테이너를 빌드하고, 자신만의 PlayerLoopSystem으로 보냅니다.
[NOTE]

일반적으로, "scope"는 게임이 실행되는 동안 반복적으로 만들어지고 파괴됩니다. 
LifetimeScope는 이 방법을 가정하고, 하나의 부모-자식 관계를 갖습니다

 


First Step

다른 클래스에 종속하는 하나의 클래스를 만들어 봅시다

namespace MyGame
{
    public class HelloWorldService
    {
        public void Hello()
        {
            UnityEngine.Debug.Log("Hello world");
        }
    }
}

 


Second Step

Composition Root를 정의해보겠습니다.
C# 스크립트를 하나 생성하고 이름을 GameLifetimeScope로 지어줍니다.

using VContainer;
using VContainer.Unity;

namespace MyGame
{
    public class GameLifetimeScope : LifetimeScope
    {
        protected override void Configure(IContainerBuilder builder)
        {
            builder.Register<HelloWorldService>(Lifetime.Singleton);
        }
    }
}

 


Third Step

게임오브젝트를 생성 후 방금 만든 GameLifetimeScope를 붙여줍니다.

Register된 오브젝트는 자동으로 종속성 주입(DI)을 가지게 됩니다.
 
그렇다면, 방금 만든 HelloWorldService 스크립트는 어떻게 사용해야할까요?
사용방법은 다음과 같습니다.

using VContainer;
using VContainer.Unity;

namespace MyGame
{
    public class GamePresenter
    {
        readonly HelloWorldService helloWorldService;

        public GamePresenter(HelloWorldService helloWorldService)
        {
            this.helloWorldService = helloWorldService;
        }
    }
}

이렇게 HelloWorldService 클래스를 사용할 하나의 클래스를 생성 후
GamelifTimeScope에 등록해줍니다.
 

builder.Register<HelloWorldService>(Lifetime.Singleton);
+ builder.Register<GamePresenter>(Lifetime.Singleton);

 


Fourth Step

PlayerLoopSystem에 등록된 오브젝트를 실행시키면 됩니다.
유니티에서 어플리케이션을 작성하기 위해서는, 유니티의 Lifecycle 이벤트를 방해해야만 합니다.(일반적으로, 모노비헤이비어의 Start / Update/ OnDestroy 등...)
VContainer에 등록된 오브젝트는 모노비헤이비어로부터 독립적으로 실행할 수 있으며, 
몇 개의 marker interface(ex. ITickable)를 등록하고 실행시킴으로써 자동적으로 할 수 있습니다.

using VContainer;
using VContainer.Unity;

 namespace MyGame
 {
    public class GamePresenter : ITickable
     {
         readonly HelloWorldService helloWorldService;

         public GamePresenter(HelloWorldService helloWorldService)
         {
             this.helloWorldService = helloWorldService;
         }

        void ITickable.Tick()
        {
            helloWorldService.Hello();
        }
     }
 }

ITickable 인터페이스를 하나 상속받습니다.
 
이제부터, Tick()함수는 유니티의 Update 실행 주기에 맞춰 실행될 것입니다.
 
이와 같이, marker interface를 통해 side effect entry points(해석 불가...)을 지키는것은 좋은 방법입니다.
 
이렇게 디자인 함으로써, 모노비헤이비어의 Start, Update 등을 충분히 사용할 수 있습니다.
VContainer의 marker interface는 도메인 로직과 현재 로직의 분기점을 분류해주는 함수입니다.

builder.RegisterEntryPoint<GamePresenter>();

위와 같이 우리는 Unity의 Life Cycle 이벤트에 실행되도록 등록할 수 있습니다.
 
builder.RegisterEntryPoint<GamePresenter>()는

  • Register<GamePresenter>(Lifetime.Singleton).As<ITickable>() 이렇게 작성하는 것과 같습니다.

 

에디터를 실행해보면 잘 작동하는 것을 볼 수 있습니다.
 

 

 

 

 

 


Inversion of Control (IoC)

일반적으로 유저의 Input과 같은 이벤트에 의존해서 로직이 실행됩니다.

using UnityEngine.UI;
public class HelloScreen : MonoBehaviour
{
    public Button HelloButton;
}

위와 같은 컴포넌트가 주어졌다고 고려해봅시다.
일반적인 유니티 프로그래밍에서는, HelloScreen 스크립트에서 모든 로직을 담았습니다만.
DI을 사용하면, HelloScreen과 흐름을 제어할 수 있는 영역을 나눌 수 있습니다.
 
다음과 같이 말이죠.

namespace MyGame
{
    public class GamePresenter : IStartable
     {
         readonly HelloWorldService helloWorldService;
         readonly HelloScreen helloScreen;  

         public GamePresenter(
             HelloWorldService helloWorldService,
            HelloScreen helloScreen)
         {
             this.helloWorldService = helloWorldService;
            this.helloScreen = helloScreen;
         }

        void IStartable.Start()
        {
            helloScreen.HelloButton.onClick.AddListener(() => helloWorldService.Hello());
        }
      }    
}

이렇게 로직을 구성함으로써 우리는 Domain Logic / Control Flow / View Compoenet 로 part를 나눌 수 있게 되었습니다.

  • GamePresenter: 오직 흐름을 제어하는데 책임을 짊어짐
  • HelloWorldService: 언제, 어디서나 불림 당할 수 있는 책임을 짊어짐 - 기능을 담당
  • HelloScreen: 보여주는데 책임을 짊어짐 - UI

 
마지막으로 HelloScreen을 등록하는 것을 잊으면 안됩니다.

public class GameLifetimeScope : LifetimeScope
{
    [SerializeField] HelloScreen helloScreen;

     protected override void Configure(IContainerBuilder builder)
     {
         builder.RegisterEntryPoint<GamePresenter>();
         builder.Register<HelloWorldService>(Lifetime.Singleton);
         builder.RegisterComponent(helloScreen);
     }
}