VContainer Toàn Tập
Minh Khoa
Author
Mục tiêu bài viết: Hướng dẫn chi tiết cách sử dụng VContainer - DI Framework Nhanh & Hiện đại nhất cho Unity. Bài viết cung cấp kiến thức từ khái niệm nền tảng đến kiến trúc dự án thực tế.
## 🌟 PHẦN 1 — TẠI SAO LÀ VCONTAINER?
Trong hệ sinh thái Unity, Zenject từng là vua của Dependency Injection (DI). Nhưng Zenject khá nặng nề, có overhead lúc khởi tạo, sinh ra Garbage Collection (GC) và codebase cũ kỹ.
VContainer ra đời để giải quyết những vấn đề đó:
- Nhanh kinh ngạc: Khởi tạo (Resolution) gần như cấu trúc O(1) và cực kỳ tối ưu, nhanh hơn Zenject gấp nhiều lần.
- Zero Allocation: Việc resolve object (sau khi khởi tạo) không sinh ra rác (Zero GC Alloc). Hỗ trợ game chạy mượt mà không lo giật lag.
- Thuần C# và Gọn nhẹ: Không ma thuật phức tạp. API thiết kế thân thiện, dễ đọc, dễ hiểu.
- Tích hợp Unity Lifecycle: Cung cấp cơ chế "Plain C# class" (không kế thừa MonoBehaviour) nhưng vẫn dùng
Awake,Start,Updateđược.
🏗️ PHẦN 2 — CÁC KHÁI NIỆM CỐT LÕI
Để làm chủ VContainer, bạn cần hiểu 3 thành phần chính:
1. LifetimeScope (Composition Root)
Đây là cái "thùng chứa" (Container). Mọi object, service sẽ được khai báo ở đây. Nó kế thừa MonoBehaviour và được gắn vào một GameObject trong Scene (hoặc Project).
2. Builder (Đăng ký / Registration)
Là công cụ dùng trong LifetimeScope để nói với hệ thống: "Tôi có Service A, khi ai cần Interface B, hãy đưa Service A cho họ."
3. Lifetime (Vòng đời của Object)
Khi VContainer tạo ra một Object cho bạn, Object đó sống bao lâu?
- Lifetime.Singleton: Chỉ duy nhất 1 instance được tạo ra. Ai gọi cũng trả về đúng instance này.
- Lifetime.Transient: Cứ mỗi lần có ai xin (resolve), nó tạo ra một
new Object()mới tinh. - Lifetime.Scoped: Hoạt động như Singleton, nhưng giới hạn trong một
LifetimeScopecụ thể. (Thường dùng với Child Scopes).
🛠️ PHẦN 3 — SỬ DỤNG CƠ BẢN (STEP-BY-STEP)
Bài toán: Ta có một AudioService và một PlayerController cần xài nó.
B1: Viết class độc lập (Không cần MonoBehaviour)
csharp
public class AudioService
{
public void Play(stringsfxName)=>Debug.Log($"Playing:{sfxName}");
}
public class PlayerController
{
private readonly AudioService_audio;
// VContainer sẽ tự động "nhét" (inject) AudioService vào đây
public PlayerController(AudioService audio)
{
_audio =audio;
}
publicvoidJump()
{
_audio.Play("jump_sound");
}
}
B2: Trói chúng lại bằng LifetimeScope Tạo script GameLifetimeScope.cs và gắn vào 1 GameObject rỗng trong scene.
csharp
usingVContainer;
usingVContainer.Unity;
public class GameLifetimeScope :LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
// Đăng ký AudioService là Singleton
builder.Register<AudioService>(Lifetime.Singleton);
// Đăng ký PlayerController. Nhưng vì nó không nằm trong scene,
// nếu đăng ký thường sẽ không ai gọi nó chạy.
// VContainer có EntryPoint để tự động chạy.
builder.RegisterEntryPoint<PlayerController>();
}
}
🌀 PHẦN 4 — ENTRY POINT & UNITY LIFECYCLE (THAY THẾ MONOBEHAVIOUR)
Một trong những tính năng mạnh nhất của VContainer là tách Logic ra khỏi MonoBehaviour. Thay vì hàm Start(), Update(), ta dùng các Interface.
csharp
usingVContainer;
usingVContainer.Unity;
public class EnemyManager :IInitializable,IStartable,ITickable,IDisposable
{
// Chạy tương tự Awake()
public void Initialize() {Debug.Log("Init"); }
// Chạy tương tự Start()
public void Start() {Debug.Log("Start"); }
// Chạy tương tự Update()
public void Tick() {Debug.Log("Update..."); }
// Chạy tương tự OnDestroy()
public void Dispose() {Debug.Log("Clean up"); }
}
Đăng ký class này:
csharp
// Đăng ký như một EntryPoint (Gắn nó vào vòng lặp game của Unity)
builder.RegisterEntryPoint<EnemyManager>();
🔌 PHẦN 5 — CÁC LOẠI INJECTION VÀ CÁC KIỂU ĐĂNG KÝ (REGISTRATION)
1. Cách Inject (Bơm Dependency)
A. Constructor Injection (🎯 Khuyên dùng 100%) Đảm bảo object luôn ở trạng thái hoàn chỉnh khi được tạo.
csharp
public class ShopLogic {
public ShopLogic(CurrencyService currency) { ... }
}
B. Method Injection (Dùng cho MonoBehaviour) Vì MonoBehaviour bị Unity tự tạo (không qua Constructor được), ta dùng thuộc tính [Inject].
csharp
public class UIHealthBar :MonoBehaviour
{
private HealthSystem _health;
[Inject]// Hàm này sẽ tự động chạy ngay sau khi Awake() xong
public void Construct(HealthSystem health)
{
_health =health;
}
}
2. Các Kiểu Đăng Ký Chuyên Sâu
Đăng ký Interface (AsImplementedInterfaces)
csharp
// Bất cứ ai cần ILogger, sẽ nhận được UnityLogger
builder.Register<UnityLogger>(Lifetime.Singleton).AsImplementedInterfaces();
// Hoặc ngắn gọn:
builder.Register<UnityLogger>(Lifetime.Singleton).As<ILogger>();
Đăng ký MonoBehaviour có sẵn trong Scene
csharp
// Kéo thả component từ Inspector vào LifetimeScope, rồi đăng ký
[SerializeField]private Camera _mainCamera;
// ... trong Register
builder.RegisterComponent(_mainCamera);
Inject nhiều Implementations (IEnumerable)
csharp
builder.Register<KeyboardInput>(Lifetime.Singleton).As<IInputProvider>();
builder.Register<GamepadInput>(Lifetime.Singleton).As<IInputProvider>();
// Tiêu thụ
public class PlayerMovement
{
// Lấy hết các loại input provider
public PlayerMovement(IEnumerable<IInputProvider>inputs) { ... }
}
🏭 PHẦN 6 — FACTORY PATTERN VÀ SPAWN PREFAB THỰC TIỄN
Đây là phần khiến nhiều người lúng túng nhất: "Làm sao spawn Enemy/Bullet lúc chọt game mà Enemy/Bullet đó cũng được Inject?"
Cách 1: Inject IObjectResolver vào Component Spawn
csharp
usingVContainer;
usingVContainer.Unity;
public class EnemySpawner :MonoBehaviour
{
// IObjectResolver là cốt lõi của VContainer, dùng để "đẻ" ra object
[Inject]private readonly IObjectResolver _resolver;
public GameObject enemyPrefab;
public void Spawn()
{
// Unity Instantiate bình thường sẽ không Inject được.
// Phải dùng Instantiate của VContainer
varenemy =_resolver.Instantiate(enemyPrefab);
}
}
Cách 2: Dùng Func (Factory Delegate) Thuần C# (TỐT HƠN)
Đừng bắt class logic của bạn phụ thuộc IObjectResolver.
Trong LifetimeScope:
csharp
public Enemy Prefab;
...
// Đăng ký một Factory (một hàm trả về Enemy)
builder.RegisterFactory<Enemy>(resolver=>
{
return ()=>resolver.Instantiate(Prefab);
},Lifetime.Scoped);
Trong class Spawner:
csharp
public class WaveController
{
private readonly Func<Enemy> _enemyFactory;
// Inject ra cái máy ép (factory), chứ không tiêm sẵn Enemy
public WaveController(Func<Enemy>enemyFactory)
{
_enemyFactory =enemyFactory;
}
public void SpawnWave()
{
Enemy newEnemy =_enemyFactory();// Chạy delegate sẽ Instatiate và Inject Enemy
}
}
🌳 PHẦN 7 — CẤU TRÚC SCOPE & THỰC TIỄN DỰ ÁN LỚN (HIERARCHICAL)
Một game lớn không thể nhét hết vào 1 LifetimeScope. Ta sẽ tổ chức theo dạng cây: ProjectScope -> SceneScope -> ChildScope
1. ProjectScope (Global Scope)
Khởi tạo tự động khi game chạy, tồn tại mãi mãi (DontDestroyOnLoad).
- Chứa: SaveSystem, NetworkManager, AudioSystem, UserProfile.
- Cách tạo: Tạo prefab
ProjectLifetimeScope. Vào Project Settings -> VContainer -> Parent và gán nó làm Root.
2. SceneScope
- Nằm trong các scene (VD: MainMenu, GamePlay). Các Scope này tự động nhận ProjectScope làm Cha.
- Chứa: EnemyManager, UIManager của Scene, LevelController.
- Lợi ích: Service của SceneScope có thể xài chung với Service của ProjectScope (Ví dụ MenuController xài AudioSystem của Cha). Nhưng Cha không truy xuất được con.
3. SubScope (Đẻ Scope động)
Ví dụ: Spawn ra một chiếc xe Tăng. Chiếc xe Tăng phức tạp đến mức cần 1 cục Container riêng cho nó (TurretController, TrackController, Health).
csharp
varchildScope =_resolver.CreateScope(builder=>
{
builder.Register<TurretController>(Lifetime.Scoped);
// ...
});
// Resolve thử từ Child Scope
varturret =childScope.Resolve<TurretController>();
// Xong việc thì tiêu hủy scope
childScope.Dispose();
⛔ PHẦN 8 — ANTI-PATTERNS (NHỮNG LỖI CẦN TRÁNH VỚI VCONTAINER)
Tránh những sai lầm sau để codebase không bị "thối" (code smell):
1. Service Locator Pattern Trá Hình
csharp
// ❌ XẤU: Truyền toàn bộ VContainer vào Object (Thế này thì dùng DI làm gì?)
public class Player {
public Player(IObjectResolver resolver) {
varaudio =resolver.Resolve<AudioService>();
}
}
// ✅ TỐT: Chân thật và Rõ ràng về cái gì mình cần
public class Player {
public Player(AudioService audio) { ... }
}
2. Inject Vô Tội Vạ Vào Data Classes
Không bao giờ tiêm service vào Model/Data Class (Data transfer objects, JSON cấu trúc...). Dependency Injection (DI) chỉ dùng cho Services / Logic / Controllers. Dữ liệu nên "ngu" (dumb data).
3. Vòng Lặp Phụ Thuộc (Circular Dependency)
csharp
// Lỗi này sẽ sập game lúc chạy:
public ServiceA(ServiceB b) { }
public ServiceB(ServiceA a) { }
// Cách sửa: Rút Interface ra, hoặc quy hoạch lại luồng dữ liệu, sử dụng Event
//Message Broker thay cho Dependency trực tiếp.