• ในสภาพแวดล้อม 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

อ่านเพิ่มเติมที่เกี่ยวข้อง

ยังไม่มีความคิดเห็น

ยังไม่มีความคิดเห็น