Serious Sam ที่จัดการศัตรูจำนวนมากผ่านการเชื่อมต่อโมเด็ม 56k
(staniks.github.io)- 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
- จำนวน retry สูงสุดตั้งด้วย
- แพ็กเก็ตแบบ 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: serializeCPacketที่จะส่ง แล้วส่งผ่าน socket API
- abstraction การสื่อสารจริงรายไคลเอนต์อยู่ที่
CClientInterface- เซิร์ฟเวอร์มี interface ของผู้เล่นแต่ละคนผ่าน array
cm_aciClients - ไคลเอนต์สื่อสารกับเซิร์ฟเวอร์ผ่าน
cm_ciLocalClient - ทั้งไคลเอนต์และเซิร์ฟเวอร์ใช้
cm_ciBroadcastเพื่อสร้างการเชื่อมต่อ
- เซิร์ฟเวอร์มี interface ของผู้เล่นแต่ละคนผ่าน array
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 - ไม่บีบอัด
- LZ77
- การบีบอัดเริ่มต้นดูเหมือนเป็น 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 รวมถึงสถานะเกมCSessionStateCNetworkLibraryสืบทอด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
- unreliable:
MSG_GAMESTREAMBLOCKSเป็นข้อความ unreliable แต่หากขาดหายอาจทำให้การซิงก์พังได้- Serious Engine ตรวจ sequence ที่ขาดในขั้นประมวลผล game stream และขอให้ส่งซ้ำ
- หากมีบล็อก sequence ถัดไปตามที่คาดไว้ ก็ประมวลผล
- หากไม่มีบล็อกถัดไปและไม่มีบล็อกที่ใหม่กว่านั้น ก็ไม่ทำอะไรใน loop นั้น
- หากไม่มีบล็อกถัดไปแต่มีบล็อกใหม่กว่า แปลว่าอาจขาดหาย จึงตั้ง timeout
- หลัง timeout จะขอ sequence และจำนวนบล็อกที่ขาดด้วย
MSG_REQUESTGAMESTREAMRESEND
- เซิร์ฟเวอร์ส่งบล็อก game stream ที่ถูกร้องขออีกครั้ง
การประมวลผลบล็อก game stream
MSG_SEQ_ADDPLAYERถูกส่งเมื่อผู้เล่นเข้ามาในเกม และมี player index กับ descriptorCPlayerCharacterMSG_SEQ_REMPLAYERถูกส่งเมื่อผู้เล่นตัดการเชื่อมต่อ และมีเพียง player indexMSG_SEQ_CHARACTERCHANGEถ่ายทอดการเปลี่ยนชื่อผู้เล่น, ทีม, รูปลักษณ์- ใน Serious Sam บัฟเฟอร์รูปลักษณ์เก็บโครงสร้าง
CPlayerSettings - ภายในมีชื่อไฟล์โมเดลผู้เล่น, นโยบายเลือกอาวุธอัตโนมัติ, ชนิด crosshair และ flag หลายตัว
- ใน Serious Sam บัฟเฟอร์รูปลักษณ์เก็บโครงสร้าง
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 ความคิดเห็น
ความคิดเห็นจาก Hacker News
เป็นหนึ่งในนักพัฒนาที่รับผิดชอบการทำ โค้ดเครือข่าย ของ Serious Sam
มักจะนอนใต้โต๊ะในออฟฟิศ Croteam แล้วคุ้ย Usenet โดยเฉพาะได้รับแรงบันดาลใจจากโพสต์ที่อธิบายระบบ prediction ของ QuakeWorld
คืนนั้น ระหว่างที่เพื่อนร่วมงาน Dan ใช้เครื่อง Unix 486 เก่าเป็นเราเตอร์เพื่อจำลอง latency สำหรับทดสอบ ก็เขียน implementation แบบ minimum viable ง่าย ๆ ขึ้นมา
นั่นเป็นเรื่องก่อนที่ตัวเกมจริงจะถูกสร้างขึ้นบนมันเสียอีก
จนถึงตอนนี้ยังได้ยินเสียงนั้นอยู่เลย
ชอบเกม co-op กับเกมยิงมาก แต่เพื่อน ๆ มักอยากเล่นแต่ Counter-Strike
Serious Sam ทำให้บางครั้งผมโน้มน้าวให้พวกเขามาเล่นเกมที่ผมชอบด้วยกันได้
ลุงเป็นคนสำคัญมากในชีวิตผม และการเล่นเกมที่เขาชอบก็เป็นวิธีดี ๆ ในการเชื่อมโยงกับความทรงจำถึงเขา
มันยังคงเป็นเกมที่ยอดเยี่ยม multiplayer ที่ยอดเยี่ยม และความทรงจำที่ดีมาก
Serious Sam เป็น เกมสำหรับ LAN party ที่แข็งแกร่งเสมอ
ไม่ใช่เพราะเป็นเกมที่ภาพหวือหวาที่สุดในตอนนั้น และไม่ใช่เพราะใครวางแผนไว้ล่วงหน้า
แต่มันครอง LAN party ได้เพราะเมื่อเกมอื่น ๆ ล้มตายจากปัญหาไดรเวอร์ ปัญหาความร้อน ปัญหาอัปเดต ฯลฯ พอเปิด Serious Sam มันก็แค่ใช้งานได้
จุดนี้ยังต่อเนื่องมาถึงภาคต่อ ๆ มา ต่อให้ PC ของใครพังเละไปเลย มันก็ยังรองรับ split-screen ได้อย่างเสถียร และจัดการอุปกรณ์อินพุตได้ดี
ส่วนที่เป็นระบบของเกมนั้นยอดเยี่ยมจริง ๆ ในแง่ ความน่าเชื่อถือ
คล้าย ๆ กัน Counter-Strike ก็ไม่ได้กราฟิกดี แต่รันได้ดีแม้บน PC ระดับเครื่องปิ้งขนมปัง จึงได้รับความนิยมอยู่นาน
มันเป็นเกมยิงมุมมองบุคคลที่หนึ่งเกมเดียวที่รันได้ดีอย่างสม่ำเสมอบน 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 ก็ได้ และถ้าจำเป็นจะโทษผมเป็นพิเศษก็ได้
เป็นเกมที่ดีจริง ๆ และคำอธิบายเชิงเทคนิคเกี่ยวกับการทำ multiplayer ก็เจ๋งมาก
Croteam เป็นทีมพัฒนาเกมที่มีพรสวรรค์จริง ๆ
ผมสนุกกับ The Talos Principle ทั้งภาค 1 และ 2 มาก และในภาค 1 พวกเขายังเป็นหนึ่งในผู้บุกเบิกยุคแรก ๆ ที่สร้าง custom Vulkan game engine แบบเต็มตัวด้วย
สงสัยว่านี่เป็นไอเดียแบบเดียวกับ “นักธนู 1500 คนบน 28.8K” ของ Age of Empires หรือเปล่า
https://www.gamedeveloper.com/programming/1500-archers-on-a-...
เกมจำนวนมากใช้ระบบแบบนี้มานานแล้ว แต่ทุกวันนี้ดูเหมือนจะพบได้น้อยลงกว่าเมื่อก่อนด้วยหลายเหตุผล
มีทรัพยากรหรือไม่มีก็พอ ไม่จำเป็นต้องจำกัดเพียงเพื่อให้มีข้อจำกัด
เกมที่มีแบนด์วิดท์มากกว่า 10 เท่ายังลำบากกับการรองรับศัตรูจำนวนมากขนาดนั้น
เพิ่งตระหนักได้ว่า การเพิ่มขึ้นของทรัพยากรทางเทคนิคดูเหมือนจะส่งผลย้อนกลับต่อประสิทธิภาพและความสร้างสรรค์ในวิทยาการคอมพิวเตอร์
ยิ่งแบนด์วิดท์ พื้นที่จัดเก็บ หน่วยความจำ และพลังประมวลผลเพิ่มขึ้น ซอฟต์แวร์กลับตอบสนองด้วยการช้าลง บวมขึ้น และไร้ความสามารถมากขึ้นต่อหน่วยทรัพยากร
น่าจะเรียกว่า เอฟเฟกต์การออกแบบซอฟต์แวร์แบบ Benjamin Button ได้
ถ้าในบทความมีตัวเลข ผมหาไม่เจอ
เกมสมัยใหม่หลายเกมก็เป็น multiplayer และมีจำนวนศัตรูมากพอจะเรียกได้ว่า “ขนาดใหญ่” และถ้าประเด็นสำคัญคือจำนวนผู้เล่น ก็มีเกมที่รองรับผู้เล่นจำนวนมากเช่นกัน
ผมมองว่าเป็นเกมที่ใช้เวลา เคลื่อนที่ถอยหลัง มากกว่าการเดินหน้า
มีบน Steam และอยู่ในจักรวาล Serious Sam แม้จะไม่รู้ว่าเป็นผู้สร้างรายเดียวกันหรือเปล่า
โครงสร้างของ Factorio ก็คล้ายกัน คือแทบจะส่งแค่อีเวนต์อินพุต แล้วพึ่งพา แกนจำลองแบบ lockstep
มีข้อยกเว้นบางส่วนที่เห็นได้ชัด เช่น เครื่องมือวางแผนทางรถไฟ
ดูเหมือนเป็นข้อจำกัดด้านการออกแบบที่น่าพอใจและทดสอบได้ดี
ยังจำได้ว่าเคยเล่น Serious Sam จากเดโมของ PC Gamer ตอนเด็ก ๆ
ตอนนั้นมันก็ถูกมองว่าเป็น เกมสไตล์ย้อนยุค ที่เหมือนย้อนกลับไปยุค DOOM และ Quake เก่า ๆ แล้ว
ตอนนี้ผ่านไปตามตัวอักษร 20 ปี และมันเองก็กลายเป็นคลาสสิกไปแล้ว
Starsiege: Tribes ใหญ่และสนุกแบบบ้าบอได้แม้บน การเชื่อมต่อ 56K
https://www.gamedevs.org/uploads/tribes-networking-model.pdf
จริง ๆ ไม่นานมานี้ผมยังดาวน์โหลด Tribes 2 มาเล่นกับบอทเมื่อไม่กี่เดือนก่อน
เป็นเกมเก่าแต่ยังสนุกอยู่ และผมมักคิดว่าอยากทำใหม่ด้วยอะไรอย่าง Unity
สักวันอาจจะทำก็ได้
ในปี 1999 ได้ไถลลงภูมิประเทศที่สร้างแบบ procedural เหมือนเล่นสกี เห็นคนอื่นสกีขึ้นไปบนนั้น แถมยังมีแผนที่ใหญ่และผู้เล่นจำนวนมากด้วย