- ในสภาพแวดล้อม FPS ที่รวดเร็ว ข้อมูลสถานะที่มาถึงช้ามีคุณค่าต่ำ Quake 3 จึงเลือกแนวทางลดความหน่วงด้วย การออกแบบที่เน้น UDP/IP
- NetChannel ทำหน้าที่นามธรรมการสื่อสารบน UDP ที่อาจสูญหายได้ และเซิร์ฟเวอร์จะคำนวณเฉพาะความต่างของสถานะที่จำเป็นอีกครั้งด้วย บันทึกสแนปช็อต แยกตามไคลเอนต์
- เซิร์ฟเวอร์ใช้ Master Gamestate, gamestate ล่าสุด 32 รายการ และ dummy gamestate ร่วมกัน เพื่อทำให้ การอัปเดตเต็มและการอัปเดตแบบเดลตา ใช้ขั้นตอนเดียวกัน
- หากไม่มี ACK จากไคลเอนต์ เซิร์ฟเวอร์จะเปรียบเทียบสแนปช็อตล่าสุดที่ยืนยันแล้วกับสถานะปัจจุบัน และใส่ทั้ง การเปลี่ยนแปลงที่ตกหล่นกับการเปลี่ยนแปลงใหม่ ไว้ในข้อความเดียว
- แม้ C จะไม่มี introspection ในตัว ก็ยังใช้
netField_t กับมาโครเพื่อหาความต่างของฟิลด์ได้ และ NetChannel หลีกเลี่ยงการแตกแฟรกเมนต์โดยเราเตอร์ด้วย การแบ่งล่วงหน้าที่ 1400 ไบต์
โมเดลเครือข่ายบนสมมติฐาน UDP/IP
- โมเดลเครือข่ายของ Quake 3 ได้รับการประเมินว่าเป็นส่วนที่สง่างามที่สุดส่วนหนึ่งของเอนจิน และในระดับต่ำจะนามธรรมการสื่อสารด้วยโมดูล NetChannel ซึ่งปรากฏครั้งแรกใน Quake World
- ในเกมที่รวดเร็ว ข้อมูลที่พลาดไปในการส่งครั้งแรกจะกลายเป็นข้อมูลเก่าอย่างรวดเร็ว ดังนั้นการส่งสถานะล่าสุดจึงได้เปรียบกว่าการส่งซ้ำ
- ด้วยเหตุนี้เอนจินจึง ไม่มีร่องรอยของ TCP/IP และมองว่าความหน่วงที่เกิดจากการส่งแบบเชื่อถือได้นั้นยอมรับได้ยาก
- มีการเพิ่มเลเยอร์สองชั้นที่ใช้แทนกันไม่ได้ลงใน network stack
- การเข้ารหัส โดยใช้คีย์ที่แชร์ล่วงหน้า
- การบีบอัด โดยใช้คีย์ Huffman ที่คำนวณไว้ล่วงหน้า
- เซิร์ฟเวอร์ลดขนาด UDP datagram พร้อมกับชดเชยความไม่เชื่อถือได้
- สร้าง แพ็กเก็ตเดลตา จากบันทึกสแนปช็อต
- ใช้วิธี introspection หน่วยความจำเพื่อค้นหาและส่งเฉพาะฟิลด์ที่เปลี่ยนไป
บทบาทของเซิร์ฟเวอร์และไคลเอนต์
- โฟลว์ฝั่งไคลเอนต์เรียบง่าย
- ส่งคำสั่งไปยังเซิร์ฟเวอร์ทุกเฟรม
- รับการอัปเดต gamestate จากเซิร์ฟเวอร์
- เซิร์ฟเวอร์ต้องเผยแพร่ Master Gamestate ไปยังไคลเอนต์แต่ละราย พร้อมคำนึงถึงแพ็กเก็ต UDP ที่สูญหายด้วย
- กลไกหลักประกอบด้วยสามองค์ประกอบ
- Master Gamestate: สถานะเกมที่เป็นจริงโดยทั่วไป คำสั่งจากไคลเอนต์จะเข้ามาผ่าน NetChannel แล้วถูกแปลงเป็น
event_t ก่อนที่เซิร์ฟเวอร์จะแก้ไขสถานะเกม
- gamestate ล่าสุด 32 รายการต่อไคลเอนต์: เก็บสถานะที่ส่งผ่านเครือข่ายไว้ในอาร์เรย์แบบวนซ้ำ และเรียกว่าสแนปช็อต
- dummy gamestate: สถานะที่ทุกฟิลด์เป็น 0 ใช้เป็นฐานในการสร้างเดลตาเมื่อไม่มีสถานะก่อนหน้า
- เซิร์ฟเวอร์ใช้สามองค์ประกอบนี้สร้างข้อความอัปเดตที่จะส่งต่อให้ NetChannel
- เนื่องจากต้องเก็บ gamestate จำนวนมากสำหรับแต่ละไคลเอนต์ การใช้หน่วยความจำจึงสูงขึ้น
- ตามการวัด ใช้ 8MB สำหรับผู้เล่น 4 คน
สร้างการอัปเดตเต็มและการอัปเดตบางส่วนด้วยสแนปช็อต
- ตัวอย่างใช้สถานการณ์ที่ส่งอัปเดตให้ Client1 โดยสถานะของ Client2 ประกอบด้วยสี่ฟิลด์คือ
pos[X], pos[Y], pos[Z], health
- การสื่อสารเกิดขึ้นผ่าน UDP/IP และบนอินเทอร์เน็ตข้อความอาจสูญหายได้บ่อย
-
เฟรมเซิร์ฟเวอร์แรก
- เซิร์ฟเวอร์สะท้อนอัปเดตที่ได้รับจากไคลเอนต์ทั้งหมดลงใน Master Gamestate แล้วเผยแพร่สถานะไปยัง Client1
- โมดูลเครือข่ายทำตามขั้นตอนเดียวกันทุกครั้ง
- คัดลอก Master Gamestate ไปยังสล็อตถัดไปในบันทึกของไคลเอนต์
- เปรียบเทียบสแนปช็อตที่คัดลอกกับสแนปช็อตอื่น
- ในการอัปเดตครั้งแรก ไม่มีสแนปช็อตที่ถูกต้องในบันทึกของ Client1 จึงเปรียบเทียบกับ dummy snapshot
- เนื่องจากทุกฟิลด์ของ dummy snapshot เป็น 0 ผลลัพธ์จึงเป็นการอัปเดตเต็ม
- ด้านหน้าของแต่ละฟิลด์มี bit marker ที่บอกว่าเปลี่ยนแปลงหรือไม่
- ตัวอย่างการอัปเดตเต็มใช้ 132 บิต
- รูปแบบคือ
[1 A_on32bits 1 B_on32bits 1 B_on32bits 1 C_on32bits]
-
เฟรมเซิร์ฟเวอร์ที่สอง
- ในเฟรมถัดไป Client2 เคลื่อนที่ตามแกน Y ทำให้ค่า
pos[1] กลายเป็น E
- Client1 ได้ ACK การรับอัปเดตก่อนหน้าแล้ว ดังนั้น Snapshot1 จึงอยู่ในสถานะ ACK
- เซิร์ฟเวอร์คัดลอก Master Gamestate ไปยังสล็อตบันทึกถัดไปเพื่อสร้าง Snapshot2 แล้วเปรียบเทียบกับ Snapshot1 ที่ถูกต้อง
- ผลคือส่งผ่านเครือข่ายเฉพาะ
pos[1] = E ที่เปลี่ยนไป
- เนื่องจากแต่ละฟิลด์มี bit marker การอัปเดตบางส่วนนี้จึงใช้ 36 บิต
- รูปแบบคือ
[0 1 32bitsNewValue 0 0]
-
เฟรมเซิร์ฟเวอร์ที่สาม
- ในเฟรมถัดไป Client2 เสียพลังชีวิต ทำให้
health = H
- Client1 ไม่ได้ ACK อัปเดตล่าสุด
- อาจเป็นเพราะแพ็กเก็ต UDP ของเซิร์ฟเวอร์สูญหาย หรือ ACK ของไคลเอนต์สูญหายก็ได้
- ไม่ว่ากรณีใด สแนปช็อตนั้นก็ใช้ไม่ได้
- เซิร์ฟเวอร์คัดลอก Master Gamestate ไปยังสล็อตถัดไปเพื่อสร้าง Snapshot3 แล้วเปรียบเทียบกับ Snapshot1 ที่ ACK ล่าสุด
- ข้อความที่ส่งเป็นการอัปเดตบางส่วน และรวมทั้งการเปลี่ยนแปลงก่อนหน้า
pos[1] = E กับการเปลี่ยนแปลงใหม่ health = H
- หาก Snapshot1 เก่าเกินไปจนใช้ไม่ได้ เอนจินจะส่งการอัปเดตเต็มอีกครั้งโดยอิงจาก dummy snapshot
วิธีชดเชยการสูญหายด้วยขั้นตอนเดียวกัน
- ความเรียบง่ายของระบบสแนปช็อตอยู่ที่อัลกอริทึมเดียวกันสามารถจัดการงานสองอย่างโดยอัตโนมัติ
- สร้างการอัปเดตเต็มหรือการอัปเดตบางส่วน
- ส่งข้อมูลเดิมที่ไม่ได้รับและข้อมูลใหม่ซ้ำในข้อความเดียว
- ไม่ได้จัดการการสูญหายของแพ็กเก็ต UDP ด้วยโฟลว์ซับซ้อนแยกต่างหาก แต่ชดเชยโดยคำนวณความต่างระหว่างสแนปช็อตที่ ACK ล่าสุดกับ Master Gamestate ปัจจุบัน
- เมื่อไม่มีสถานะก่อนหน้า หรือสถานะนั้นใช้ไม่ได้ จะส่งสถานะทั้งหมดโดยอิงจาก dummy snapshot เพื่อกู้คืน
วิธีค้นหาความต่างของฟิลด์ใน C
- Quake 3 แม้ภาษา C จะไม่มี introspection แต่ก็จัดเตรียมตำแหน่งของแต่ละฟิลด์ไว้ล่วงหน้าด้วยอาร์เรย์
netField_t และคำสั่งพรีโปรเซสเซอร์
netField_t เก็บชื่อฟิลด์, offset และจำนวนบิต
- มาโคร
NETF(x) ใช้ stringizing operator และการคำนวณ offset สำหรับ entityState_t เพื่อให้เขียนข้อมูลฟิลด์ได้สั้นลง
- โครงสร้างตัวอย่างมีดังนี้
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 },
...
}
- การใช้งานทั้งหมดอยู่ในบางส่วนของ MSG_WriteDeltaEntity
- Quake 3 ไม่ตีความความหมายของสิ่งที่เปรียบเทียบ แต่ไล่ตาม index, offset และ size ของ
entityStateFields แล้วส่งความต่างผ่านเครือข่าย
เหตุผลที่แบ่งล่วงหน้าที่ 1400 ไบต์
- โมดูล NetChannel แบ่งข้อความเป็นชิ้นละ 1400 ไบต์ แม้ขนาดสูงสุดของ UDP datagram จะเป็น 65507 ไบต์
- โค้ดที่เกี่ยวข้องอยู่ใน Netchan_Transmit
- เนื่องจาก MTU ของเครือข่ายส่วนใหญ่คือ 1500 ไบต์ การแบ่งที่ 1400 ไบต์จึงเป็นการเลือกเพื่อไม่ให้เราเตอร์แตกแพ็กเก็ตเป็นแฟรกเมนต์บนเส้นทางอินเทอร์เน็ต
- เหตุผลที่ต้องหลีกเลี่ยงการแตกแฟรกเมนต์โดยเราเตอร์มีสองข้อ
- เมื่อเข้าสู่เครือข่าย เราเตอร์ต้องยึดแพ็กเก็ตไว้ระหว่างแตกเป็นแฟรกเมนต์
- เมื่อออกจากเครือข่าย ต้องรอทุกชิ้นของ datagram แล้วประกอบกลับใหม่ ซึ่งมีต้นทุนสูง
ข้อความที่ต้องส่งถึงแน่นอน
- ระบบสแนปช็อตชดเชย UDP datagram ที่สูญหายในเครือข่ายได้ แต่บางข้อความและคำสั่งจำเป็นต้องส่งถึงอย่างแน่นอน
- ตัวอย่างเช่น เมื่อผู้เล่นออกจากเกม หรือเมื่อเซิร์ฟเวอร์สั่งให้ไคลเอนต์โหลดเลเวลใหม่
- การรับประกันนี้ถูกนามธรรมโดย NetChannel
อ่านเพิ่มเติมที่เกี่ยวข้อง
ยังไม่มีความคิดเห็น