การเขียนเอ็นจิ้นการผจญภัยข้อความสำหรับผู้เล่นหลายคนใน Node.js: การออกแบบเซิร์ฟเวอร์เอ็นจิ้นเกม (ตอนที่ 2)

เผยแพร่แล้ว: 2022-03-10
สรุปอย่างรวดเร็ว ↬ ยินดีต้อนรับสู่ส่วนที่สองของชุดนี้ ส่วนแรก เราครอบคลุมสถาปัตยกรรมของแพลตฟอร์มที่ใช้ Node.js และแอปพลิเคชันไคลเอนต์ ที่จะช่วยให้ผู้คนสามารถกำหนดและเล่นการผจญภัยข้อความของตนเองเป็นกลุ่ม คราวนี้ เราจะพูดถึงการสร้างโมดูลที่เฟอร์นันโดกำหนดไว้เป็นครั้งสุดท้าย (เอ็นจิ้นเกม) และจะเน้นไปที่ขั้นตอนการออกแบบด้วย เพื่อให้กระจ่างถึงสิ่งที่จำเป็นต้องเกิดขึ้นก่อนที่คุณจะเริ่มเขียนโค้ด โครงการงานอดิเรกของตัวเอง

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

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

ส่วนอื่นๆ ของซีรีส์นี้

  • ตอนที่ 1: บทนำ
  • ส่วนที่ 3: การสร้าง Terminal Client
  • ตอนที่ 4: การเพิ่มแชทในเกมของเรา

กลศาสตร์การต่อสู้

นี่อาจเป็นการเปลี่ยนแปลงครั้งใหญ่ที่สุดจากแผนเดิม ฉันรู้ว่าฉันบอกว่าฉันจะใช้ D&D-esque ซึ่งพีซีและ NPC แต่ละเครื่องที่เกี่ยวข้องจะได้รับมูลค่าความคิดริเริ่ม และหลังจากนั้น เราจะเปิดการต่อสู้แบบผลัดกันเล่น เป็นความคิดที่ดี แต่การใช้งานบนบริการที่ใช้ REST นั้นค่อนข้างซับซ้อน เนื่องจากคุณไม่สามารถเริ่มการสื่อสารจากฝั่งเซิร์ฟเวอร์ หรือรักษาสถานะระหว่างการโทรได้

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

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

ทริกเกอร์

หากคุณให้ความสำคัญกับคำจำกัดความของเกม JSON จากบทความก่อนหน้านี้ คุณอาจสังเกตเห็นคำจำกัดความของทริกเกอร์ที่พบในไอเท็มฉาก หนึ่งที่เกี่ยวข้องกับการอัปเดตสถานะเกม ( statusUpdate ) ระหว่างการใช้งาน ฉันตระหนักว่ามันทำงานเป็นปุ่มสลับที่ให้อิสระที่จำกัด คุณเห็นไหมว่าในวิธีการใช้งาน (จากมุมมองที่เป็นสำนวน) คุณสามารถกำหนดสถานะได้ แต่การไม่ตั้งค่านั้นไม่ใช่ตัวเลือก ดังนั้นฉันจึงแทนที่เอฟเฟกต์ทริกเกอร์นี้ด้วยเอฟเฟกต์ใหม่สองอัน: addStatus และ removeStatus สิ่งเหล่านี้จะช่วยให้คุณกำหนด ได้อย่างแม่นยำว่า เอฟเฟกต์เหล่านี้จะเกิดขึ้นเมื่อใด - ถ้าเป็นเช่นนั้น ฉันรู้สึกว่าสิ่งนี้ง่ายต่อการเข้าใจและให้เหตุผล

ซึ่งหมายความว่าทริกเกอร์ตอนนี้มีลักษณะดังนี้:

 "triggers": [ { "action": "pickup", "effect":{ "addStatus": "has light", "target": "game" } }, { "action": "drop", "effect": { "removeStatus": "has light", "target": "game" } } ]

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

เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

การดำเนินการ

ด้วยการอัปเดตเหล่านั้น เราสามารถเริ่มครอบคลุมการใช้งานจริงได้ จากมุมมองทางสถาปัตยกรรม ไม่มีอะไรเปลี่ยนแปลง เรายังคงสร้าง REST API ที่จะมีตรรกะของเอ็นจิ้นเกมหลัก

กองเทคโนโลยี

สำหรับโครงการนี้โดยเฉพาะ โมดูลที่ฉันจะใช้มีดังต่อไปนี้:

โมดูล คำอธิบาย
Express.js แน่นอน ฉันจะใช้ Express เป็นพื้นฐานสำหรับเครื่องยนต์ทั้งหมด
วินสตัน ทุกอย่างเกี่ยวกับการบันทึกจะถูกจัดการโดย Winston
การกำหนดค่า ตัวแปรคงที่และขึ้นอยู่กับสภาพแวดล้อมทั้งหมดจะได้รับการจัดการโดยโมดูล config.js ซึ่งทำให้งานในการเข้าถึงง่ายขึ้นอย่างมาก
พังพอน นี่จะเป็น ORM ของเรา ฉันจะสร้างแบบจำลองทรัพยากรทั้งหมดโดยใช้ Mongoose Models และใช้เพื่อโต้ตอบกับฐานข้อมูลโดยตรง
uuid เราจำเป็นต้องสร้าง ID ที่ไม่ซ้ำ — โมดูลนี้จะช่วยเราทำงานนั้น

สำหรับเทคโนโลยีอื่นๆ ที่ใช้นอกเหนือจาก Node.js เรามี MongoDB และ Redis ฉันชอบใช้ Mongo เนื่องจากไม่มีสคีมาที่จำเป็น ข้อเท็จจริงง่ายๆ นั้นทำให้ฉันนึกถึงโค้ดและรูปแบบข้อมูลของฉัน โดยไม่ต้องกังวลเกี่ยวกับการอัปเดตโครงสร้างของตาราง การย้ายสคีมา หรือประเภทข้อมูลที่ขัดแย้งกัน

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

ฉันจะใช้คุณลักษณะการหมดอายุของคีย์ของ Redis เพื่อจัดการบางแง่มุมของโฟลว์โดยอัตโนมัติ (เพิ่มเติมเกี่ยวกับเรื่องนี้ในไม่ช้า)

คำจำกัดความของ API

ก่อนย้ายเข้าสู่การโต้ตอบระหว่างไคลเอ็นต์กับเซิร์ฟเวอร์และข้อกำหนดโฟลว์ข้อมูล ฉันต้องการข้ามปลายทางที่กำหนดไว้สำหรับ API นี้ มีไม่มากนัก ส่วนใหญ่เราต้องปฏิบัติตามคุณสมบัติหลักที่อธิบายไว้ในส่วนที่ 1:

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

รายการด้านบนแปลเป็นรายการปลายทางต่อไปนี้:

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

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

แอพไคลเอนต์

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

อินสแตนซ์เกม

การสร้างเกมใหม่โดยพื้นฐานแล้วหมายถึงการสร้างตัวอย่างใหม่ของเกมโดยเฉพาะ ตัวอย่างใหม่นี้จะมีสำเนาของฉากและเนื้อหาทั้งหมด การปรับเปลี่ยนใด ๆ ที่ทำกับเกมจะมีผลกับปาร์ตี้เท่านั้น ด้วยวิธีนี้ หลายกลุ่มสามารถเล่นเกมเดียวกันในแบบของตนเองได้

สถานะเกมของผู้เล่น

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

คำสั่งผู้เล่น

เมื่อทุกอย่างได้รับการตั้งค่าและแอปพลิเคชันไคลเอนต์ได้ลงทะเบียนและเข้าร่วมเกมแล้ว ก็สามารถเริ่มส่งคำสั่งได้ คำสั่งที่ใช้ในเวอร์ชันนี้ของเอ็นจิ้นได้แก่: move look pickup และ attack

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

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

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

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

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

    นอกจากนี้ คำสั่งนี้มีเวอร์ชันย่อสำหรับ look at room: look around ; นั่นเป็นเพราะผู้เล่นจะพยายามตรวจสอบห้องบ่อยมาก ดังนั้นการให้คำสั่งสั้น (หรือนามแฝง) ที่พิมพ์ง่ายกว่าจึงสมเหตุสมผลมาก
  • คำสั่ง pickup มีบทบาทสำคัญมากสำหรับการเล่นเกม คำสั่งนี้จะดูแลการเพิ่มไอเท็มลงในช่องเก็บของของผู้เล่นหรือในมือของพวกเขา (หากเป็นของว่าง) เพื่อให้เข้าใจว่าแต่ละไอเท็มควรถูกจัดเก็บไว้ที่ไหน คำจำกัดความของไอเท็มดังกล่าวมีคุณสมบัติ "ปลายทาง" ที่ระบุว่ามีไว้สำหรับสินค้าคงคลังหรือในมือของผู้เล่น อะไรก็ตามที่หยิบขึ้นมาจากฉากได้สำเร็จจะถูกลบออกจากฉากนั้น อัปเดตเวอร์ชันของเกมอินสแตนซ์ของเกม
  • คำสั่ง use จะช่วยให้คุณสร้างผลกระทบต่อสิ่งแวดล้อมโดยใช้ไอเท็มในคลังของคุณ ตัวอย่างเช่น การรับกุญแจในห้องจะทำให้คุณสามารถใช้มันเพื่อเปิดประตูล็อคในอีกห้องหนึ่งได้
  • มีคำสั่งพิเศษหนึ่งคำสั่งที่ไม่เกี่ยวกับการเล่นเกม แต่มีคำสั่งตัวช่วยแทนเพื่อรับข้อมูลเฉพาะ เช่น ID เกมปัจจุบันหรือชื่อผู้เล่น คำสั่งนี้เรียกว่า get และผู้เล่นสามารถใช้คำสั่งนี้เพื่อสอบถามเอ็นจิ้นเกมได้ ตัวอย่างเช่น รับ gameid
  • สุดท้าย คำสั่งสุดท้ายที่ใช้กับเอ็นจิ้นรุ่นนี้คือคำสั่ง attack ฉันครอบคลุมเรื่องนี้แล้ว โดยพื้นฐานแล้ว คุณจะต้องระบุเป้าหมายและอาวุธที่คุณใช้โจมตี ด้วยวิธีนี้ระบบจะสามารถตรวจสอบจุดอ่อนของเป้าหมายและกำหนดผลลัพธ์ของการโจมตีของคุณได้

ปฏิสัมพันธ์ระหว่างไคลเอนต์กับเอ็นจิ้น

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

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

มาล้างมือกันเถอะ

ฉันได้ออกแบบให้มากที่สุดเท่าที่จะมากได้ โดยหวังว่าข้อมูลดังกล่าวจะช่วยให้คุณเข้าใจส่วนต่างๆ ต่อไปนี้ มาทำความเข้าใจส่วนต่างๆ ของเอ็นจิ้นเกมกัน

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

ไฟล์หลัก

อย่างแรกเลย: นี่คือโปรเจ็กต์ Express และโค้ดสำเร็จรูปที่สร้างขึ้นโดยใช้ตัวสร้างของ Express ดังนั้นไฟล์ app.js ควรจะคุ้นเคยสำหรับคุณ ฉันแค่ต้องการปรับแต่งสองอย่างที่ฉันชอบทำกับโค้ดนั้นเพื่อทำให้งานของฉันง่ายขึ้น

ขั้นแรก ฉันเพิ่มข้อมูลโค้ดต่อไปนี้เพื่อรวมไฟล์เส้นทางใหม่โดยอัตโนมัติ:

 const requireDir = require("require-dir") const routes = requireDir("./routes") //... Object.keys(routes).forEach( (file) => { let cnt = routes[file] app.use('/' + file, cnt) })

มันค่อนข้างง่ายจริงๆ แต่ไม่จำเป็นต้องใช้ไฟล์เส้นทางแต่ละไฟล์ที่คุณสร้างในอนาคตด้วยตนเอง ยังไงก็ตาม require-dir เป็นโมดูลง่ายๆ ที่จะดูแลการเรียกค้นไฟล์ทุกไฟล์ภายในโฟลเดอร์โดยอัตโนมัติ แค่นั้นแหละ.

การเปลี่ยนแปลงอื่นๆ ที่ฉันชอบทำคือปรับแต่งตัวจัดการข้อผิดพลาดเล็กน้อย ฉันควรจะเริ่มใช้สิ่งที่มีประสิทธิภาพมากกว่านี้ แต่สำหรับความต้องการที่มีอยู่ ฉันรู้สึกว่าสิ่งนี้ทำให้งานเสร็จ:

 // error handler app.use(function(err, req, res, next) { // render the error page if(typeof err === "string") { err = { status: 500, message: err } } res.status(err.status || 500); let errorObj = { error: true, msg: err.message, errCode: err.status || 500 } if(err.trace) { errorObj.trace = err.trace } res.json(errorObj); });

โค้ดด้านบนดูแลข้อความแสดงข้อผิดพลาดประเภทต่างๆ ที่เราอาจต้องจัดการ ไม่ว่าจะเป็นออบเจ็กต์แบบเต็ม อ็อบเจ็กต์ข้อผิดพลาดจริงที่ส่งโดย Javascript หรือข้อความแสดงข้อผิดพลาดทั่วไปโดยไม่มีบริบทอื่น รหัสนี้จะรวบรวมทั้งหมดและจัดรูปแบบให้อยู่ในรูปแบบมาตรฐาน

การจัดการคำสั่ง

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

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

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

parser นั้นง่ายมาก เนื่องจากคำสั่งที่นำมาใช้ทั้งหมดตอนนี้มีคำสั่งจริงตามคำแรก (เช่น "ย้ายไปทางเหนือ", "pick up knife" เป็นต้น) การแยกสตริงและรับส่วนแรกเป็นเรื่องง่าย:

 const requireDir = require("require-dir") const validCommands = requireDir('./commands') class CommandParser { constructor(command) { this.command = command } normalizeAction(strAct) { strAct = strAct.toLowerCase().split(" ")[0] return strAct } verifyCommand() { if(!this.command) return false if(!this.command.action) return false if(!this.command.context) return false let action = this.normalizeAction(this.command.action) if(validCommands[action]) { return validCommands[action] } return false } parse() { let validCommand = this.verifyCommand() if(validCommand) { let cmdObj = new validCommand(this.command) return cmdObj } else { return false } } }

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

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

ฉันจะไม่ลงรายละเอียดเกี่ยวกับคำสั่งใด ๆ เพราะเป็นอีกครั้งที่มีรหัสมากเกินไปที่จะแสดงที่นี่ แต่คุณสามารถเห็นในรหัสเส้นทางต่อไปนี้ว่าฉันจัดการทั่วไปในการจัดการคำสั่งที่มีอยู่ (และอนาคตใด ๆ ):

 /** Interaction with a particular scene */ router.post('/:id/:playername/:scene', function(req, res, next) { let command = req.body command.context = { gameId: req.params.id, playername: req.params.playername, } let parser = new CommandParser(command) let commandObj = parser.parse() //return the command instance if(!commandObj) return next({ //error handling status: 400, errorCode: config.get("errorCodes.invalidCommand"), message: "Unknown command" }) commandObj.run((err, result) => { //execute the command if(err) return next(err) res.json(result) }) })

คำสั่งทั้งหมดต้องการเพียงวิธีการ run เท่านั้น อย่างอื่นที่พิเศษกว่าและมีไว้สำหรับใช้ภายใน

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

ปิดความคิด

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

ฉันหวังว่าคุณจะสนุกกับการอ่าน! โปรดแสดงความคิดเห็นด้านล่างพร้อมข้อเสนอแนะหรือคำแนะนำประเภทใดก็ได้ ฉันชอบที่จะอ่านสิ่งที่คุณคิด และหากคุณอยากเริ่มทดสอบ API ด้วยโค้ดฝั่งไคลเอ็นต์ของคุณเอง

เจอกันใหม่ตอนหน้าค่ะ!

ส่วนอื่นๆ ของซีรีส์นี้

  • ตอนที่ 1: บทนำ
  • ส่วนที่ 3: การสร้าง Terminal Client
  • ตอนที่ 4: การเพิ่มแชทในเกมของเรา