Một Số Suy Nghĩ Gần Đây Về Khung ECS - nói dối e blog

Một Số Suy Nghĩ Gần Đây Về Khung ECS

Động cơ trò chơi của chúng tôi sử dụng khung ECS. Trong năm phát triển vừa qua, chúng tôi đã tích lũy được nhiều kinh nghiệm quý báu trong việc ứng dụng ECS. Tôi cũng đã chia sẻ một số bài viết liên quan đến ECS trên blog cá nhân:

  • Khung ECS trong Lua
  • Thực thể (Entity) trong ECS

Trong hai tháng gần đây, dựa trên kinh nghiệm tích lũy được, chúng tôi đã tiến hành cải tiến lớn đối với thiết kế ban đầu của khung. Những thay đổi này xuất phát từ việc hiểu sâu sắc hơn về bản chất vấn đề khung cần giải quyết, cũng như tổng kết các mẫu thiết kế (design patterns) từ thực tiễn ứng dụng trong các tình huống điển hình.

Những thay đổi quan trọng

1. Loại bỏ các kỹ thuật phức tạp không cần thiết
Chúng tôi đã loại bỏ một số yếu tố được đề cập trong bài viết “Kỹ thuật ECS trong Lua”, ví dụ như cơ chế Notify vốn được chứng minh là quá phức tạp và có thể thay thế bằng giải pháp khác. Ngoài ra, việc cho phép Component chứa phương thức hóa ra là dư thừa - một di sản của tư duy hướng đối tượng (OOP) mà chúng tôi từng bám víu.

2. Cách tiếp cận mới về System
System tồn tại với mục tiêu chính là tách biệt các nghiệp vụ khác nhau, chia bài toán động cơ thành các phần độc lập càng nhiều càng tốt, đồng thời cho phép điều khiển quy trình thông qua dữ liệu.

Chúng tôi nhận ra hai nguyên tắc quan trọng:

  • Không truyền trạng thái qua gọi hàm trực tiếp: Vì ECS xử lý hàng loạt đối tượng đồng loại, chúng tôi muốn thực hiện toàn bộ Step A cho mọi đối tượng trước khi chuyển sang Step B. Không thể áp dụng mô hình “xử lý xong Step A cho một đối tượng rồi lập tức gọi Step B” như trước đây.
  • Không lưu trạng thái trung gian trong System: Cũng do đặc thù xử lý hàng loạt, trạng thái phải được lưu trữ ở nơi khác.

3. Giải quyết bài toán thứ tự thực thi System
Thiết kế ban đầu sử dụng sắp xếp tên System và đánh dấu quan hệ phụ thuộc để xác định thứ tự. Tuy nhiên, cách tiếp cận này trở nên cồng kềnh khi số lượng System tăng lên.

Giải pháp mới:

  • Sử dụng Singleton để lưu trạng thái và trao đổi dữ liệu giữa các System
  • Thiết kế phương thức init với thứ tự xác định thông qua sắp xếp topo dựa trên quan hệ phụ thuộc
  • Phát triển ý tưởng xem init như một nhóm System đặc biệt thay vì khái niệm底层

Phân nhóm System linh hoạt

Chúng tôi nhận thấy các System phục vụ mục đích đa dạng với tần suất cập nhật khác nhau:

  • System rendering cần cập nhật mỗi khung hình
  • System vật lý & animation chạy ở tần suất cố định
  • System xử lý input nên được kích hoạt khi có sự kiện, không nên poll liên tục

Từ đó, chúng tôi thiết kế cơ chế phân nhóm System với các trình điều khiển riêng:

  • Nhóm xử lý sự kiện đầu vào
  • Nhóm rendering theo khung hình
  • Nhóm vật lý theo đồng hồ hệ thống

Một System giờ đây được xem như tập hợp các hàm xử lý thuộc các nhóm khác nhau, với quan hệ phụ thuộc được khai báo rõ ràng. Điều này cho phép linh hoạt trong việc quản lý thứ tự và ngữ cảnh thực thi.

Quản lý vòng đời Entity

Chúng tôi cải tiến cơ chế xử lý tạo/xóa Entity:

  • Tạo Entity ngay lập tức, nhưng các tương tác với hệ thống khác (vật lý, rendering…) được trì hoãn sang khung hình kế tiếp
  • Xóa Entity thông qua cơ chế trì hoãn, với phương thức world:clear_removed được gọi định kỳ

Các API mới:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- Lặp qua các Component mới tạo (chỉ xử lý 1 lần)
world:each_new(component_type, function(entity)
  -- xử lý khởi tạo vật lý hoặc rendering
end)

-- Lặp qua các Component đã xóa (có thể xử lý nhiều lần)
world:each_removed(component_type, function(entity)
  -- dọn dẹp tài nguyên
end)

-- Gọi cuối mỗi frame để hoàn tất xóa
world:clear_removed()

Hệ thống kiểu Component

Chúng tôi thiết kế hệ thống kiểu để giải quyết hai vấn đề cốt lõi:

  1. Chuyển đổi World sang dạng tuần tự (serialize/deserialize) để lưu trữ và tải từ file
  2. Tự động tạo giao diện chỉnh sửa Component trong editor thông qua cơ chế phản chiếu (reflection)

Cú pháp định nghĩa Component trong Lua:

1
2
3
4
5
6
7
8
p:type "NAME"
  ["temp"].a "int" (1)        -- Trường kiểu int với giá trị mặc định 1
  .b "OBJECT"                 -- Tham chiếu đối tượng
  [ "private" ].c "id" (0)    -- Trường private kiểu id
  .array "int[4]" { 1,2,3,4 } -- Mảng 4 phần tử
  .any "var" (nil)            -- Giá trị kiểu bất kỳ
  .map "int{}" { x = 1, y = 2 }-- Bảng ánh xạ
  .color "color"              -- Trường màu đặc biệt

Các thẻ trong dấu ngoặc vuông (ví dụ: [“temp”], [“private”]) được dùng để đánh dấu đặc tính của trường, hỗ trợ module serialize và editor trong việc xử lý phản chiếu kiểu dữ liệu.

Triết lý thiết kế mới

Chúng tôi nhận ra rằng:

  • ECS không chỉ là mô hình lập trình, mà là cách tư duy về sự phân tách trách nhiệm trong hệ thống
  • Tính linh hoạt đến từ sự trừu tượng hóa đúng mức, không phải từ việc thêm tính năng phức tạp
  • Hiệu suất đến từ việc hiểu rõ luồng dữ liệu, chứ không phải tối ưu premature

Những cải tiến này đã giúp động cơ trò chơi của chúng tôi đạt được sự cân bằng giữa hiệu suất, tính mở rộng và dễ bảo trì. Trong tương lai, chúng tôi dự định tích hợp thêm cơ chế parallel processing cho các System không có phụ thuộc dữ liệu, cũng như tối ưu hóa further cho hệ thống phản chiếu kiểu.

0%