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:
|
|
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:
- Chuyển đổi World sang dạng tuần tự (serialize/deserialize) để lưu trữ và tải từ file
- 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:
|
|
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.