ECS Architecture: Cách Viết Game Code Sạch Và Hiệu Năng Cao
Entity Component System (ECS) là một architectural pattern đang ngày càng phổ biến trong game development, đặc biệt khi hiệu năng là ưu tiên hàng đầu. Khác với Object-Oriented Programming (OOP) truyền thống, ECS tách biệt data (Components) khỏi logic (Systems) và sử dụng composition thay vì inheritance. Bài viết này sẽ giúp bạn hiểu ECS từ gốc rễ và áp dụng nó trong dự án game của mình. Khám phá các dự án game sử dụng ECS tại kho source game LamGame.
1. Vấn Đề Của OOP Trong Game Development
OOP là paradigm phổ biến nhất trong software development và cũng là cách Unity truyền thống hoạt động với MonoBehaviour. Tuy nhiên, OOP có một số vấn đề khi áp dụng cho game với nhiều entity. Đầu tiên là inheritance hierarchy phức tạp. Giả sử bạn có class hierarchy: Entity → Character → Enemy → FlyingEnemy. Bây giờ bạn muốn thêm FlyingAlly - nó cần fly behavior từ FlyingEnemy nhưng lại là ally, không phải enemy. OOP buộc bạn phải duplicate code hoặc tạo complex inheritance tree.
Thứ hai là cache miss và memory layout kém. Trong OOP, mỗi object chứa tất cả data của nó trong một block memory liên tục. Khi bạn iterate qua 10,000 enemies để update position, CPU phải load toàn bộ enemy object vào cache mặc dù chỉ cần truy cập position field. Các field khác như inventory, dialogue, quest data chiếm cache space vô ích, gây cache miss liên tục và giảm hiệu năng nghiêm trọng.
Thứ ba là tight coupling. Trong OOP, behavior thường được implement trong class chứa data, tạo ra coupling giữa data và logic. Khi bạn muốn thay đổi cách movement hoạt động, bạn phải sửa trong mọi class có movement code. Điều này vi phạm Single Responsibility Principle và làm code khó maintain.
2. ECS: Ba Thành Phần Cốt Lõi
Entity chỉ là một ID (thường là integer) đại diện cho một "thing" trong game world. Entity không chứa data hay logic - nó chỉ là identifier để nhóm các component lại với nhau. Một entity có thể là player, enemy, bullet, particle, hoặc bất kỳ thứ gì.
Component là pure data container, không chứa logic. Mỗi component đại diện cho một khía cạnh của entity. Ví dụ: Position component chứa x, y, z; Velocity component chứa vx, vy, vz; Health component chứa currentHP, maxHP; SpriteRenderer component chứa sprite reference và color. Entity được định nghĩa bởi tập hợp components nó có - entity có Position + Velocity + Health + AI là enemy, entity có Position + Velocity + Health + PlayerInput là player.
System chứa toàn bộ logic, hoạt động trên các entity có tập hợp component phù hợp. MovementSystem query tất cả entity có Position và Velocity, rồi update position dựa trên velocity. DamageSystem query entity có Health và DamageReceived, rồi giảm HP. Mỗi system chỉ quan tâm đến các component nó cần, không biết và không cần biết về các component khác.
// Component definitions - chỉ chứa data
public struct Position : IComponentData { public float3 Value; }
public struct Velocity : IComponentData { public float3 Value; }
public struct Health : IComponentData { public float Current; public float Max; }
public struct EnemyTag : IComponentData { } // Tag component, không có data
// System - chỉ chứa logic
public partial struct MovementSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
float dt = SystemAPI.Time.DeltaTime;
foreach (var (pos, vel) in
SystemAPI.Query<RefRW<Position>, RefRO<Velocity>>())
{
pos.ValueRW.Value += vel.ValueRO.Value * dt;
}
}
}
public partial struct DamageSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (health, damage, entity) in
SystemAPI.Query<RefRW<Health>, RefRO<DamageReceived>>()
.WithEntityAccess())
{
health.ValueRW.Current -= damage.ValueRO.Amount;
ecb.RemoveComponent<DamageReceived>(entity);
if (health.ValueRO.Current <= 0)
ecb.DestroyEntity(entity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
3. Data-Oriented Design (DOD)
ECS thường đi kèm với Data-Oriented Design - một approach tập trung vào cách data được tổ chức trong memory để tối ưu cho CPU cache. Thay vì Array of Structures (AoS) như OOP, DOD sử dụng Structure of Arrays (SoA). Trong AoS, data của mỗi entity nằm liên tục: [Entity1{pos,vel,hp}, Entity2{pos,vel,hp}, ...]. Trong SoA, data cùng loại nằm liên tục: [pos1,pos2,pos3,...], [vel1,vel2,vel3,...], [hp1,hp2,hp3,...].
Khi MovementSystem iterate qua positions và velocities, CPU chỉ cần load hai mảng liên tục vào cache - không có data thừa, không có cache miss. Trên modern CPU với cache line 64 bytes, SoA layout có thể nhanh hơn AoS từ 5-10 lần cho các operation iterate qua nhiều entity. Đây là lý do chính ECS có hiệu năng vượt trội so với OOP cho game có nhiều entity.
4. Unity DOTS (Data-Oriented Technology Stack)
Unity DOTS là implementation chính thức của ECS trong Unity, bao gồm ba thành phần chính: Entities package (ECS framework), Burst Compiler (compile C# thành highly optimized native code), và Job System (multi-threaded task scheduling). Ba thành phần này kết hợp tạo ra hiệu năng gần native C++ trong Unity.
Burst Compiler là "phép thuật" đằng sau hiệu năng của DOTS. Nó compile C# code (với một số restrictions) thành SIMD-optimized native code, tận dụng tối đa instruction set của CPU (SSE, AVX, NEON). Code compiled bởi Burst có thể nhanh hơn C# thông thường từ 10-100 lần cho các math-heavy operations. Job System cho phép distribute work across multiple CPU cores một cách an toàn, với dependency tracking tự động để tránh race conditions.
// Kết hợp ECS + Burst + Jobs cho maximum performance
[BurstCompile]
public partial struct OptimizedMovementJob : IJobEntity
{
public float DeltaTime;
void Execute(ref Position pos, in Velocity vel)
{
pos.Value += vel.Value * DeltaTime;
}
}
public partial struct MovementSystemV2 : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
new OptimizedMovementJob
{
DeltaTime = SystemAPI.Time.DeltaTime
}.ScheduleParallel(); // Tự động chạy parallel trên nhiều cores
}
}
5. Khi Nào Nên Dùng ECS?
ECS không phải silver bullet cho mọi game. Nó phù hợp nhất cho game có nhiều entity cùng loại cần update mỗi frame: RTS với hàng nghìn unit, bullet hell với hàng vạn đạn, simulation game với nhiều NPC, particle system phức tạp. Đối với game có ít entity nhưng logic phức tạp (adventure game, visual novel, puzzle game), OOP truyền thống với MonoBehaviour hoàn toàn đủ tốt và dễ develop hơn nhiều.
Bạn cũng có thể sử dụng hybrid approach: phần lớn game logic dùng MonoBehaviour, chỉ các hệ thống cần hiệu năng cao (particle, crowd simulation, pathfinding) dùng ECS. Unity DOTS hỗ trợ hybrid workflow này thông qua managed components và entity-gameobject bridge.
6. ECS Frameworks Ngoài Unity
Ngoài Unity DOTS, có nhiều ECS framework khác đáng chú ý. Arch ECS là lightweight C# ECS framework với API đơn giản và hiệu năng cao, phù hợp cho cả Unity và non-Unity projects. Flecs là C/C++ ECS framework mạnh mẽ với query language phong phú. EnTT là C++ ECS header-only library được sử dụng trong nhiều game engine. Trong Godot, bạn có thể sử dụng các ECS addon từ cộng đồng hoặc implement pattern tương tự thông qua GDExtension.
ECS là một paradigm shift trong cách nghĩ về game architecture. Nó đòi hỏi thay đổi mindset từ "object có behavior" sang "data được transform bởi systems". Quá trình chuyển đổi có thể khó khăn ban đầu, nhưng kết quả là code sạch hơn, dễ test hơn, và hiệu năng cao hơn đáng kể. Nếu bạn quan tâm đến game architecture và engine programming, hãy xem các cơ hội việc làm game tại LamGame.