วิธีจัดการกับ netcode


10

ฉันสนใจที่จะประเมินวิธีต่างๆที่ netcode สามารถ "เชื่อมต่อ" เอ็นจิ้นเกม ตอนนี้ฉันกำลังออกแบบเกมที่มีผู้เล่นหลายคนและจนถึงตอนนี้ฉันได้ตัดสินใจแล้วว่าฉันต้องการ (อย่างน้อยที่สุด) ที่มีเธรดแยกต่างหากเพื่อจัดการซ็อกเก็ตเครือข่ายซึ่งแตกต่างจากส่วนอื่น ๆ ของเครื่องยนต์ที่จัดการห่วงกราฟิกและสคริปต์

ฉันมีวิธีที่เป็นไปได้วิธีหนึ่งในการสร้างเกมเครือข่ายแบบเธรดเดียวทั้งหมดซึ่งต้องตรวจสอบเครือข่ายหลังจากแสดงแต่ละเฟรมโดยใช้ซ็อกเก็ตที่ไม่ปิดกั้น อย่างไรก็ตามสิ่งนี้ไม่เหมาะสมอย่างชัดเจนเพราะเวลาที่ใช้ในการเรนเดอร์เฟรมจะถูกเพิ่มเข้ากับความล่าช้าของเครือข่าย: ข้อความที่มาถึงผ่านเครือข่ายจะต้องรอจนกว่าการเรนเดอร์เฟรมปัจจุบัน แต่อย่างน้อยวิธีการเล่นเกมนี้จะยังคงราบรื่นไม่มากก็น้อย

การมีเธรดแยกต่างหากสำหรับการเชื่อมต่อเครือข่ายช่วยให้เกมสามารถตอบสนองต่อเครือข่ายได้อย่างสมบูรณ์ตัวอย่างเช่นมันสามารถส่งแพ็กเก็ต ACK กลับทันทีเมื่อได้รับการอัพเดตสถานะจากเซิร์ฟเวอร์ แต่ฉันสับสนเล็กน้อยเกี่ยวกับวิธีที่ดีที่สุดในการสื่อสารระหว่างรหัสเกมและรหัสเครือข่าย เธรดเครือข่ายจะผลักแพ็คเก็ตที่ได้รับไปยังคิวและเธรดเกมจะอ่านจากคิวในเวลาที่เหมาะสมระหว่างการวนรอบของมันดังนั้นเราจึงไม่ได้กำจัดความล่าช้าแบบ up-to-one-frame นี้

นอกจากนี้ดูเหมือนว่าฉันต้องการให้เธรดที่จัดการการส่งแพ็กเก็ตแยกจากที่กำลังตรวจสอบแพ็กเก็ตที่ลงมาในท่อเพราะมันจะไม่สามารถส่งออกได้ในขณะที่มันอยู่ตรงกลาง ตรวจสอบว่ามีข้อความเข้ามาหรือไม่ ฉันกำลังคิดเกี่ยวกับการทำงานของselectหรือคล้ายกัน

ฉันเดาคำถามของฉันคืออะไรวิธีที่ดีที่สุดในการออกแบบเกมสำหรับการตอบสนองเครือข่ายที่ดีที่สุด? เห็นได้ชัดว่าลูกค้าควรส่งข้อมูลผู้ใช้โดยเร็วที่สุดเท่าที่จะทำได้ไปยังเซิร์ฟเวอร์ดังนั้นฉันจึงอาจมีรหัสส่งสุทธิมาทันทีหลังจากวนรอบการประมวลผลเหตุการณ์ทั้งภายในวงเกม สิ่งนี้สมเหตุสมผลหรือไม่?

คำตอบ:


13

ละเว้นการตอบสนอง บน LAN ping นั้นไม่มีนัยสำคัญ บนอินเทอร์เน็ตการเดินทางไปกลับ 60-100ms เป็นพร อธิษฐานเผื่อเหล่าเทพแห่งความล่าช้าที่คุณจะไม่ได้รับมากกว่า> 3K ซอฟต์แวร์ของคุณจะต้องมีการอัปเดต / วินาทีในระดับต่ำมากเพื่อให้เกิดปัญหา หากคุณถ่ายภาพ 25 อัปเดต / วินาทีแสดงว่าคุณมีเวลาสูงสุด 40ms เมื่อคุณได้รับแพ็คเก็ตและดำเนินการต่อ และนั่นคือกรณีเธรดเดียว ...

ออกแบบระบบของคุณเพื่อความยืดหยุ่นและความถูกต้อง นี่คือแนวคิดของฉันในการเชื่อมต่อระบบย่อยเครือข่ายเข้ากับรหัสเกม: การส่งข้อความ การแก้ปัญหาจำนวนมากสามารถ "ส่งข้อความ" ฉันคิดว่าการส่งข้อความรักษาโรคมะเร็งในหนูทดลองครั้งหนึ่ง การส่งข้อความช่วยฉันประหยัด $ 200 หรือมากกว่าในประกันภัยรถยนต์ของฉัน แต่จริงจังการส่งข้อความอาจเป็นวิธีที่ดีที่สุดในการแนบระบบย่อยใด ๆ กับโค้ดเกมในขณะที่ยังคงรักษาระบบย่อยอิสระสองระบบ

ใช้การส่งข้อความสำหรับการสื่อสารระหว่างระบบย่อยเครือข่ายและเอ็นจิ้นเกมและสำหรับเรื่องนั้นระหว่างระบบย่อยสองระบบ การส่งข้อความระหว่างระบบย่อยสามารถทำได้ง่ายเพียงแค่หยดข้อมูลผ่านตัวชี้โดยใช้ std :: list

เพียงแค่มีคิวข้อความที่ส่งออกและอ้างอิงถึงเอ็นจิ้นเกมในระบบย่อยเครือข่าย เกมสามารถถ่ายโอนข้อความที่ต้องการส่งไปยังคิวที่ส่งออกและให้ส่งโดยอัตโนมัติหรือบางทีเมื่อเรียกฟังก์ชันบางอย่างเช่น "flushMessages ()" ถ้าเอ็นจิ้นเกมมีคิวข้อความที่มีขนาดใหญ่และใช้ร่วมกันแล้วระบบย่อยทั้งหมดที่จำเป็นในการส่งข้อความ (ตรรกะ, AI, ฟิสิกส์, เครือข่ายและอื่น ๆ ) สามารถถ่ายโอนข้อความทั้งหมดไปยังที่ที่วนหลักของเกมสามารถอ่านข้อความทั้งหมดได้ แล้วปฏิบัติตาม

ฉันจะบอกว่าใช้ซ็อกเก็ตในหัวข้ออื่นได้ดี แต่ไม่จำเป็น ปัญหาเดียวของการออกแบบนี้คือโดยทั่วไปแล้วจะไม่ตรงกัน (คุณไม่รู้แน่ชัดว่าเมื่อใดที่มีการส่งแพ็กเก็ต) และทำให้ยากต่อการดีบักและทำให้ปัญหาเกี่ยวกับเวลาปรากฏขึ้น / หายไปแบบสุ่ม ถึงกระนั้นหากทำอย่างถูกต้องแล้วสิ่งเหล่านี้จะไม่เป็นปัญหา

จากระดับที่สูงขึ้นฉันจะบอกว่าการเชื่อมต่อเครือข่ายแยกต่างหากจากเอ็นจิ้นเกม เอ็นจิ้นของเกมไม่สนใจซ็อกเก็ตหรือบัฟเฟอร์ แต่จะสนใจเกี่ยวกับเหตุการณ์ เหตุการณ์คือสิ่งต่าง ๆ เช่น "ผู้เล่น X ยิงกระสุน" "เกิดการระเบิดที่เกม T" เอ็นจินของเกมสามารถตีความได้โดยตรง สิ่งที่พวกเขาสร้างขึ้นจาก (สคริปต์การกระทำของลูกค้าผู้เล่น AI และอื่น ๆ ) ไม่สำคัญ

หากคุณถือว่าระบบย่อยเครือข่ายของคุณเป็นวิธีการส่ง / รับเหตุการณ์คุณจะได้รับประโยชน์มากกว่าเพียงแค่เรียก recv () บนซ็อกเก็ต

คุณสามารถปรับแบนด์วิดท์ให้เหมาะสมเช่นโดยการรับ 50 ข้อความเล็ก (ความยาว 1-32 ไบต์) และให้แพ็คเกจระบบย่อยของเครือข่ายเป็นแพ็กเก็ตขนาดใหญ่และส่ง บางทีมันอาจบีบอัดพวกเขาก่อนที่จะส่งหากเป็นเรื่องใหญ่ ในอีกด้านหนึ่งโค้ดสามารถคลายการบีบอัดแพ็กเก็ตขนาดใหญ่ใน 50 เหตุการณ์ที่ไม่ต่อเนื่องอีกครั้งเพื่อให้เอ็นจิ้นเกมอ่าน ทั้งหมดนี้สามารถเกิดขึ้นได้อย่างโปร่งใส

สิ่งที่ยอดเยี่ยมอื่น ๆ ได้แก่ โหมดเกมผู้เล่นเดี่ยวที่นำรหัสเครือข่ายของคุณกลับมาใช้ใหม่โดยมีไคลเอนต์แท้ + เซิร์ฟเวอร์บริสุทธิ์ที่ทำงานบนเครื่องเดียวกันสื่อสารผ่านการส่งข้อความในพื้นที่หน่วยความจำที่ใช้ร่วมกัน จากนั้นหากเกมผู้เล่นเดี่ยวของคุณทำงานอย่างถูกต้องไคลเอนต์ระยะไกล (เช่นมัลติเพลเยอร์จริง) ก็จะทำงานเช่นกัน นอกจากนี้ยังบังคับให้คุณพิจารณาล่วงหน้าว่าลูกค้าต้องการข้อมูลอะไรบ้างเนื่องจากเกมผู้เล่นเดี่ยวของคุณอาจดูถูกหรือผิดอย่างสิ้นเชิง มิกซ์แอนด์แมทช์รันเซิร์ฟเวอร์และเป็นไคลเอนต์ในเกมที่มีผู้เล่นหลายคน - มันทำงานได้อย่างง่ายดาย


คุณพูดถึงการใช้รายการ std :: ง่ายๆหรือบางอย่างเพื่อส่งข้อความ นี่อาจเป็นหัวข้อสำหรับ StackOverflow แต่เป็นความจริงหรือไม่ที่เธรดทั้งหมดใช้พื้นที่ที่อยู่เดียวกันและตราบใดที่ฉันป้องกันไม่ให้เธรดหลายเธรดพร้อมกับหน่วยความจำที่อยู่ในคิวของฉันพร้อมกันฉันควรจะไม่เป็นไร ฉันสามารถจัดสรรข้อมูลสำหรับคิวบนฮีปเหมือนปกติและใช้ mutex บางตัวได้หรือไม่
Steven Lu

ใช่ที่ถูกต้อง. mutex ปกป้องการเรียกทั้งหมดไปยังรายการ std ::
PatrickB

ขอบคุณสำหรับการตอบกลับ! ฉันมีความคืบหน้ามากกับกิจวัตรการทำเกลียวของฉันจนถึงตอนนี้ มันเป็นความรู้สึกที่ยอดเยี่ยมมีเครื่องมือเกมของฉันเอง!
Steven Lu

4
ความรู้สึกนั้นจะเสื่อม แม้ว่าทองเหลืองขนาดใหญ่ที่คุณได้รับจะอยู่กับคุณ
ChrisE

@StevenLu ค่อนข้างช้า [อย่างมาก] แต่ฉันต้องการชี้ให้เห็นว่าการป้องกันไม่ให้เธรดเกลียวด้วยหน่วยความจำพร้อมกันนั้นอาจเป็นเรื่องยากมากขึ้นอยู่กับวิธีที่คุณพยายามทำและประสิทธิภาพที่คุณต้องการ ถ้าคุณทำเช่นนี้วันนี้ฉันจะชี้ให้คุณเห็นว่าเป็นหนึ่งในการใช้งานคิวโอเพ่นซอร์สคิวแบบโอเพ่นซอร์สที่ยอดเยี่ยมดังนั้นคุณไม่จำเป็นต้องสร้างวงล้อที่ซับซ้อน
คดีของกองทุนโมนิกา

4

ฉันต้องการ (อย่างน้อยที่สุด) มีเธรดแยกต่างหากเพื่อจัดการซ็อกเก็ตเครือข่าย

ไม่คุณทำไม่ได้

ฉันมีวิธีที่เป็นไปได้วิธีหนึ่งในการสร้างเกมเครือข่ายแบบเธรดเดียวทั้งหมดซึ่งต้องตรวจสอบเครือข่ายหลังจากแสดงแต่ละเฟรมโดยใช้ซ็อกเก็ตที่ไม่ปิดกั้น อย่างไรก็ตามสิ่งนี้ไม่เหมาะสมอย่างชัดเจนเพราะเวลาที่ใช้ในการเรนเดอร์เฟรมถูกเพิ่มเข้ากับความล่าช้าของเครือข่าย

ไม่จำเป็นว่าจะสำคัญ ตรรกะของคุณอัปเดตเมื่อใด มีจุดเล็กน้อยในการรับข้อมูลออกจากเครือข่ายหากคุณยังไม่สามารถทำอะไรได้เลย ในทำนองเดียวกันมีจุดตอบสนองเล็กน้อยถ้าคุณยังไม่มีอะไรจะพูด

เช่นสามารถส่งแพ็กเก็ต ACK กลับทันทีที่ได้รับการอัพเดตสถานะจากเซิร์ฟเวอร์

หากเกมของคุณมีความรวดเร็วที่รอให้เฟรมถัดไปแสดงผลนั้นมีความล่าช้าอย่างมากนั่นจะเป็นการส่งข้อมูลที่เพียงพอซึ่งคุณไม่จำเป็นต้องส่งแพ็กเก็ต ACK แยกต่างหาก - เพียงรวมค่า ACK ไว้ในข้อมูลปกติของคุณ payloads ถ้าคุณต้องการพวกเขาเลย

สำหรับเกมเครือข่ายส่วนใหญ่เป็นไปได้อย่างสมบูรณ์แบบที่จะมีลูปเกมดังนี้:

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

คุณสามารถแยกการอัปเดตจากการเรนเดอร์ซึ่งแนะนำเป็นอย่างยิ่ง แต่ทุกสิ่งทุกอย่างอาจยังคงง่ายอยู่เว้นแต่คุณมีความต้องการเฉพาะ คุณทำเกมประเภทใดอยู่


7
การปรับปรุงการแยกตัวออกจากการเรนเดอร์ไม่แนะนำอย่างยิ่งมันเป็นสิ่งจำเป็นหากคุณต้องการเอ็นจิ้นที่ไม่ใช่อึ
AttackHobo

คนส่วนใหญ่ไม่ได้สร้างเครื่องมือและอาจเป็นเกมส่วนใหญ่ที่ยังไม่แยกสองเกม การทรุดตัวเพื่อส่งผ่านค่าเวลาที่ผ่านไปยังฟังก์ชั่นการอัพเดทจะทำงานได้ในกรณีส่วนใหญ่
Kylotan

2

อย่างไรก็ตามสิ่งนี้ไม่เหมาะสมอย่างชัดเจนเพราะเวลาที่ใช้ในการเรนเดอร์เฟรมจะถูกเพิ่มเข้ากับความล่าช้าของเครือข่าย: ข้อความที่มาถึงผ่านเครือข่ายจะต้องรอจนกว่าการเรนเดอร์เฟรมปัจจุบัน

นั่นไม่จริงเลย ข้อความผ่านเครือข่ายในขณะที่ผู้รับทำให้เฟรมปัจจุบัน ความล่าช้าของเครือข่ายถูก จำกัด จำนวนเฟรมไว้ที่ฝั่งไคลเอ็นต์ ใช่ - แต่ถ้าลูกค้ามี FPS น้อยมากว่านี่เป็นเรื่องใหญ่แล้วพวกเขาก็มีปัญหาที่ใหญ่กว่า


0

ควรสื่อสารแบตช์เครือข่าย คุณควรมุ่งมั่นสำหรับแพ็คเก็ตเดียวที่ส่งแต่ละเห็บเกม (ซึ่งบ่อยครั้งเมื่อมีการแสดงเฟรม แต่ควรเป็นอิสระจริงๆ)

เอนทิตีเกมของคุณพูดคุยกับระบบย่อยเครือข่าย (NSS) NSS จะทำการส่งข้อความ, ACK, ฯลฯ และส่งสองสาม (หวังว่าจะเป็นหนึ่ง) แพ็คเก็ต UDP ที่มีขนาดที่เหมาะสมที่สุด (โดยปกติประมาณ 1500 ไบต์) NSS จำลองแพ็คเก็ต, ช่องทาง, จัดลำดับความสำคัญส่งใหม่ ฯลฯ ขณะที่ส่งแพ็กเก็ต UDP เพียงรายการเดียว

อ่านบทแนะนำของเกมหรือเพียงแค่ใช้ENetซึ่งนำแนวคิดของ Glenn Fiedler มาใช้มากมาย

หรือคุณสามารถใช้ TCP ได้หากเกมของคุณไม่ต้องการปฏิกิริยากระตุก จากนั้นปัญหาการแบตช์ส่งใหม่และปัญหา ACK จะหายไป อย่างไรก็ตามคุณยังต้องการให้ NSS จัดการแบนด์วิดท์และแชนเนล


0

อย่า "เพิกเฉยต่อการตอบสนอง" อย่างสมบูรณ์ มีเพียงเล็กน้อยที่จะได้รับจากการเพิ่มเวลาแฝงอีก 40 มิลลิวินาทีให้กับแพ็คเก็ตที่ล่าช้าอยู่แล้ว หากคุณเพิ่มเฟรมสองเฟรม (ที่ 60fps) แสดงว่าคุณเลื่อนการประมวลผลตำแหน่งอัปเดตเฟรมอีกสองเฟรม เป็นการดีกว่าที่จะยอมรับแพ็กเก็ตอย่างรวดเร็วและประมวลผลอย่างรวดเร็วเพื่อให้คุณปรับปรุงความถูกต้องของการจำลอง

ฉันประสบความสำเร็จอย่างมากในการเพิ่มประสิทธิภาพแบนด์วิดท์โดยคิดถึงข้อมูลสถานะขั้นต่ำที่จำเป็นเพื่อแสดงสิ่งที่ปรากฏบนหน้าจอ จากนั้นดูข้อมูลแต่ละบิตและเลือกแบบจำลอง ข้อมูลตำแหน่งสามารถแสดงเป็นค่าเดลต้าเมื่อเวลาผ่านไป คุณสามารถใช้แบบจำลองทางสถิติของคุณเองสำหรับสิ่งนี้และใช้เวลานานในการดีบั๊กพวกเขาหรือคุณสามารถใช้ห้องสมุดเพื่อช่วยคุณได้ ฉันชอบที่จะใช้โมเดลจุดลอยตัวของไลบรารีนี้DataBlock_Predict_Float ทำให้ง่ายต่อการปรับแต่งแบนด์วิดท์ที่ใช้สำหรับกราฟฉากเกม

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.