- Trong môi trường FPS tốc độ cao, thông tin trạng thái đến muộn có giá trị thấp, nên Quake 3 chọn thiết kế xoay quanh UDP/IP để giảm độ trễ
- NetChannel trừu tượng hóa việc giao tiếp trên UDP có thể bị mất gói, và máy chủ dùng lịch sử snapshot theo từng client để chỉ tính lại phần khác biệt trạng thái cần thiết
- Máy chủ dùng Master Gamestate, 32 gamestate gần nhất và dummy gamestate cùng lúc để tạo cập nhật đầy đủ và cập nhật delta bằng cùng một quy trình
- Nếu không có ACK từ client, máy chủ so sánh snapshot đã được xác nhận gần nhất với trạng thái hiện tại để đưa các thay đổi bị bỏ lỡ và thay đổi mới vào cùng một thông điệp
- Dù C không có introspection tích hợp, vẫn có thể tìm khác biệt giữa các trường bằng
netField_t và macro, còn NetChannel thì chia sẵn thành các gói 1400 byte để tránh phân mảnh ở router
Mô hình mạng dựa trên giả định UDP/IP
- Mô hình mạng của Quake 3 được xem là phần thanh lịch nhất trong engine, và ở mức thấp hơn, việc giao tiếp được trừu tượng hóa bằng mô-đun NetChannel xuất hiện lần đầu trong Quake World
- Trong game tốc độ cao, thông tin bị lỡ ở lần truyền đầu tiên sẽ nhanh chóng trở thành thông tin cũ, nên gửi trạng thái mới nhất có lợi hơn là gửi lại dữ liệu cũ
- Vì vậy trong engine không có dấu vết nào của TCP/IP, do độ trễ do truyền tin cậy tạo ra bị xem là khó chấp nhận
- Hai lớp loại trừ lẫn nhau được thêm vào network stack
- Mã hóa bằng khóa dùng chung từ trước
- Nén bằng khóa Huffman được tính sẵn
- Máy chủ vừa giảm kích thước UDP datagram vừa bù lại tính không tin cậy
- Tạo gói delta bằng lịch sử snapshot
- Chỉ tìm và gửi các trường đã thay đổi bằng cách introspection trên bộ nhớ
Vai trò của máy chủ và client
- Luồng phía client khá đơn giản
- Mỗi frame gửi lệnh lên máy chủ
- Nhận cập nhật gamestate từ máy chủ
- Máy chủ phải phát tán Master Gamestate tới từng client, đồng thời phải tính đến cả các gói UDP bị mất
- Cơ chế cốt lõi gồm ba thành phần
- Master Gamestate: trạng thái game đúng một cách tổng quát; lệnh từ client đi vào qua NetChannel, được chuyển thành
event_t, rồi sửa đổi trạng thái game trên máy chủ
- 32 gamestate gần nhất cho mỗi client: các trạng thái đã gửi qua mạng được lưu trong mảng vòng, gọi là snapshot
- dummy gamestate: trạng thái mà mọi trường đều bằng 0, dùng làm mốc tạo delta khi không có trạng thái trước đó
- Từ ba thành phần này, máy chủ tạo thông điệp cập nhật để chuyển cho NetChannel
- Vì phải giữ nhiều gamestate theo từng client nên mức dùng bộ nhớ khá lớn
- Theo số liệu đo được, với 4 người chơi cần 8MB
Tạo cập nhật đầy đủ và cập nhật từng phần bằng snapshot
- Ví dụ dùng tình huống gửi cập nhật cho Client1, trong đó trạng thái của Client2 gồm bốn trường
pos[X], pos[Y], pos[Z], health
- Việc giao tiếp diễn ra qua UDP/IP, và trên Internet thì thông điệp có thể thường xuyên bị mất
-
Frame máy chủ thứ nhất
- Máy chủ áp dụng mọi cập nhật nhận từ các client vào Master Gamestate rồi phát tán trạng thái cho Client1
- Mô-đun mạng luôn làm theo cùng một quy trình
- Sao chép Master Gamestate vào ô kế tiếp trong lịch sử của client
- So sánh snapshot vừa sao chép với snapshot khác
- Ở lần cập nhật đầu tiên, lịch sử của Client1 không có snapshot hợp lệ nên sẽ so sánh với dummy snapshot
- Vì mọi trường của dummy snapshot đều là 0 nên kết quả là một bản cập nhật đầy đủ
- Trước mỗi trường đều có một bit marker cho biết trường đó có thay đổi hay không
- Bản cập nhật đầy đủ trong ví dụ dùng 132 bit
- Dạng của nó là
[1 A_on32bits 1 B_on32bits 1 B_on32bits 1 C_on32bits]
-
Frame máy chủ thứ hai
- Ở frame kế tiếp, Client2 di chuyển theo trục Y nên giá trị
pos[1] trở thành E
- Client1 đã ACK bản cập nhật trước đó, nên Snapshot1 chuyển sang trạng thái ACK
- Máy chủ sao chép Master Gamestate vào ô lịch sử tiếp theo để tạo Snapshot2, rồi so sánh với Snapshot1 hợp lệ
- Kết quả là chỉ
pos[1] = E được truyền qua mạng
- Vì mỗi trường đều có bit marker nên bản cập nhật từng phần này dùng 36 bit
- Dạng của nó là
[0 1 32bitsNewValue 0 0]
-
Frame máy chủ thứ ba
- Ở frame sau đó, Client2 mất máu nên
health = H
- Client1 không ACK bản cập nhật cuối cùng
- Có thể gói UDP từ máy chủ đã bị mất, hoặc ACK từ client đã bị mất
- Dù là trường hợp nào thì snapshot đó cũng không dùng được
- Máy chủ sao chép Master Gamestate vào ô tiếp theo để tạo Snapshot3, rồi so sánh với Snapshot1 đã được ACK gần nhất
- Thông điệp được gửi đi là một bản cập nhật từng phần, bao gồm cả thay đổi cũ
pos[1] = E lẫn thay đổi mới health = H
- Nếu Snapshot1 đã quá cũ và không còn dùng được nữa, engine sẽ quay lại dùng dummy snapshot làm mốc và gửi lại bản cập nhật đầy đủ
Cách cùng một quy trình bù cho mất gói
- Tính đơn giản của hệ thống snapshot nằm ở chỗ cùng một thuật toán tự động xử lý hai việc
- Tạo cập nhật đầy đủ hoặc cập nhật từng phần
- Gửi lại trong một thông điệp cả thông tin trước đó chưa được nhận lẫn thông tin mới
- Thay vì xử lý mất gói UDP bằng một luồng phức tạp riêng, hệ thống chỉ tính phần khác biệt giữa snapshot đã được ACK gần nhất và Master Gamestate hiện tại để bù lại
- Khi không có trạng thái trước đó hoặc trạng thái đó không còn dùng được, hệ thống phục hồi bằng cách gửi toàn bộ trạng thái dựa trên dummy snapshot
Cách tìm khác biệt giữa các trường trong C
- Quake 3 dùng ngôn ngữ C vốn không có introspection, nhưng vị trí của từng trường được dựng sẵn bằng mảng
netField_t và chỉ thị tiền xử lý
netField_t chứa tên trường, offset và số bit
- Macro
NETF(x) dùng toán tử stringizing và cách tính offset đối với entityState_t để viết gọn thông tin trường
- Cấu trúc ví dụ như sau
typedef struct { char *name; int offset; int bits; } netField_t;
// using the stringizing operator to save typing...
#define NETF(x) #x,(int)&((entityState_t*)0)->x
netField_t entityStateFields[] = {
{ NETF(pos.trTime), 32 },
{ NETF(pos.trBase[0]), 0 },
{ NETF(pos.trBase[1]), 0 },
...
}
- Toàn bộ phần triển khai có trong một phần của MSG_WriteDeltaEntity
- Quake 3 không diễn giải ý nghĩa của đối tượng đang so sánh, mà chỉ lần theo index, offset và size trong
entityStateFields rồi truyền phần khác biệt qua mạng
Vì sao chia sẵn thành 1400 byte
- Mô-đun NetChannel chia thông điệp thành các mảnh 1400 byte dù kích thước tối đa của UDP datagram là 65507 byte
- Mã liên quan nằm trong Netchan_Transmit
- Vì MTU của đa số mạng là 1500 byte, việc chia thành 1400 byte là lựa chọn nhằm tránh để router phải phân mảnh gói trên đường truyền Internet
- Có hai lý do phải tránh phân mảnh ở router
- Khi đi vào mạng, router phải giữ gói lại trong lúc phân mảnh
- Khi đi ra khỏi mạng, phải chờ đủ mọi mảnh của datagram rồi mới thực hiện việc lắp ráp lại vốn tốn kém
Những thông điệp bắt buộc phải được chuyển tới
- Hệ thống snapshot có thể bù cho UDP datagram bị mất trên mạng, nhưng vẫn có những thông điệp và lệnh bắt buộc phải được chuyển tới
- Ví dụ như khi người chơi thoát game hoặc khi máy chủ yêu cầu client tải một màn chơi mới
- Sự bảo đảm này được NetChannel trừu tượng hóa
Bài đọc liên quan
Chưa có bình luận nào.