Làm thế nào để xử lý mã mạng?


10

Tôi quan tâm đến việc đánh giá các cách khác nhau mà mã mạng có thể "kết nối" với một công cụ trò chơi. Bây giờ tôi đang thiết kế một trò chơi nhiều người chơi và cho đến nay tôi đã xác định rằng tôi cần (ít nhất) có một luồng riêng để xử lý các ổ cắm mạng, khác với phần còn lại của công cụ xử lý vòng lặp đồ họa và kịch bản.

Tôi đã có một cách tiềm năng để làm cho một trò chơi nối mạng hoàn toàn đơn luồng, đó là kiểm tra nó sau khi kết xuất từng khung hình, sử dụng các ổ cắm không chặn. Tuy nhiên, điều này rõ ràng là không tối ưu vì thời gian kết xuất khung hình được thêm vào độ trễ mạng: Tin nhắn đến qua mạng phải đợi cho đến khi kết xuất khung hình hiện tại (và logic trò chơi) hoàn tất. Nhưng, ít nhất theo cách này, trò chơi sẽ vẫn mượt mà, ít nhiều.

Có một luồng riêng cho mạng cho phép trò chơi hoàn toàn đáp ứng với mạng, ví dụ, nó có thể gửi lại gói ACK ngay lập tức khi nhận được cập nhật trạng thái từ máy chủ. Nhưng tôi hơi bối rối về cách tốt nhất để giao tiếp giữa mã trò chơi và mã mạng. Chuỗi kết nối sẽ đẩy gói tin nhận được vào hàng đợi và luồng trò chơi sẽ đọc từ hàng đợi vào thời điểm thích hợp trong vòng lặp của nó, vì vậy chúng tôi đã không thoát khỏi sự chậm trễ lên đến một khung hình này.

Ngoài ra, có vẻ như tôi muốn luồng xử lý việc gửi các gói tách biệt với luồng đang kiểm tra các gói đi xuống đường ống, bởi vì nó sẽ không thể gửi đi trong khi nó ở giữa kiểm tra nếu có tin nhắn đến. Tôi đang suy nghĩ về chức năng của selecthoặc tương tự.

Tôi đoán câu hỏi của tôi là, cách tốt nhất để thiết kế trò chơi để đáp ứng mạng tốt nhất là gì? Rõ ràng khách hàng nên gửi đầu vào của người dùng càng sớm càng tốt đến máy chủ, vì vậy tôi có thể có mã gửi mạng đến ngay sau vòng lặp xử lý sự kiện, cả bên trong vòng lặp trò chơi. Liệu điều này có ý nghĩa gì?

Câu trả lời:


13

Bỏ qua phản ứng. Trên mạng LAN, ping không đáng kể. Trên internet, chuyến đi khứ hồi 60-100ms là một điều may mắn. Hãy cầu nguyện với các vị thần tụt hậu mà bạn không nhận được các đột biến> 3K. Phần mềm của bạn sẽ phải chạy với số lượng cập nhật / giây rất thấp vì đây là một vấn đề. Nếu bạn quay trong 25 cập nhật / giây, thì bạn đã có thời gian tối đa 40ms giữa khi bạn nhận được một gói và hành động theo nó. Và đó là trường hợp đơn luồng ...

Thiết kế hệ thống của bạn cho sự linh hoạt và chính xác. Đây là ý tưởng của tôi về cách nối một hệ thống con mạng vào mã trò chơi: Nhắn tin. Giải pháp cho rất nhiều vấn đề có thể là "nhắn tin". Tôi nghĩ rằng nhắn tin đã chữa khỏi bệnh ung thư ở chuột thí nghiệm một lần. Nhắn tin giúp tôi tiết kiệm 200 đô la trở lên trong bảo hiểm xe hơi của tôi. Nhưng nghiêm túc, nhắn tin có lẽ là cách tốt nhất để gắn bất kỳ hệ thống con nào vào mã trò chơi trong khi vẫn duy trì hai hệ thống con độc lập.

Sử dụng nhắn tin cho bất kỳ liên lạc nào giữa hệ thống con và công cụ trò chơi và cho vấn đề đó giữa bất kỳ hai hệ thống con nào. Nhắn tin giữa các hệ thống con có thể đơn giản như một đốm dữ liệu được truyền bởi con trỏ bằng std :: list.

Đơn giản chỉ cần có một hàng đợi tin nhắn gửi đi và một tham chiếu đến công cụ trò chơi trong hệ thống con mạng. Trò chơi có thể kết xuất các tin nhắn mà nó muốn gửi vào hàng đợi và gửi chúng tự động, hoặc có lẽ khi một số chức năng như "flushMessages ()" được gọi. Nếu công cụ trò chơi có một hàng đợi tin nhắn chia sẻ lớn, thì tất cả các hệ thống con cần gửi tin nhắn (logic, AI, vật lý, mạng, v.v.) đều có thể đổ tin nhắn vào đó trong đó vòng lặp trò chơi chính có thể đọc tất cả các tin nhắn và sau đó hành động phù hợp.

Tôi sẽ nói rằng chạy các ổ cắm trên một chủ đề khác là tốt, mặc dù không bắt buộc. Vấn đề duy nhất với thiết kế này là nó thường không đồng bộ (bạn không biết chính xác khi nào các gói được gửi) và điều đó có thể gây khó khăn cho việc gỡ lỗi và làm cho các vấn đề liên quan đến thời gian xuất hiện / biến mất một cách ngẫu nhiên. Tuy nhiên, nếu được thực hiện đúng cách, cả hai điều này sẽ không thành vấn đề.

Từ cấp độ cao hơn, tôi muốn nói mạng riêng biệt từ chính công cụ trò chơi. Công cụ trò chơi không quan tâm đến ổ cắm hoặc bộ đệm, nó quan tâm đến các sự kiện. Sự kiện là những thứ như "Người chơi X đã nổ súng" "Một vụ nổ tại trò chơi T đã xảy ra". Những điều này có thể được giải thích bởi công cụ trò chơi trực tiếp. Chúng được tạo từ đâu (một kịch bản, hành động của khách hàng, trình phát AI, v.v.) không quan trọng.

Nếu bạn coi hệ thống con mạng của mình như một phương tiện gửi / nhận sự kiện, thì bạn sẽ nhận được một số lợi thế so với việc chỉ gọi recv () trên một ổ cắm.

Ví dụ, bạn có thể tối ưu hóa băng thông bằng cách lấy 50 tin nhắn nhỏ (có độ dài 1-32 byte) và để hệ thống con mạng gói chúng thành một gói lớn và gửi nó. Có lẽ nó có thể nén chúng trước khi gửi nếu đó là một vấn đề lớn. Mặt khác, mã có thể giải nén / giải nén gói lớn thành 50 sự kiện riêng biệt một lần nữa để công cụ trò chơi đọc. Tất cả điều này có thể xảy ra trong suốt.

Những thứ hay ho khác bao gồm chế độ trò chơi một người chơi sử dụng lại mã mạng của bạn bằng cách có một máy khách thuần túy + ​​một máy chủ thuần túy chạy trên cùng một máy giao tiếp qua tin nhắn trong không gian bộ nhớ dùng chung. Sau đó, nếu trò chơi một người chơi của bạn hoạt động đúng, một máy khách từ xa (tức là nhiều người chơi thực sự) cũng sẽ hoạt động. Ngoài ra, nó buộc bạn phải cân nhắc trước những dữ liệu nào cần thiết cho khách hàng vì trò chơi một người chơi của bạn sẽ có vẻ đúng hoặc sai hoàn toàn. Trộn và kết hợp, chạy máy chủ VÀ trở thành khách hàng trong trò chơi nhiều người chơi - tất cả đều hoạt động dễ dàng như vậy.


Bạn đã đề cập bằng cách sử dụng một danh sách std :: đơn giản hoặc một số như vậy để truyền tin nhắn. Đây có thể là một chủ đề cho StackOverflow, nhưng có đúng là tất cả các luồng chia sẻ cùng một không gian địa chỉ không, và miễn là tôi giữ nhiều luồng không bị vặn với bộ nhớ thuộc hàng đợi của mình, tôi có ổn không? Tôi chỉ có thể phân bổ dữ liệu cho hàng đợi trên heap giống như bình thường, và chỉ cần sử dụng một số mutexes trong đó?
Steven Lu

Vâng đúng rồi. Một mutex bảo vệ tất cả các cuộc gọi đến std :: list.
PatrickB

Cảm ơn vì đã trả lời! Tôi đã đạt được rất nhiều tiến bộ với thói quen xâu chuỗi của tôi cho đến nay. Thật là một cảm giác tuyệt vời, có công cụ trò chơi của riêng tôi!
Steven Lu

4
Cảm giác đó sẽ mòn dần. Những đồng thau lớn bạn đạt được, mặc dù, ở lại với bạn.
ChrisE

@StevenLu Một phần nào đó [cực kỳ] muộn, nhưng tôi muốn chỉ ra rằng việc ngăn chặn các luồng bị vặn với bộ nhớ đồng thời có thể cực kỳ khó khăn, tùy thuộc vào cách bạn cố gắng thực hiện và mức độ hiệu quả mà bạn muốn. Hôm nay bạn đã làm điều này, tôi sẽ chỉ cho bạn một trong nhiều triển khai hàng đợi đồng thời nguồn mở tuyệt vời, vì vậy bạn không cần phải phát minh lại một bánh xe phức tạp.
Vụ kiện của Quỹ Monica

4

Tôi cần (ít nhất) có một luồng riêng để xử lý các ổ cắm mạng

Không, bạn không có.

Tôi đã có một cách tiềm năng để tạo ra một trò chơi nối mạng hoàn toàn bằng một luồng, đó là kiểm tra nó sau khi kết xuất từng khung hình, sử dụng các ổ cắm không chặn. Tuy nhiên, điều này rõ ràng là không tối ưu vì thời gian để kết xuất khung hình được thêm vào độ trễ mạng:

Không nhất thiết phải quan trọng. Khi nào logic của bạn cập nhật? Có rất ít điểm lấy dữ liệu ra khỏi mạng nếu bạn chưa thể làm gì với nó. Tương tự như vậy, có rất ít điểm trả lời nếu bạn chưa có gì để nói.

ví dụ, nó có thể gửi lại gói ACK ngay lập tức khi nhận được cập nhật trạng thái từ máy chủ.

Nếu trò chơi của bạn có nhịp độ nhanh đến mức việc chờ khung hình tiếp theo được hiển thị là một độ trễ đáng kể, thì nó sẽ gửi đủ dữ liệu mà bạn không cần gửi các gói ACK riêng biệt - chỉ bao gồm các giá trị ACK bên trong dữ liệu thông thường của bạn tải trọng, nếu bạn cần chúng ở tất cả.

Đối với hầu hết các trò chơi được nối mạng, hoàn toàn có thể có một vòng lặp trò chơi như thế này:

while 1:
    read_network_messages()
    read_local_input()
    update_world()
    send_network_updates()
    render_world()

Bạn có thể tách rời các bản cập nhật từ kết xuất, rất được khuyến khích, nhưng mọi thứ khác có thể khá đơn giản trừ khi bạn có một nhu cầu cụ thể. Chính xác thì bạn đang làm loại game nào?


7
Việc tách rời cập nhật từ kết xuất không chỉ được khuyến khích cao, nó là bắt buộc nếu bạn muốn một công cụ không phải là shit.
Tấn

Hầu hết mọi người không chế tạo động cơ, và có lẽ phần lớn các trò chơi vẫn không tách rời cả hai. Giải quyết để chuyển một giá trị thời gian trôi qua cho chức năng cập nhật hoạt động có thể chấp nhận được trong hầu hết các trường hợp.
Kylotan

2

Tuy nhiên, điều này rõ ràng là không tối ưu vì thời gian kết xuất khung hình được thêm vào độ trễ của mạng: Tin nhắn đến qua mạng phải đợi cho đến khi kết xuất khung hình hiện tại (và logic trò chơi) hoàn tất.

Điều đó không đúng chút nào. Thông báo đi qua mạng trong khi người nhận kết xuất khung hiện tại. Độ trễ mạng được kẹp vào toàn bộ số khung ở phía máy khách; có - nhưng nếu khách hàng có quá ít FPS đến mức đây là một vấn đề lớn, thì họ có vấn đề lớn hơn.


0

Truyền thông mạng nên được theo đợt. Bạn nên cố gắng cho một gói duy nhất được gửi mỗi đánh dấu trò chơi (thường là khi khung được hiển thị nhưng thực sự phải độc lập).

Các thực thể trò chơi của bạn nói chuyện với hệ thống con mạng (NSS). NSS sắp xếp các tin nhắn, ACK, v.v. và gửi một vài (hy vọng một) gói UDP có kích thước tối ưu (thường là ~ 1500 byte). NSS mô phỏng các gói, kênh, mức độ ưu tiên, gửi lại, v.v. trong khi chỉ gửi các gói UDP duy nhất.

Đọc qua hướng dẫn của người chơi trên hướng dẫn của trò chơi hoặc chỉ sử dụng ENet thực hiện nhiều ý tưởng của Glenn Fiedler.

Hoặc bạn chỉ có thể sử dụng TCP nếu trò chơi của bạn không cần phản ứng co giật. Sau đó, tất cả các vấn đề theo đợt, gửi lại và ACK biến mất. Tuy nhiên, bạn vẫn muốn có một NSS để quản lý băng thông và kênh.


0

Đừng hoàn toàn "bỏ qua phản ứng". Có rất ít để đạt được từ việc thêm độ trễ 40ms khác cho các gói đã bị trì hoãn. Nếu bạn thêm một vài khung hình (ở tốc độ 60 khung hình / giây) thì bạn đang trì hoãn việc xử lý vị trí cập nhật một vài khung hình khác. Tốt hơn là chấp nhận các gói nhanh chóng và xử lý chúng nhanh chóng để bạn cải thiện độ chính xác của mô phỏng.

Tôi đã thành công lớn trong việc tối ưu hóa băng thông bằng cách nghĩ về thông tin trạng thái tối thiểu cần thiết để thể hiện những gì hiển thị trên màn hình. Sau đó nhìn vào từng bit dữ liệu và chọn một mô hình cho nó. Thông tin vị trí có thể được thể hiện dưới dạng giá trị delta theo thời gian. Bạn có thể sử dụng các mô hình thống kê của riêng mình cho việc này và dành nhiều năm để gỡ lỗi chúng hoặc bạn có thể sử dụng thư viện để giúp bạn. Tôi thích sử dụng mô hình điểm nổi DataBlock_Predict_Float của thư viện này giúp việc tối ưu hóa băng thông được sử dụng cho biểu đồ cảnh trò chơi thực sự dễ dàng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.