การเขียนเอ็นจิ้นการผจญภัยข้อความสำหรับผู้เล่นหลายคนใน Node.js: การออกแบบเซิร์ฟเวอร์เอ็นจิ้นเกม (ตอนที่ 2)
เผยแพร่แล้ว: 2022-03-10หลังจากการพิจารณาอย่างรอบคอบและการนำโมดูลไปใช้จริงแล้ว คำจำกัดความบางอย่างที่ฉันทำระหว่างขั้นตอนการออกแบบจะต้องเปลี่ยนไป นี่ควรเป็นฉากที่คุ้นเคยสำหรับทุกคนที่เคยทำงานกับลูกค้าที่กระตือรือร้นที่ฝันถึงผลิตภัณฑ์ในอุดมคติ แต่ต้องถูกควบคุมโดยทีมพัฒนา
เมื่อใช้งานและทดสอบคุณลักษณะแล้ว ทีมของคุณจะเริ่มสังเกตเห็นว่าคุณลักษณะบางอย่างอาจแตกต่างไปจากแผนเดิม และไม่เป็นไร เพียงแจ้ง ปรับเปลี่ยน และไปต่อ ดังนั้น โดยไม่ต้องกังวลใจอีกต่อไป ให้ฉันอธิบายก่อนว่ามีอะไรเปลี่ยนแปลงไปจากแผนเดิมบ้าง
ส่วนอื่นๆ ของซีรีส์นี้
- ตอนที่ 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: การเพิ่มแชทในเกมของเรา