VContainer 初体验

IOC 控制反转

IOC 提供一个对象生成容器,在我们需要取得某个对象时,不再使用New关键字进行对象生成操作,而是通过IOC容器内部控制来获得对象。

使用这种思想方式,可以让我们无需关心对象的生成方式,只需要告诉容器我需要xxx对象即可,而高旭容器我需要xxx对象的方式就叫做DI依赖注入。

DI依赖注入

这种思想,我是在学习.net core时发现的,一般采用特性注入或者使用构造函数注入

经过标记的字段会在该类构造时在IOC容器中执行构造获取对应的对象示例。

VContainer

VContainer是在Unity端使用的一种轻量级的DI框架,通过GameLifeScope划分作用域,和普通的web后端的Di框架不同的是,针对Unity的Mono特殊性,提出了GameLifeScope的方式,通过在Game Object上挂载Scope,可以在代码中动态切换使用的实例和Scope。并且提供了一种父子继承Scope方式供用户使用。

Scope 作用域

作用域指函数、变量的作用范围,在Di中一般可以注册singleton单例,Trans复例等

在netcore中有类似下面这样的形式:

1
2
3
4
var serviceCollection = new ServiceCollection()
.AddTransient<ILoginService, EFLoginService>()
.AddSingleton<ILoginService, EFLoginService>()
.AddScoped<ILoginService, EFLoginService>();

其中添加的Addxxxx就是为其添加一项注入并指定注入作用域,同时在这里进行的是一个接口的注入操作,在使用这个接口依赖时会生成一个对应的接口实现实例。

在VContainer中的注入和以上的方式很像。

单例?

一般来说,在Unity中我们会继承自一个泛型单例类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/// <summary>
/// 基础单例(线程锁)
/// </summary>
/// <typeparam name="T"></typeparam>
public class Singleton<T> where T : new()
{
private static T _instance;

public static T Instance
{
get
{
if (_instance != null) return _instance;
try
{
_instance = new T();
Debug.Log($"{typeof(T)}".Color(new Color(0.2f,0.6f,0.8f)) + "|单例OK");
Monitor.Enter(_instance);
}
finally
{
Monitor.Exit(_instance);
}

return _instance;
}
}
}

例如继承自这个泛型单例来实现单例效果,在Di中我们的实例无需在外部控制单例,直接由IOC容器管理。

GameLifeScope

为其GameObject添加一个继承自LifetimeScope的脚本即添加了一个Scope。

++ 游戏中至少需要有一个Scope ++

GameLifeScope

1
2
3
4
5
6
7
8
9
10
11
public class BaseLifeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.Register<MusicManager>(Lifetime.Singleton);
builder.RegisterInstance(GameSettingAssetMgr.Instance);
builder.Register<UIManager>(Lifetime.Singleton);
builder.RegisterEntryPoint<GameLauncher>();
builder.RegisterEntryPoint<GameLoop>();
}
}

同时我们可以继承父级LifeScope来灵活选择我们的Scope实现。

1
2
3
4
5
6
7
8
9
10
11
12
public class GameLiftScope : BaseLifeScope
{
public Character gamePanel;

protected override void Configure(IContainerBuilder builder)
{
base.Configure(builder);


builder.RegisterComponentInNewPrefab(gamePanel, Lifetime.Scoped);
}
}

可注入对象

在上面我们描述了Scope,使用 builder.Register<T>进行注册。

Mono

可注入Mono的对象,例如游戏中的UI等与Mono关系密切的对象。

可将场景中的GameObject直接注入到容器中来使用。

image-20221006113633790

Instance

可注入具体的对象实例,例如单例我们可以直接注入对象的单例,也可以在Scope中选择单例。

EnterPoint

EnterPoint可以说是VContainer设计十分精妙的部分——将Mono转移到普通的C#类中。

通过builder.RegisterEntryPoint<T>();注入一个入口点。这些入口点能够代替Unity的Start、Update、FixedUpdate等函数。

通过实现不同的> Ixxx接口重写方法来编写原生Unity事件,如下例子:


这是一个类似Start的实现,继承自IStartable即可,同时这里我注入了GameSettingAssetMgrMusicManager两个对象。

在游戏开始运行时,执行了打印当前游戏语言和播放Bgm的指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GameLauncher : IStartable
{
[Inject] private readonly GameSettingAssetMgr _gameSettingAssetMgr;
[Inject] private readonly MusicManager _musicManager;

public GameLauncher(MusicManager musicManager, GameSettingAssetMgr gameSettingAssetMgr)
{
_musicManager = musicManager;
_gameSettingAssetMgr = gameSettingAssetMgr;
}

public void Start()
{
Debug.Log(_gameSettingAssetMgr.lan);
_musicManager.PlayBgm("12345SEX");
}
}

可以看到,在我运行游戏时,根据依赖自动生成了对象并执行了相关操作。

image-20221006112341489

同理我们可以,实现更多的接口来实现更多的Unity原生事件,将原本Mono脚本才能执行的内容转移到普通Class中。

现在让我们为GameLauncher类实现Update实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
实现ITickable接口

public void Tick()
{
if (Input.GetKeyDown(KeyCode.Q))
{
_uiManager.ShowPanel("GamePanel", UILayer.Mid);
Debug.Log("显示面板");
}

if (Input.GetKeyDown(KeyCode.W))
{
_uiManager.SwitchShowState("GamePanel");
Debug.Log("切换面板显示");
}
}

在Mono中使用

因为Mono不支持构造函数,所以在Mono中使用时必须使用注入特性或注入方法的方式。

1
builder.RegisterComponentInHierarchy<Character>();

注入特性

1
2
[Inject] private MusicManager _musicManager;
[Inject] private GameSettingAssetMgr _gameSettingAssetMgr;

注入方法

1
2
3
4
5
6
7
8
9
10
private MusicManager _musicManager;
private GameSettingAssetMgr _gameSettingAssetMgr;


[Inject]
public void Inject(GameSettingAssetMgr gameSettingAssetMgr, MusicManager musicManager)
{
_gameSettingAssetMgr = gameSettingAssetMgr;
_musicManager = musicManager;
}

相关链接

  1. 关于|虚拟容器 (hadashikick.jp)
  2. hadashiA/VContainer: The extra fast, minimum code size, GC-free DI (Dependency Injection) library running on Unity Game Engine. (github.com)