1 คะแนน โดย GN⁺ 2024-06-14 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Serious Engine 1 วาง โหมดเล่นคนเดียว·มัลติเพลเยอร์·การเล่นเดโมซ้ำ ไว้บน deterministic simulation เดียวกัน โดยบันทึก·ส่งการกระทำของผู้เล่นและบล็อก game stream แทนสถานะทั้งหมดในทุก tick
  • เดโมและมัลติเพลเยอร์แชร์สถานะเกมเริ่มต้น แล้วใช้ input delta เช่น CPlayerAction ดังนั้น determinism ของ seed สุ่มและ game logic จึงเป็นหัวใจของการซิงก์
  • เลเยอร์เครือข่ายจัดการ sequence number, ACK, การส่งซ้ำ, การจำกัดแบนด์วิดท์ และบัฟเฟอร์แยกตามการเชื่อมต่อเองบน UDP เพื่อพยายามให้เล่นได้แม้บนสายที่ช้า
  • CNetworkMessage รองรับการ serialize, การบีบอัด LZ77/LZRW1 และ delta encoding แบบ XOR แต่ข้อความรวมถึงแชตไม่ได้เข้ารหัส และอาจถูกบีบอัดเพียงอย่างเดียว
  • โมเดล client-server ของ Serious Sam ประหยัดแบนด์วิดท์ด้วยการให้แต่ละไคลเอนต์คง simulation ของตัวเองไว้ แต่ต้องมีเครื่องมือประกอบอย่าง การตรวจสอบการซิงก์·prediction·การส่งซ้ำ·การตรวจ CRC

โครงสร้าง simulation ร่วมของ Serious Engine

  • ซอร์สโค้ด Serious Engine 1 ถูกเปิดเป็น GNU GPL v2 ในปี 2016 และการวิเคราะห์นี้อ้างอิงจากการอ่านและดีบัก codebase ที่เปิดเผยนี้
  • Serious Sam ถูกออกแบบเป็น เกมมัลติเพลเยอร์ ตั้งแต่ต้น และแคมเปญเล่นคนเดียวภายในก็ทำงานเหมือนกรณีพิเศษของโครงสร้างมัลติเพลเยอร์
  • โหมดที่เอนจินรองรับมีดังนี้
    • แคมเปญเล่นคนเดียวแบบออฟไลน์
    • การเล่นร่วมมือแบบออนไลน์, LAN, โลคัล และโหมดเกมหลายแบบ
    • split screen ที่มีผู้เล่นหลายคนเข้าร่วมจากไคลเอนต์เดียว
    • การบันทึกและเล่นเดโมซ้ำ

การบันทึกเดโม: บันทึกการกระทำแทนสถานะทั้งหมด

  • หากเก็บเดโมเป็นสถานะเกมทั้งหมดทุก tick ไฟล์จะใหญ่ Serious Engine จึงบันทึก สถานะเกมทั้งหมด หนึ่งครั้ง ณ จุดเริ่มบันทึก แล้วหลังจากนั้นบันทึก บล็อก game stream ทุก tick
  • บล็อก game stream มีชนิดข้อความต่อไปนี้
    • MSG_SEQ_ALLACTIONS: การกระทำของผู้เล่น
    • MSG_SEQ_ADDPLAYER: เพิ่มผู้เล่น
    • MSG_SEQ_REMPLAYER: ลบผู้เล่น
    • MSG_SEQ_PAUSE: หยุดชั่วคราวหรือยกเลิก
    • MSG_SEQ_CHARACTERCHANGE: เปลี่ยนคุณสมบัติตัวละครของผู้เล่น
  • หัวใจคือ MSG_SEQ_ALLACTIONS โดยเอนจิน deserialize ออบเจ็กต์ CPlayerAction สำหรับผู้เล่นที่ active แต่ละคน แล้วนำไปใช้กับ CPlayerTarget
  • CPlayerAction เก็บสถานะผู้เล่น
    • ความเร็วการเคลื่อนที่อิงพิกัดโลก pa_vTranslation
    • การหมุนตัวละครอิงพิกัดโลก pa_aRotation
    • การหมุนมุมมองอิงพิกัดโลก pa_aViewRotation
    • ปุ่มที่กำลังกดอยู่ pa_ulButtons
    • timestamp หน่วยมิลลิวินาทีอิง TSC pa_llCreated
  • ตอนเล่นซ้ำ จะอ่านสถานะเกมเริ่มต้น แล้วนำการกระทำของผู้เล่นในแต่ละ tick ไปใช้เหมือนการเล่นจริง

ทำไมจึงต้องมี determinism

  • โครงสร้างนี้ตั้งอยู่บนสมมติฐานว่าทุกอย่างในเกม คาดการณ์ได้ทั้งหมด และมีเพียงการกระทำของผู้เล่นเท่านั้นที่เปลี่ยนเกมได้
  • ค่าสุ่มก็ถูกจัดการด้วยตัวสร้างเลขสุ่มเทียมที่ใช้ seed ซึ่งเป็นส่วนหนึ่งของสถานะเกม
    • CEntity::IRnd() ใช้ CSessionState::Rnd()
    • ses_ulRandomSeed ถูก initialize ระหว่างการ deserialize สถานะเกม
  • หากใช้ตัวสร้างเลขสุ่มจริงหรือใช้ seed ต่างกัน แม้เล่นเดโมเดียวกันผลลัพธ์ก็อาจต่างไป และนำไปสู่ การซิงก์ไม่ตรงกัน

floating point และการประมวลผล tick

  • Serious Sam เวอร์ชัน PC เดิมออกเฉพาะ Windows ดังนั้นสมมติฐานว่าจะใช้คอมไพเลอร์และ runtime เดียวกันช่วยลดปัญหาการซิงก์ของ floating point ได้
  • renderer เป็น DLL และการเรียก OpenGL หรือ DirectX API อาจเปลี่ยน precision ของ FPU ได้ Serious Engine จึงใช้ guard สำหรับ precision เช่น CSetFPUPrecision FPUPrecision(FPT_24BIT)
  • ไม่พบจุดที่ตั้งค่า rounding control อย่างชัดเจน แต่มี assert ที่ตรวจสถานะ _RC_NEAR ด้วย _controlfp
  • game logic แยกจาก framerate การเรนเดอร์
    • การเรนเดอร์เปลี่ยนไปตามฮาร์ดแวร์และการตั้งค่า และดูเหมือนถูกจำกัดภายในไว้ที่ 500 FPS
    • game logic ถูกตรึงไว้ที่ 20 tick ต่อวินาที
  • การเคลื่อนไหวที่ลื่นไหลสร้างจาก linear interpolation ระหว่าง tick ปัจจุบันกับ tick ก่อนหน้า และสามารถปิด interpolation ได้จากคอนโซลด้วย /net_bLerping=0

เลเยอร์แพ็กเก็ตที่สร้างเองบน UDP

  • ในมัลติเพลเยอร์ของ Serious Engine ยังมีชื่อฟังก์ชันอย่าง StartPeerToPeer_t เหลืออยู่ แต่โมเดลจริงเป็นโครงสร้าง client-server
  • เซิร์ฟเวอร์รับและประมวลผลข้อความจากไคลเอนต์ แล้วส่งข้อมูลที่เกี่ยวข้องต่อให้ไคลเอนต์ทั้งหมด
  • ผู้เล่นแต่ละคนรัน simulation ของตัวเองคล้ายระบบเดโม และรับข้อมูลการกระทำของผู้เล่นอื่นเพื่อเดินสถานะต่อ
  • เครือข่ายใช้ UDP และวางโปรโตคอลของตัวเองทับเพื่อแก้ปัญหาลำดับสลับและแพ็กเก็ตสูญหาย
  • CPacket จัดการลำดับและความน่าเชื่อถือของแพ็กเก็ต
    • pa_ulSequence: ใช้เป็น sequence number สำหรับจัดลำดับและตัดแพ็กเก็ตซ้ำ
    • pa_ubReliable: เก็บ flag ความน่าเชื่อถือ
    • pa_ubRetryNumber: ติดตามจำนวนครั้งการส่งซ้ำ
    • pa_tvSendWhen: ใช้กับเวลาที่กำหนดให้ส่งและ congestion control
  • แพ็กเก็ตแบบ reliable จะรอ ACK และหากไม่มี ACK ก็จะส่งซ้ำ
    • จำนวน retry สูงสุดตั้งด้วย net_iMaxSendRetries และค่าเริ่มต้นดูเหมือนเป็น 10
    • ช่วงเวลาระหว่างการส่งซ้ำตั้งด้วย net_fSendRetryWait และค่าเริ่มต้นดูเหมือนเป็น 0.5f
  • แพ็กเก็ตแบบ reliable สามารถสร้าง stream ที่แบ่งเป็นหลายแพ็กเก็ตได้
    • แพ็กเก็ตแรกคือ UDP_PACKET_RELIABLE_HEAD
    • แพ็กเก็ตสุดท้ายคือ UDP_PACKET_RELIABLE_TAIL
    • แพ็กเก็ต reliable เดี่ยวจะมีทั้งสอง flag
  • แพ็กเก็ตแบบ unreliable ไม่สร้าง stream เพราะเมื่อสูญหาย stream อาจเสียได้

lifecycle ของการเชื่อมต่อและ packet routing

  • CCommunicationInterface รับผิดชอบการสื่อสารในเลเยอร์แพ็กเก็ต และมีฟังก์ชัน interface สำหรับเซิร์ฟเวอร์·ไคลเอนต์·broadcast
  • interface ฝั่งเซิร์ฟเวอร์และไคลเอนต์ถือว่ารู้ปลายทางที่เชื่อมต่ออยู่แล้ว ส่วน broadcast interface ใช้ส่งรับกับที่อยู่ใดก็ได้
  • CCommunicationInterface รักษา master buffer สองชุด
    • cci_pbMasterInput: deserialize แพ็กเก็ต UDP ที่เข้ามาเป็น CPacket แล้วเก็บไว้
    • cci_pbMasterOutput: serialize CPacket ที่จะส่ง แล้วส่งผ่าน socket API
  • abstraction การสื่อสารจริงรายไคลเอนต์อยู่ที่ CClientInterface
    • เซิร์ฟเวอร์มี interface ของผู้เล่นแต่ละคนผ่าน array cm_aciClients
    • ไคลเอนต์สื่อสารกับเซิร์ฟเวอร์ผ่าน cm_ciLocalClient
    • ทั้งไคลเอนต์และเซิร์ฟเวอร์ใช้ cm_ciBroadcast เพื่อสร้างการเชื่อมต่อ
  • adr_uwID ของ CAddress ใช้เป็นตัวระบุไคลเอนต์เฉพาะหรือเครื่องหมายแพ็กเก็ต broadcast
    • หากค่าเป็น '//' หรือ 0 จะเป็นแพ็กเก็ต broadcast
    • ค่าอื่นคือ client ID ภายใน session

การสร้างการเชื่อมต่อและกลไกความปลอดภัยพื้นฐาน

  • เพื่อเชื่อมต่อกับเซิร์ฟเวอร์ ไคลเอนต์จะส่ง แพ็กเก็ต reliable broadcast ที่มี flag UDP_PACKET_CONNECT_REQUEST
  • หากมีไคลเอนต์จากที่อยู่และพอร์ตเดียวกันเชื่อมต่ออยู่แล้ว เซิร์ฟเวอร์จะละเว้นคำขอ
  • ถ้าเป็นไคลเอนต์ใหม่ เซิร์ฟเวอร์จะหา client interface ที่ว่าง แล้วทำงานต่อไปนี้
    • สร้างตัวระบุเฉพาะของไคลเอนต์นั้น
    • ส่งตัวระบุกลับไปยังไคลเอนต์ด้วยแพ็กเก็ต reliable broadcast UDP_PACKET_CONNECT_RESPONSE
  • ตัวระบุไม่ได้ใช้เพียง index คงที่ แต่สร้างจากการผสมส่วนหนึ่งของค่าตัวจับเวลากับ client index
  • หากจะปลอมเป็นผู้เล่นอื่นต้องเดา uwID ให้ถูก จึงลด attack surface ลง
  • หากผู้เล่นที่ยังไม่ได้เชื่อมต่อส่งแพ็กเก็ตที่ไม่ใช่ broadcast Serious Engine อาจพิมพ์คำเตือนลงคอนโซล

เล่นคนเดียวและเดโมเป็นกรณีพิเศษของการเชื่อมต่อโลคัล

  • การเล่นคนเดียวและการเล่นเดโมซ้ำภายในก็มีเซิร์ฟเวอร์และไคลเอนต์อยู่ แต่ทำงานใน process เดียวกัน
  • ไม่จำเป็นต้องใช้ socket ระหว่าง process เดียวกัน Client_OpenLocal() จึงเชื่อม client interface โลคัลกับ interface ฝั่งเซิร์ฟเวอร์เข้าหากัน
  • CClientInterface สองตัวที่เชื่อมกันจะย้ายแพ็กเก็ตจาก output buffer ของฝั่งหนึ่งไปยัง input buffer ของอีกฝั่งด้วย ExchangeBuffers
  • การเล่นโลคัลไม่จำเป็นต้องผ่าน master I/O buffer และ socket เครือข่ายจริง

เลเยอร์ข้อความเครือข่าย

  • CNetworkMessage เป็น abstraction ของข้อความเหนือแพ็กเก็ต และอ่านเขียนได้เหมือน stream
  • ข้อความถูก serialize·deserialize ด้วย Read, Write, ReadBits, WriteBits และ operator <<, >>
  • สามารถบรรจุข้อความย่อยได้ และหลังเขียนข้อมูลที่ต้องการแล้วสามารถใช้ Shrink เพื่อลดขนาดบัฟเฟอร์ให้พอดีกับขนาดข้อมูล
  • บัฟเฟอร์ของ CNetworkMessage ถูก allocate ผ่าน AllocMemory และดูเหมือนภายในเรียก malloc
  • มี CLinearAllocator อยู่ แต่ไม่พบจุดที่ใช้งาน และ message buffer ถูก allocate·reallocate บ่อย

การบีบอัดและ delta encoding

  • ข้อความสามารถถูกบีบอัดด้วย compressor ที่ระบุ หรือ compressor เริ่มต้นตาม message type
  • MESSAGETYPE ใช้ 6 บิตล่างเป็น type และ 2 บิตที่เหลือเป็นวิธีบีบอัด
    • LZ77 CzlibCompressor
    • LZRW1 CLZCompressor
    • ไม่บีบอัด
  • การบีบอัดเริ่มต้นดูเหมือนเป็น LZRW1 และเปลี่ยนได้ด้วย shell variable net_iCompression
  • CPlayerAction ไม่ได้ถูกส่งตรง ๆ แต่สร้าง delta ด้วยการ XOR การกระทำปัจจุบันกับการกระทำล่าสุด แล้วส่งไป
  • ฝั่งรับ XOR delta กับการกระทำล่าสุดอีกครั้งเพื่อกู้ CPlayerAction เดิม
  • delta จะบีบอัดได้ดีขึ้นเมื่อข้อมูลเปลี่ยนแปลงน้อย
    • ปุ่มที่กดมักคงอยู่หลายเฟรม
    • ความเร็วและการหมุนมุมมองก็ไม่ได้แกว่งไปทั่วทั้งช่วงของ floating point มากนัก
  • เมื่อเซิร์ฟเวอร์ส่งการกระทำของผู้เล่นหลายคนพร้อมกันผ่าน MSG_SEQ_ALLACTIONS วิธีนี้อาจยิ่งได้ผลมากขึ้น

การเข้ารหัสข้อความและแชต

  • ข้อความของ Serious Engine ไม่ได้เข้ารหัส
  • หากปิดการบีบอัดด้วย net_iCompression=0 ข้อความแชตในเกมจะมองเห็นเป็น plaintext ใน payload ของแพ็กเก็ต UDP
  • ในสถานการณ์จริง หากเปิดการบีบอัดไว้ ผู้ดักฟังแพ็กเก็ตต้องระบุและคลาย LZ compressed stream แต่ข้อมูลที่ต้องใช้ทั้งหมดอยู่ในแพ็กเก็ต
  • เกมในยุคนั้นจำนวนมากไม่ได้จัดการเรื่องการเข้ารหัส และหากทำกลไกอย่าง authentication กับ key exchange ก็อาจเพิ่มความซับซ้อน
  • เว็บในยุคนั้นส่วนใหญ่ก็ยังเป็น HTTP

เลเยอร์ game session

  • CNetworkLibrary แม้ชื่อจะเป็นอย่างนั้น แต่จัดการ game session รวมถึงสถานะเกม CSessionState
  • CNetworkLibrary สืบทอด CMessageDispatcher ที่กล่าวถึงก่อนหน้า
  • ตอนเริ่มเซิร์ฟเวอร์ เอนจินทำขั้นตอนต่อไปนี้
    • initialize การเก็บ CRC เพื่อเตรียมตรวจว่าไคลเอนต์ที่เชื่อมต่อมีไฟล์เดียวกับเซิร์ฟเวอร์หรือไม่
    • สร้าง CSessionState ใหม่แล้ว serialize เพื่อเก็บเป็นสถานะพื้นฐาน ga_pubDefaultState
    • โหลดอินสแตนซ์โลกโลคัล
    • initialize global communication interface
    • initialize สถานะ session โลคัล และเมื่อไคลเอนต์เชื่อมต่อก็เตรียมส่ง state delta ระหว่างสถานะพื้นฐานกับสถานะปัจจุบันของเซิร์ฟเวอร์ได้
    • จบการเก็บ CRC แล้วบันทึกไว้ใน ga_ulCRC
  • การตรวจ CRC ใกล้เคียงกับ การตรวจพบการซิงก์ไม่ตรงกันตั้งแต่เนิ่น ๆ มากกว่าการป้องกันโกง
  • ขั้นตอนที่ไคลเอนต์เข้าร่วมมี flow ต่อไปนี้
    • initialize สถานะ session โลคัลที่ว่างและ communication interface
    • ส่ง build version, ชื่อโหมด, รหัสผ่านเซิร์ฟเวอร์, จำนวนผู้เล่นโลคัล และ CSessionSocketParams ด้วย MSG_REQ_CONNECTREMOTESESSIONSTATE
    • รับข้อความ, ชื่อไฟล์ world, flag ความยาก·โหมดเกม และคุณสมบัติ session ด้วย MSG_REP_CONNECTREMOTESESSIONSTATE
    • initialize สถานะเกมอ้างอิง
    • ส่ง MSG_REQ_STATEDELTA เพื่อขอความต่างจากสถานะปัจจุบันของเซิร์ฟเวอร์
    • หลังรับ MSG_REP_STATEDELTA แล้วสร้าง game state stream กลับขึ้นมาด้วย diff ย้อนกลับ
    • initialize สถานะ session โลคัลด้วย CSessionState::Read_t()
    • ทำการตรวจ CRC และหากไม่ตรงกันจะตัดการเชื่อมต่อ

main loop และการส่ง game stream ซ้ำ

  • main loop ของไคลเอนต์และเซิร์ฟเวอร์โดยรวมคล้ายกัน แต่เซิร์ฟเวอร์ทำงานเพิ่มเติม
  • loop อัปเดต local client interface และ broadcast interface แล้วให้สถานะ session โลคัลประมวลผลข้อความเครือข่ายที่เข้ามา
  • เซิร์ฟเวอร์ยังรับผิดชอบการแลกบัฟเฟอร์ระหว่าง client interface ที่จับคู่กัน, อัปเดต client interface ฝั่งเซิร์ฟเวอร์, อัปเดต GameAgent และประมวลผลคำสั่ง remote admin shell
  • SessionStateLoop() แยกประมวลผลข้อความ unreliable และ reliable
    • unreliable: MSG_GAMESTREAMBLOCKS, MSG_KEEPALIVE, MSG_INF_PINGS, MSG_CHAT_OUT
    • reliable: MSG_INF_DISCONNECTED, MSG_ADMIN_RESPONSE
  • MSG_GAMESTREAMBLOCKS เป็นข้อความ unreliable แต่หากขาดหายอาจทำให้การซิงก์พังได้
  • Serious Engine ตรวจ sequence ที่ขาดในขั้นประมวลผล game stream และขอให้ส่งซ้ำ
    • หากมีบล็อก sequence ถัดไปตามที่คาดไว้ ก็ประมวลผล
    • หากไม่มีบล็อกถัดไปและไม่มีบล็อกที่ใหม่กว่านั้น ก็ไม่ทำอะไรใน loop นั้น
    • หากไม่มีบล็อกถัดไปแต่มีบล็อกใหม่กว่า แปลว่าอาจขาดหาย จึงตั้ง timeout
    • หลัง timeout จะขอ sequence และจำนวนบล็อกที่ขาดด้วย MSG_REQUESTGAMESTREAMRESEND
  • เซิร์ฟเวอร์ส่งบล็อก game stream ที่ถูกร้องขออีกครั้ง

การประมวลผลบล็อก game stream

  • MSG_SEQ_ADDPLAYER ถูกส่งเมื่อผู้เล่นเข้ามาในเกม และมี player index กับ descriptor CPlayerCharacter
  • MSG_SEQ_REMPLAYER ถูกส่งเมื่อผู้เล่นตัดการเชื่อมต่อ และมีเพียง player index
  • MSG_SEQ_CHARACTERCHANGE ถ่ายทอดการเปลี่ยนชื่อผู้เล่น, ทีม, รูปลักษณ์
    • ใน Serious Sam บัฟเฟอร์รูปลักษณ์เก็บโครงสร้าง CPlayerSettings
    • ภายในมีชื่อไฟล์โมเดลผู้เล่น, นโยบายเลือกอาวุธอัตโนมัติ, ชนิด crosshair และ flag หลายตัว
  • MSG_SEQ_PAUSE ถ่ายทอดการหยุดชั่วคราวหรือยกเลิก และพิมพ์ชื่อผู้เล่นที่ร้องขอลงคอนโซล
  • MSG_SEQ_ALLACTIONS เก็บเวลา tick ปัจจุบันและการกระทำของผู้เล่นทั้งหมด
    • นำ CPlayerAction ไปใช้กับ CPlayerTarget แต่ละตัว
    • หลังจากนั้นจึงประมวลผล timer, event, moving entity และ physics
  • การตรวจการซิงก์ทำด้วย MakeSynchronisationCheck()
    • สร้าง CSyncCheck ผ่าน ChecksumForSync() ของ entity และ player target เป็นต้น
    • ไคลเอนต์ส่ง MSG_SYNCCHECK ไปยังเซิร์ฟเวอร์ และหากไม่ตรงกับสถานะเซิร์ฟเวอร์จะถูกตัดการเชื่อมต่อ

ลด input lag ด้วย prediction

  • prediction เป็นกลไกเพื่อลดปัญหาที่เกมเร็วรู้สึกหน่วงเพราะ latency อินเทอร์เน็ต
  • prediction ของผู้เล่นโลคัลใช้การกระทำที่ส่งไปยังเซิร์ฟเวอร์
  • prediction ของผู้เล่นระยะไกลใช้การกระทำล่าสุดที่ได้รับจากเซิร์ฟเวอร์
  • หากไคลเอนต์เดินสถานะเกมจริงเองโดยไม่รอการตอบสนองจากเซิร์ฟเวอร์ จะไม่รู้การกระทำของผู้เล่นอื่นและอาจทำให้ซิงก์พัง
  • Serious Engine ใช้ predictor เพื่อไม่ให้สถานะจริงกับสถานะที่ทำนายปะปนกัน
    • predictor ใกล้เคียงกับสำเนา “ผี” ที่เชื่อมกับ entity ปกติ
    • temporary predictor ถูกสร้างระหว่าง prediction และไม่มี entity ที่เชื่อมกับสถานะเกมจริง
  • ตอนประมวลผล prediction tick จะประมวลผลเฉพาะ predictor entity
  • เมื่อไคลเอนต์ได้รับการกระทำของผู้เล่นจากเซิร์ฟเวอร์ จะทำลาย predictor เดิมและเริ่ม prediction cycle ใหม่
  • ตอนเรนเดอร์ entity เดิมที่กำลังถูกทำนายจะไม่ถูกวาด แต่จะวาด predictor แทน ทำให้ดูเหมือนการเคลื่อนไหวดำเนินต่อโดยไม่เปลี่ยนสถานะจริงมากนัก
  • ผู้เล่นโลคัลสามารถทำนายได้เท่ากับจำนวนการกระทำที่ส่งไปยังเซิร์ฟเวอร์ซึ่งเก็บใน plt_abPrediction เท่านั้น
  • ใน prediction ของผู้เล่นระยะไกล หาก cli_bLerpActions ปิดอยู่ จะทำซ้ำการกระทำล่าสุดที่ได้รับ
  • หาก cli_bLerpActions เปิดอยู่ จะทำ linear interpolation ระหว่างสองการกระทำล่าสุด แต่ค่าเริ่มต้นคือปิด

เปรียบเทียบกับ Doom และ Quake

  • networking ของ Doom จริง ๆ เป็น peer-to-peer แต่ไคลเอนต์แลกเปลี่ยนโครงสร้างคล้าย CPlayerAction และรัน simulation แยกกันเอง
  • Doom ก็ใช้ระบบคล้ายกันสำหรับการบันทึกและเล่นเดโมซ้ำ
  • Quake ใช้โครงสร้างต่างออกไป โดยไคลเอนต์ใกล้เคียงกับการรับอัปเดตสถานะจากเซิร์ฟเวอร์ ไม่ได้ประมวลผล game logic ขนาดใหญ่เอง
  • วิธีของ Quake ทำให้ไม่ต้องกังวลปัญหาการซิงก์มากนัก และอาจป้องกันโกงได้ง่ายกว่า เช่น ไม่ส่งข้อมูล entity หลังผนังจากเซิร์ฟเวอร์
  • Serious Sam มี session ที่มีศัตรูและวัตถุ active มากกว่า Quake มากเป็นเรื่องปกติ ดังนั้นหากส่งสถานะของวัตถุจำนวนมากทุก tick อาจกินแบนด์วิดท์สูง

portability และข้อจำกัดเชิงโครงสร้าง

  • ข้อความเครือข่ายบางส่วน serialize struct ด้วยวิธีใกล้เคียง reinterpret cast
  • เมื่อสมมติว่าใช้คอมไพเลอร์เดียวและแพลตฟอร์มเดียวอาจใช้ได้ แต่ในเกมข้ามแพลตฟอร์ม layout และ padding ของ struct อาจต่างกัน
  • ไฟล์ executable 32-bit อาจพยายามจัด alignment ขอบเขต 4 ไบต์ ส่วน executable 64-bit อาจเป็นขอบเขต 8 ไบต์
  • ยังมีปัญหา endianness
    • x86 PC เป็น little endian
    • PS3 เป็น big endian
  • โครงสร้างของ Serious Engine ดูสง่างามในแง่ที่ abstract ความต่างของสื่อส่งข้อมูลอย่างเครือข่ายและไฟล์ออกจาก game logic แต่เพราะเป็นโมเดลที่ทุกไคลเอนต์มีสำเนาสถานะเกม จึงโกงได้
  • ตัวอย่างเช่น ไคลเอนต์ที่ถูกดัดแปลงอาจแสดง outline ของผู้เล่นอื่นที่อยู่หลังผนังใน deathmatch ได้

1 ความคิดเห็น

 
GN⁺ 2024-06-14
ความคิดเห็นจาก Hacker News
  • เป็นหนึ่งในนักพัฒนาที่รับผิดชอบการทำ โค้ดเครือข่าย ของ Serious Sam
    มักจะนอนใต้โต๊ะในออฟฟิศ Croteam แล้วคุ้ย Usenet โดยเฉพาะได้รับแรงบันดาลใจจากโพสต์ที่อธิบายระบบ prediction ของ QuakeWorld
    คืนนั้น ระหว่างที่เพื่อนร่วมงาน Dan ใช้เครื่อง Unix 486 เก่าเป็นเราเตอร์เพื่อจำลอง latency สำหรับทดสอบ ก็เขียน implementation แบบ minimum viable ง่าย ๆ ขึ้นมา
    นั่นเป็นเรื่องก่อนที่ตัวเกมจริงจะถูกสร้างขึ้นบนมันเสียอีก

    • สงสัยว่าทำไมถึงทำให้ ผู้ชายหัวระเบิด วิ่งเข้ามาพร้อมเสียงกรีดร้อง และยิ่งเข้าใกล้เสียงก็ยิ่งดังขึ้น
      จนถึงตอนนี้ยังได้ยินเสียงนั้นอยู่เลย
    • ชอบบรรยากาศที่ใต้บทความเกี่ยวกับวิธีสร้างเกมระดับตำนาน มีใครบางคนพูดแบบสบาย ๆ ว่า “อ้อใช่ อันนั้นผมทำเองแหละ สนุกดี” จริง ๆ
    • ชอบเกมนั้นมาก
      ชอบเกม co-op กับเกมยิงมาก แต่เพื่อน ๆ มักอยากเล่นแต่ Counter-Strike
      Serious Sam ทำให้บางครั้งผมโน้มน้าวให้พวกเขามาเล่นเกมที่ผมชอบด้วยกันได้
    • Serious Sam เป็นหนึ่งในเกมโปรดของลุงผม และเขาก็ชอบ Duke Nukem 3D ด้วย
      ลุงเป็นคนสำคัญมากในชีวิตผม และการเล่นเกมที่เขาชอบก็เป็นวิธีดี ๆ ในการเชื่อมโยงกับความทรงจำถึงเขา
      มันยังคงเป็นเกมที่ยอดเยี่ยม multiplayer ที่ยอดเยี่ยม และความทรงจำที่ดีมาก
    • ต้องขอบคุณ การเล่นแบบแบ่งหน้าจอ จริง ๆ
  • Serious Sam เป็น เกมสำหรับ LAN party ที่แข็งแกร่งเสมอ
    ไม่ใช่เพราะเป็นเกมที่ภาพหวือหวาที่สุดในตอนนั้น และไม่ใช่เพราะใครวางแผนไว้ล่วงหน้า
    แต่มันครอง LAN party ได้เพราะเมื่อเกมอื่น ๆ ล้มตายจากปัญหาไดรเวอร์ ปัญหาความร้อน ปัญหาอัปเดต ฯลฯ พอเปิด Serious Sam มันก็แค่ใช้งานได้
    จุดนี้ยังต่อเนื่องมาถึงภาคต่อ ๆ มา ต่อให้ PC ของใครพังเละไปเลย มันก็ยังรองรับ split-screen ได้อย่างเสถียร และจัดการอุปกรณ์อินพุตได้ดี
    ส่วนที่เป็นระบบของเกมนั้นยอดเยี่ยมจริง ๆ ในแง่ ความน่าเชื่อถือ

    • Serious Sam รันได้เร็วแม้บนฮาร์ดแวร์ห่วย ๆ แต่ก็ยังดูค่อนข้างดี
      คล้าย ๆ กัน Counter-Strike ก็ไม่ได้กราฟิกดี แต่รันได้ดีแม้บน PC ระดับเครื่องปิ้งขนมปัง จึงได้รับความนิยมอยู่นาน
    • เสียง “aaaaaaaaaaaaah” ที่ดังออกมาจากลำโพงหลาย ๆ ตัวนี่สนุกดี
    • ช่วงปลายยุค 90 ผมดูแลเว็บไซต์ซัพพอร์ตด้านเทคนิคของ EA และทีมซัพพอร์ต/QA ก็เล่น Serious Sam กันชุดใหญ่หลังเลิกงาน
      มันเป็นเกมยิงมุมมองบุคคลที่หนึ่งเกมเดียวที่รันได้ดีอย่างสม่ำเสมอบน PC สำหรับทำงาน และสนุกมากจริง ๆ
      ตอนนั้นที่ EA งาน QA กับซัพพอร์ตด้านเทคนิคค่อนข้างทับซ้อนกันมาก โดยคนซัพพอร์ตจะทำหน้าที่เป็น beta tester ภายในช่วงฤดูร้อนเพื่อให้ทันเกมออกปลายปี แล้วพอเข้าฤดูหนาวช่วงใกล้คริสต์มาสที่โทรศัพท์เริ่มเยอะขึ้นก็กลับไปทำซัพพอร์ตด้านเทคนิค
  • ตอนทำ multiplayer ใน พอร์ต Game Boy Color ของ Vigilante 8 ใช้ gameplay แบบ deterministic
    สายลิงก์ของ GBC รับส่งข้อมูลพร้อมกันสองทางได้ครั้งละ 1 ไบต์ และทำงานเหมือนคู่ของ shift register ที่เติมข้อมูลให้กันและกันผ่านสายเคเบิล
    เกมถูกล็อกกับ frame rate ของ GBC และแทบทุก V-Blank ก็ต้องมีงานอัปเดตหน้าจอจำนวนมาก หากพลาดไปการเลื่อนภาพที่ลื่นไหลก็จะสะดุด
    ตอนเริ่ม multiplayer จะแลกเปลี่ยน seed กัน และการทำงานเป็นประมาณนี้: ในเฟรม A อ่านอินพุต บีบอัดเป็น 1 ไบต์ แล้วใส่ลงในบัฟเฟอร์ส่ง ระหว่าง render เฟรม B การส่งข้อมูลจะเกิดขึ้น พอเริ่มเฟรม C ก็จะมีทั้งอินพุต local ที่ส่งไปในเฟรม A และอินพุตของอีกฝ่ายที่ได้รับในเฟรม B
    จากนั้นนำอินพุตเหล่านั้นไปใช้กับสถานะเกมและ render เฟรม C ดังนั้นทั้งอินพุต local และ remote จึงถูกนำไปใช้ด้วย ดีเลย์ 1 เฟรม
    การเล่น local ไม่มี input lag เพราะฉะนั้นถ้าแพ้ใน multiplayer จะโทษ latency ก็ได้ และถ้าจำเป็นจะโทษผมเป็นพิเศษก็ได้

    • ไม่กี่สัปดาห์ก่อนผมซื้อ cartridge นี้มา เพราะชอบ cartridge แบบ rumble เก่า ๆ ของ GBC และทึ่งที่มันมี multiplayer ผ่าน link cable
      เป็นเกมที่ดีจริง ๆ และคำอธิบายเชิงเทคนิคเกี่ยวกับการทำ multiplayer ก็เจ๋งมาก
  • Croteam เป็นทีมพัฒนาเกมที่มีพรสวรรค์จริง ๆ
    ผมสนุกกับ The Talos Principle ทั้งภาค 1 และ 2 มาก และในภาค 1 พวกเขายังเป็นหนึ่งในผู้บุกเบิกยุคแรก ๆ ที่สร้าง custom Vulkan game engine แบบเต็มตัวด้วย

    • เสียดายมากที่ The Talos Principle 2 ทิ้งเอนจินของตัวเองแล้วใช้ Unreal Engine
    • เพิ่งรู้ว่า DLC ของ Talos 2 จะออกบน Steam วันศุกร์นี้
  • สงสัยว่านี่เป็นไอเดียแบบเดียวกับ “นักธนู 1500 คนบน 28.8K” ของ Age of Empires หรือเปล่า
    https://www.gamedeveloper.com/programming/1500-archers-on-a-...

    • ใช่ ทั้งสองอย่างเป็นระบบ deterministic lockstep
      เกมจำนวนมากใช้ระบบแบบนี้มานานแล้ว แต่ทุกวันนี้ดูเหมือนจะพบได้น้อยลงกว่าเมื่อก่อนด้วยหลายเหตุผล
    • ตัวเลขนี้ทำให้เห็นว่า unit limit ใน Tempest Rising แปลกแค่ไหน
      มีทรัพยากรหรือไม่มีก็พอ ไม่จำเป็นต้องจำกัดเพียงเพื่อให้มีข้อจำกัด
  • เกมที่มีแบนด์วิดท์มากกว่า 10 เท่ายังลำบากกับการรองรับศัตรูจำนวนมากขนาดนั้น
    เพิ่งตระหนักได้ว่า การเพิ่มขึ้นของทรัพยากรทางเทคนิคดูเหมือนจะส่งผลย้อนกลับต่อประสิทธิภาพและความสร้างสรรค์ในวิทยาการคอมพิวเตอร์
    ยิ่งแบนด์วิดท์ พื้นที่จัดเก็บ หน่วยความจำ และพลังประมวลผลเพิ่มขึ้น ซอฟต์แวร์กลับตอบสนองด้วยการช้าลง บวมขึ้น และไร้ความสามารถมากขึ้นต่อหน่วยทรัพยากร
    น่าจะเรียกว่า เอฟเฟกต์การออกแบบซอฟต์แวร์แบบ Benjamin Button ได้

    • อยากรู้ตัวเลขว่า “ศัตรูจำนวนมากขนาดนั้น” หมายถึงกี่ตัว
      ถ้าในบทความมีตัวเลข ผมหาไม่เจอ
      เกมสมัยใหม่หลายเกมก็เป็น multiplayer และมีจำนวนศัตรูมากพอจะเรียกได้ว่า “ขนาดใหญ่” และถ้าประเด็นสำคัญคือจำนวนผู้เล่น ก็มีเกมที่รองรับผู้เล่นจำนวนมากเช่นกัน
    • โดยทั่วไปเป็นที่รู้จักกันมากกว่าในชื่อ software bloat
    • ใช่ สิ่งนี้เป็นที่รู้จักกันในชื่อ กฎของ Wirth
  • ผมมองว่าเป็นเกมที่ใช้เวลา เคลื่อนที่ถอยหลัง มากกว่าการเดินหน้า

    • ถึงขั้นมีเกมที่เอาเรื่องนั้นมาเป็นธีมด้วย ชื่อว่า “I Hate Running Backwards”
      มีบน Steam และอยู่ในจักรวาล Serious Sam แม้จะไม่รู้ว่าเป็นผู้สร้างรายเดียวกันหรือเปล่า
    • คุณกับ Netrisca อยู่ด้วยกัน แต่ศัตรูนับพันเหล่านั้นอยู่ลำพัง
    • ยังมีปืนบางกระบอกที่ให้ความรู้สึกเหมือนยิงออกไปเสี้ยววินาทีก่อนกดปุ่มเมาส์ด้วย
    • ยังจำได้ว่าต้องพยายามเก็บกระสุนที่เกิดขึ้นมาขณะถอยหลังแบบนั้นอย่างสิ้นหวัง
  • โครงสร้างของ Factorio ก็คล้ายกัน คือแทบจะส่งแค่อีเวนต์อินพุต แล้วพึ่งพา แกนจำลองแบบ lockstep
    มีข้อยกเว้นบางส่วนที่เห็นได้ชัด เช่น เครื่องมือวางแผนทางรถไฟ

    • สักวันอยากลองทำงานกับโครงสร้าง lockstep แบบนั้น
      ดูเหมือนเป็นข้อจำกัดด้านการออกแบบที่น่าพอใจและทดสอบได้ดี
  • ยังจำได้ว่าเคยเล่น Serious Sam จากเดโมของ PC Gamer ตอนเด็ก ๆ
    ตอนนั้นมันก็ถูกมองว่าเป็น เกมสไตล์ย้อนยุค ที่เหมือนย้อนกลับไปยุค DOOM และ Quake เก่า ๆ แล้ว
    ตอนนี้ผ่านไปตามตัวอักษร 20 ปี และมันเองก็กลายเป็นคลาสสิกไปแล้ว

  • Starsiege: Tribes ใหญ่และสนุกแบบบ้าบอได้แม้บน การเชื่อมต่อ 56K

    • นักพัฒนา Tribes เคยเขียน white paper ที่พูดถึงแนวคิดโค้ดเครือข่ายคล้าย ๆ กัน
      https://www.gamedevs.org/uploads/tribes-networking-model.pdf
    • น่าจะเป็นเกมโปรดที่สุดของผมตอนเด็ก โดยเฉพาะ Tribes 2
      จริง ๆ ไม่นานมานี้ผมยังดาวน์โหลด Tribes 2 มาเล่นกับบอทเมื่อไม่กี่เดือนก่อน
      เป็นเกมเก่าแต่ยังสนุกอยู่ และผมมักคิดว่าอยากทำใหม่ด้วยอะไรอย่าง Unity
      สักวันอาจจะทำก็ได้
    • Tribes ยอดเยี่ยมมาก
      ในปี 1999 ได้ไถลลงภูมิประเทศที่สร้างแบบ procedural เหมือนเล่นสกี เห็นคนอื่นสกีขึ้นไปบนนั้น แถมยังมีแผนที่ใหญ่และผู้เล่นจำนวนมากด้วย