Trong các dòng game chiến thuật quy mô lớn (SLG, RTS), khi số lượng đơn vị tăng từ vài trăm lên đến hàng chục nghìn, mô hình Cây hành vi hướng đối tượng truyền thống (Node-based OOP) thường trở thành nút thắt cổ chai nghiêm trọng cho CPU. Việc kiểm tra bằng Profiler thường cho thấy hàng loạt lời gọi hàm ảo (virtual methods), tình trạng truy vết con trỏ (pointer chasing) rời rạc trong bộ nhớ và tỷ lệ Cache Miss cực cao tại các tầng L1/L2.
Để duy trì tiến trình xử lý dưới 16ms cho 10.000 thực thể, chúng ta cần thay đổi tư duy thiết kế theo ba hướng: Phẳng hóa cấu trúc dữ liệu (Flattening), Phân cấp xử lý theo khoảng cách (AI LOD) và Thực thi không cấp phát bộ nhớ (Heap-less execution).
Hạn chế của mô hình OOP cổ điển
Việc triển khai các node (Selector, Sequence, Action) kế thừa từ một lớp cơ sở BTNode gặp phải các vấn đề về hiệu năng khi mở rộng quy mô:
- Tính cục bộ dữ liệu kém (Poor Cache Locality): Mỗi Node là một đối tượng trên Heap. Một cây hành vi gồm 30 node sẽ nằm rải rác ở nhiều địa chỉ bộ nhớ khác nhau, khiến CPU Prefetcher không thể dự đoán và nạp trước dữ liệu.
- Chi phí bảng hàm ảo (Vtable Overhead): Mỗi bước
Tick()yêu cầu tra cứu bảng hàm ảo, ngăn cản trình biên dịch thực hiện Inlining và làm rối loạn bộ dự đoán nhánh (Branch Predictor). - Áp lực Garbage Collector (GC): Các biến tạm hoặc Delegate trong các node Decorator sinh ra lượng lớn rác bộ nhớ mỗi khung hình, gây ra tình trạng giật lag do GC Collect.
Giải pháp 1: Thực thi dựa trên dòng lệnh Bytecode (Flat Bytecode Engine)
Thay vì cấu trúc cây phân cấp, chúng ta biên dịch toàn bộ logic hành vi thành một mảng cấu trúc (struct) liên tục trong bộ nhớ, tương tự như các chỉ thị Bytecode. Mỗi thực thể chỉ cần lưu trữ một ngữ cảnh thực thi (Context) cực nhẹ gồm con trỏ lệnh (PC) và ngăn xếp trạng thái.
// Định nghĩa mã lệnh và trạng thái
public enum ENodeOp : byte
{
Sequence,
Selector,
CheckCondition,
MoveToTarget,
ExecuteAttack
}
public enum ENodeStatus : byte
{
Ready,
Success,
Failure,
Running
}
// Cấu trúc lệnh phẳng: Tối ưu kích thước (8 bytes)
public struct BTCommand
{
public ENodeOp OpCode;
public byte ChildCount;
public short OnFailJump; // Bước nhảy khi thất bại (Short-circuit)
public short OnSuccessJump; // Bước nhảy khi thành công
public short DataIndex; // Trỏ đến mảng tham số tĩnh
}
// Ngữ cảnh thực thi siêu nhẹ cho mỗi Agent (16-24 bytes)
public struct AgentBTState
{
public short PC; // Program Counter
public ENodeStatus LastStatus;
public byte StackPointer;
public unsafe fixed short NodeStack[8]; // Ngăn xếp nội bộ, tránh cấp phát Heap
}
public struct BTBytecodeRunner
{
public void Tick(ref AgentBTState state, ReadOnlySpan<BTCommand> bytecode, Blackboard data)
{
while (state.PC < bytecode.Length)
{
var cmd = bytecode[state.PC];
switch (cmd.OpCode)
{
case ENodeOp.CheckCondition:
bool isReady = data.HasTarget;
if (!isReady)
{
state.PC += cmd.OnFailJump;
state.LastStatus = ENodeStatus.Failure;
}
else
{
state.PC++;
state.LastStatus = ENodeStatus.Success;
}
break;
case ENodeOp.ExecuteAttack:
// Thực thi logic nguyên tử
state.LastStatus = ENodeStatus.Running;
return;
default:
state.PC++;
break;
}
}
state.PC = 0; // Reset cho chu kỳ sau
}
}
Giải pháp 2: Phân cấp tần suất xử lý (Spatial AI LOD)
Không phải tất cả 10.000 đơn vị đều cần cập nhật AI ở tần suất 60Hz. Các thực thể ở xa hoặc ngoài tầm nhìn có thể giảm tần suất xử lý để tiết kiệm tài nguyên CPU.
| Cấp độ (LOD) | Khoảng cách | Tần suất (Hz) | Chiến lược cập nhật |
|---|---|---|---|
| LOD 0 | 0 - 20m | 30 - 60 Hz | Cập nhật đầy đủ mọi khung hình |
| LOD 1 | 20 - 60m | 10 Hz | Cập nhật mỗi 3-5 khung hình |
| LOD 2 | 60 - 150m | 2 Hz | Chỉ cập nhật di chuyển cơ bản |
| LOD 3 | > 150m | 0.5 Hz | Cập nhật thống kê hoặc đóng băng |
// Bộ điều phối lập lịch theo khung hình (Bucket Scheduler)
public struct AIScheduler
{
public static bool CanUpdate(int entityId, int lodLevel, int frameCount)
{
return lodLevel switch
{
0 => true, // LOD0 chạy mỗi khung hình
1 => (entityId + frameCount) % 4 == 0, // 1/4 đơn vị mỗi khung hình
2 => (entityId + frameCount) % 15 == 0,
3 => (entityId + frameCount) % 60 == 0,
_ => false
};
}
}
Kết quả thực nghiệm
Thử nghiệm trên cấu hình CPU đa nhân với 10.000 thực thể, mỗi thực thể sở hữu cây hành vi gồm 25 node logic (Tuần tra/Tìm địch/Tấn công):
- Trước khi tối ưu (OOP + 30Hz Full Tick): Thời gian xử lý trung bình 28.4ms, gây sụt giảm FPS nghiêm trọng. Lượng rác (GC Alloc) phát sinh khoảng 4.2 MB/s.
- Sau khi tối ưu (Bytecode + AI LOD + Parallel Jobs): Thời gian xử lý giảm xuống còn 0.85ms. Lượng rác phát sinh gần như bằng 0 B/s.
Chuyển đổi cấu trúc AI từ "Cây đối tượng" sang "Dòng dữ liệu nén" kết hợp với lập lịch thông minh là con đường duy nhất để vận hành các hệ thống AI quy mô vạn đơn vị trên phần cứng dân dụng hiện nay.