ความสามัคคีกับ MVC: วิธีเพิ่มระดับการพัฒนาเกมของคุณ
เผยแพร่แล้ว: 2022-03-11โปรแกรมเมอร์มือใหม่มักจะเริ่มเรียนรู้การแลกเปลี่ยนกับโปรแกรม Hello World
แบบคลาสสิก จากที่นั่น การมอบหมายที่ใหญ่ขึ้นและใหญ่ขึ้นจะต้องตามมา ความท้าทายใหม่แต่ละครั้งจะนำบทเรียนสำคัญกลับบ้าน:
ยิ่งโปรเจ็กต์ใหญ่ ปาเก็ตตี้ยิ่งใหญ่
ในไม่ช้า จะเห็นได้ง่ายว่าในทีมใหญ่หรือเล็ก เราไม่สามารถทำอย่างฟุ่มเฟือยได้ รหัสจะต้องได้รับการบำรุงรักษาและอาจใช้เวลานาน บริษัทที่คุณเคยทำงานด้วยไม่สามารถค้นหาข้อมูลติดต่อของคุณและถามคุณทุกครั้งที่ต้องการแก้ไขหรือปรับปรุง codebase (และคุณก็ไม่ต้องการให้เป็นเช่นนั้น)
นี่คือสาเหตุที่รูปแบบการออกแบบซอฟต์แวร์มีอยู่ พวกเขากำหนดกฎง่ายๆ เพื่อกำหนดโครงสร้างโดยรวมของโครงการซอฟต์แวร์ พวกเขาช่วยโปรแกรมเมอร์ตั้งแต่หนึ่งคนขึ้นไปแยกส่วนหลักของโปรเจ็กต์ขนาดใหญ่และจัดระเบียบให้เป็นมาตรฐาน ขจัดความสับสนเมื่อพบส่วนที่ไม่คุ้นเคยของโค้ดเบส
กฎเหล่านี้เมื่อทุกคนปฏิบัติตาม อนุญาตให้ดูแลและนำทางโค้ดเดิมได้ดีขึ้น และเพิ่มโค้ดใหม่ได้รวดเร็วยิ่งขึ้น ใช้เวลาน้อยลงในการวางแผนวิธีการพัฒนา เนื่องจากปัญหาไม่ได้มาในรูปแบบเดียว จึงไม่มีรูปแบบการออกแบบสัญลักษณ์แสดงหัวข้อย่อยสีเงิน เราต้องพิจารณาจุดแข็งและจุดอ่อนของแต่ละรูปแบบอย่างรอบคอบ และค้นหาสิ่งที่เหมาะสมที่สุดสำหรับความท้าทายที่อยู่ในมือ
ในบทช่วยสอนนี้ ฉันจะเล่าประสบการณ์ของฉันกับแพลตฟอร์มการพัฒนาเกม Unity ยอดนิยมและรูปแบบ Model-View-Controller (MVC) สำหรับการพัฒนาเกม ในการพัฒนาเจ็ดปีของฉัน หลังจากต่อสู้กับสปาเก็ตตี้ผู้พัฒนาเกม ฉันได้บรรลุโครงสร้างโค้ดที่ยอดเยี่ยมและความเร็วในการพัฒนาโดยใช้รูปแบบการออกแบบนี้
ฉันจะเริ่มต้นด้วยการอธิบายสถาปัตยกรรมพื้นฐานของ Unity เล็กน้อย รูปแบบ Entity-Component จากนั้นฉันจะอธิบายต่อว่า MVC เข้ากันได้อย่างไร และใช้โปรเจ็กต์จำลองเล็กๆ น้อยๆ เป็นตัวอย่าง
แรงจูงใจ
ในวรรณกรรมของซอฟต์แวร์ เราจะพบรูปแบบการออกแบบมากมาย แม้ว่าจะมีกฎเกณฑ์อยู่ชุดหนึ่ง แต่นักพัฒนามักจะบิดเบือนกฎเล็กน้อยเพื่อปรับรูปแบบให้เข้ากับปัญหาเฉพาะของตนได้ดียิ่งขึ้น
“เสรีภาพในการเขียนโปรแกรม” นี้เป็นข้อพิสูจน์ว่าเรายังไม่พบวิธีการออกแบบซอฟต์แวร์ที่ชัดเจนเพียงวิธีเดียว ดังนั้น บทความนี้ไม่ได้มุ่งหมายให้เป็นทางออกที่ดีที่สุดสำหรับปัญหาของคุณ แต่เพื่อแสดงประโยชน์และความเป็นไปได้ของรูปแบบที่รู้จักกันดีสองรูปแบบ: Entity-Component และ Model-View-Controller
รูปแบบเอนทิตี-ส่วนประกอบ
Entity-Component (EC) เป็นรูปแบบการออกแบบที่เรากำหนดลำดับชั้นขององค์ประกอบที่ประกอบขึ้นเป็นแอปพลิเคชัน (Entities) เป็นครั้งแรก และต่อมาเรากำหนดคุณลักษณะและข้อมูลที่แต่ละรายการจะมี (ส่วนประกอบ) ในแง่ "โปรแกรมเมอร์" มากขึ้น เอนทิตีสามารถเป็นอ็อบเจ็กต์ที่มีอาร์เรย์ตั้งแต่ 0 ขึ้นไป ส่วนประกอบ ลองวาดภาพเอนทิตีดังนี้:
some-entity [component0, component1, ...]
นี่เป็นตัวอย่างง่ายๆ ของแผนผัง EC
- app [Application] - game [Game] - player [KeyboardInput, Renderer] - enemies - spider [SpiderAI, Renderer] - ogre [OgreAI, Renderer] - ui [UI] - hud [HUD, MouseInput, Renderer] - pause-menu [PauseMenu, MouseInput, Renderer] - victory-modal [VictoryModal, MouseInput, Renderer] - defeat-modal [DefeatModal, MouseInput, Renderer]
EC เป็นแบบแผนที่ดีในการบรรเทาปัญหาของการสืบทอดหลายแบบ ซึ่งโครงสร้างคลาสที่ซับซ้อนสามารถนำเสนอปัญหาเช่นปัญหาเพชรที่คลาส D ซึ่งสืบทอดสองคลาส B และ C ที่มีคลาสพื้นฐาน A เดียวกัน สามารถทำให้เกิดความขัดแย้งได้เนื่องจาก B และ C ปรับเปลี่ยนคุณสมบัติของ A แตกต่างกัน
ปัญหาประเภทนี้สามารถเกิดขึ้นได้ทั่วไปในการพัฒนาเกมซึ่งมักใช้การสืบทอดอย่างกว้างขวาง
ด้วยการแบ่งคุณสมบัติและตัวจัดการข้อมูลออกเป็นส่วนประกอบที่มีขนาดเล็กลง พวกมันสามารถแนบและนำกลับมาใช้ใหม่ในเอนทิตีต่างๆ ได้โดยไม่ต้องอาศัยการสืบทอดหลายรายการ (ซึ่งยังไงก็ตาม มันไม่ใช่คุณสมบัติของ C# หรือ Javascript ซึ่งเป็นภาษาหลักที่ใช้โดย Unity ).
ที่ซึ่งเอนทิตี-ส่วนประกอบสั้นลง
ด้วยระดับที่สูงกว่า OOP หนึ่งระดับ EC จึงช่วยจัดระเบียบและจัดระเบียบสถาปัตยกรรมโค้ดของคุณได้ดียิ่งขึ้น อย่างไรก็ตาม ในโครงการขนาดใหญ่ เรายังคง "ว่างเกินไป" และเราสามารถพบตัวเองใน "มหาสมุทรแห่งคุณลักษณะ" ที่มีช่วงเวลาที่ยากลำบากในการค้นหาเอนทิตีและส่วนประกอบที่เหมาะสม หรือหาวิธีที่พวกเขาควรมีปฏิสัมพันธ์ มีหลายวิธีในการประกอบเอนทิตีและส่วนประกอบสำหรับงานที่กำหนด
วิธีหนึ่งในการหลีกเลี่ยงความยุ่งเหยิงคือการกำหนดแนวทางเพิ่มเติมบางประการที่ด้านบนขององค์ประกอบเอนทิตี ตัวอย่างเช่น วิธีหนึ่งที่ฉันชอบคิดเกี่ยวกับซอฟต์แวร์คือการแบ่งซอฟต์แวร์ออกเป็นสามประเภท:
- บางคนจัดการข้อมูลดิบ ทำให้สามารถสร้าง อ่าน อัปเดต ลบ หรือค้นหาได้ (เช่น แนวคิด CRUD)
- คนอื่นใช้อินเทอร์เฟซสำหรับองค์ประกอบอื่น ๆ เพื่อโต้ตอบ ตรวจจับเหตุการณ์ที่เกี่ยวข้องกับขอบเขตและเรียกการแจ้งเตือนเมื่อเกิดขึ้น
- สุดท้าย องค์ประกอบบางอย่างมีหน้าที่รับผิดชอบในการรับการแจ้งเตือนเหล่านี้ ตัดสินใจเชิงตรรกะทางธุรกิจ และตัดสินใจว่าควรจัดการข้อมูลอย่างไร
โชคดีที่เรามีรูปแบบที่ทำงานในลักษณะนี้อยู่แล้ว
รูปแบบ Model-View-Controller (MVC)
Model-View-Controller pattern (MVC) แบ่งซอฟต์แวร์ออกเป็นสามองค์ประกอบหลัก: Models (Data CRUD), Views (Interface/Detection) และ Controllers (Decision/Action) MVC มีความยืดหยุ่นเพียงพอที่จะนำไปใช้งานได้ แม้กระทั่งบน ECS หรือ OOP
การพัฒนาเกมและ UI มีเวิร์กโฟลว์ปกติในการรอข้อมูลจากผู้ใช้ หรือเงื่อนไขการกระตุ้นอื่นๆ การส่งการแจ้งเตือนเหตุการณ์เหล่านั้นในที่ที่เหมาะสม ตัดสินใจว่าจะทำอย่างไรเพื่อตอบกลับ และอัปเดตข้อมูลตามนั้น การดำเนินการเหล่านี้แสดงให้เห็นอย่างชัดเจนถึงความเข้ากันได้ของแอปพลิเคชันเหล่านี้กับ MVC
วิธีการนี้แนะนำเลเยอร์นามธรรมอีกชั้นหนึ่งที่จะช่วยในการวางแผนซอฟต์แวร์ และยังช่วยให้โปรแกรมเมอร์หน้าใหม่สามารถนำทางได้แม้ในฐานโค้ดที่ใหญ่ขึ้น ด้วยการแบ่งกระบวนการคิดออกเป็นข้อมูล อินเทอร์เฟซ และการตัดสินใจ นักพัฒนาสามารถลดจำนวนไฟล์ต้นฉบับที่ต้องค้นหาเพื่อเพิ่มหรือแก้ไขฟังก์ชันการทำงาน
ความสามัคคีและEC
มาดูสิ่งที่ Unity มอบให้เราก่อนดีกว่า
Unity เป็นแพลตฟอร์มการพัฒนาบน EC โดยที่ Entities ทั้งหมดเป็นอินสแตนซ์ของ GameObject
และฟีเจอร์ที่ทำให้ "มองเห็นได้" "เคลื่อนย้ายได้" "โต้ตอบได้" และอื่นๆ มีให้โดยคลาสที่ขยาย Component
แผงลำดับชั้นของตัวแก้ไข Unity และแผง ตัว ตรวจสอบ เป็นวิธีที่มีประสิทธิภาพในการประกอบแอปพลิเคชันของคุณ แนบส่วนประกอบ กำหนดค่าสถานะเริ่มต้นและบูตสแตรปเกมของคุณด้วยซอร์สโค้ดที่น้อยกว่าปกติมาก
ตามที่เราได้พูดคุยกัน เราสามารถประสบปัญหา "คุณลักษณะมากเกินไป" และพบว่าตัวเองอยู่ในลำดับชั้นที่ใหญ่โต ด้วยคุณลักษณะที่กระจัดกระจายอยู่ทุกหนทุกแห่ง ทำให้ชีวิตของนักพัฒนาซอฟต์แวร์ยากขึ้นมาก
คิดในทาง MVC แทน เราสามารถเริ่มต้นด้วยการแบ่งสิ่งต่าง ๆ ตามหน้าที่ของพวกมัน โดยจัดโครงสร้างแอปพลิเคชันของเราดังตัวอย่างด้านล่าง:
การปรับ MVC ให้เข้ากับสภาพแวดล้อมการพัฒนาเกม
ตอนนี้ ฉันอยากจะแนะนำการดัดแปลงเล็กๆ น้อยๆ สองครั้งกับรูปแบบ MVC ทั่วไป ซึ่งช่วยให้ปรับให้เข้ากับสถานการณ์เฉพาะที่ฉันเคยเจอในการสร้างโครงการ Unity ด้วย MVC:
- การอ้างอิงคลาส MVC จะกระจัดกระจายไปทั่วโค้ดได้อย่างง่ายดาย - ภายใน Unity นักพัฒนามักจะต้องลากและวางอินสแตนซ์ไปรอบๆ เพื่อให้สามารถเข้าถึงได้ หรือเข้าถึงผ่านคำสั่ง find ที่ยุ่งยาก เช่น
GetComponent( ... )
- นรกอ้างอิงที่หายไปจะเกิดขึ้นหาก Unity ขัดข้องหรือข้อผิดพลาดบางอย่างทำให้การอ้างอิงที่ลากทั้งหมดหายไป - สิ่งนี้ทำให้จำเป็นต้องมีออบเจ็กต์อ้างอิงรูทเดียว ซึ่งสามารถเข้าถึงและกู้คืนอินสแตนซ์ทั้งหมดใน แอปพลิเคชัน ได้ - องค์ประกอบบางอย่างห่อหุ้มฟังก์ชันการทำงานทั่วไปที่ควรนำมาใช้ใหม่ได้อย่างมาก และไม่ได้จัดอยู่ในหมวดหมู่หลักหนึ่งในสามประเภทหลักของ Model, View หรือ Controller สิ่งเหล่านี้ฉันชอบเรียกง่ายๆว่า Components พวกเขายังเป็น "ส่วนประกอบ" ในแง่ Entity-Component แต่ทำหน้าที่เป็นผู้ช่วยในกรอบงาน MVC เท่านั้น - ตัวอย่างเช่น
Rotator
Component ซึ่งหมุนเฉพาะสิ่งของด้วยความเร็วเชิงมุมที่กำหนด และไม่แจ้งเตือน จัดเก็บ หรือตัดสินใจใดๆ
เพื่อช่วยบรรเทาปัญหาทั้งสองนี้ ฉันจึงได้แก้ไขรูปแบบที่เรียกว่า AMVCC หรือ Application-Model-View-Controller-Component
- แอปพลิเคชัน - จุดเริ่มต้นของแอปพลิเคชันและคอนเทนเนอร์ของอินสแตนซ์ที่สำคัญทั้งหมดและข้อมูลที่เกี่ยวข้องกับแอปพลิเคชัน
- MVC - ตอนนี้คุณควรรู้เรื่องนี้แล้ว :)
- ส่วนประกอบ - สคริปต์ขนาดเล็กที่มีเนื้อหาครบถ้วนที่สามารถนำกลับมาใช้ใหม่ได้
การปรับเปลี่ยนทั้งสองนี้ตอบสนองความต้องการของฉันสำหรับโครงการทั้งหมดที่ฉันเคยใช้
ตัวอย่าง: 10 เด้ง
ยกตัวอย่างง่ายๆ ให้ดูที่เกมเล็กๆ ชื่อ 10 Bounces ซึ่งฉันจะใช้ประโยชน์จากองค์ประกอบหลักของรูปแบบ AMVCC
การตั้งค่าเกมนั้นง่ายมาก: A Ball
with a SphereCollider
and a Rigidbody
(ซึ่งจะเริ่มตกหลังจาก "เล่น"), Cube
เป็นพื้นและ 5 สคริปต์เพื่อสร้าง AMVCC
ลำดับชั้น
ก่อนเขียนสคริปต์ ฉันมักจะเริ่มที่ลำดับชั้นและสร้างโครงร่างของชั้นเรียนและทรัพย์สินของฉัน ติดตามรูปแบบ AMVCC ใหม่นี้เสมอ
ดังที่เราเห็นแล้วว่า view
GameObject มีองค์ประกอบภาพทั้งหมดและองค์ประกอบที่มีสคริปต์ View
อื่นๆ ด้วย GameObjects model
และ controller
สำหรับโปรเจ็กต์ขนาดเล็ก มักจะมีเฉพาะสคริปต์ที่เกี่ยวข้องเท่านั้น สำหรับโปรเจ็กต์ที่ใหญ่กว่า พวกเขาจะมี GameObjects พร้อมสคริปต์ที่เจาะจงมากขึ้น
เมื่อมีผู้นำทางโครงการของคุณต้องการเข้าถึง:
- Data : ไปที่
application > model > ...
- ลอจิก/เวิร์กโฟลว์: ไปที่
application > controller > ...
- Rendering/Interface/Detection: ไปที่
application > view > ...
หากทุกทีมปฏิบัติตามกฎง่ายๆ เหล่านี้ โครงการเดิมจะไม่เป็นปัญหา
โปรดทราบว่าไม่มีคอนเทนเนอร์ Component
เนื่องจากตามที่เราได้พูดคุยกัน พวกเขามีความยืดหยุ่นมากกว่าและสามารถแนบไปกับองค์ประกอบต่างๆ ได้ตามเวลาว่างของนักพัฒนา
การเขียนสคริปต์
หมายเหตุ: สคริปต์ที่แสดงด้านล่างเป็นเวอร์ชันนามธรรมของการนำไปใช้จริงในโลกแห่งความเป็นจริง การใช้งานโดยละเอียดจะไม่เป็นประโยชน์ต่อผู้อ่านมากนัก อย่างไรก็ตาม หากคุณต้องการสำรวจเพิ่มเติม นี่คือลิงก์ไปยังเฟรมเวิร์ก MVC ส่วนตัวของฉันสำหรับ Unity, Unity MVC คุณจะพบคลาสหลักที่ใช้เฟรมเวิร์กโครงสร้าง AMVCC ที่จำเป็นสำหรับแอปพลิเคชันส่วนใหญ่
มาดูโครงสร้างของสคริปต์สำหรับ 10 Bounces กัน
ก่อนเริ่ม สำหรับผู้ที่ไม่คุ้นเคยกับเวิร์กโฟลว์ของ Unity เรามาอธิบายสั้นๆ ว่าสคริปต์และ GameObjects ทำงานร่วมกันได้อย่างไร ใน Unity "ส่วนประกอบ" ในความหมายของ Entity-Component จะแสดงโดยคลาส MonoBehaviour
เพื่อให้มีอยู่ในระหว่างรันไทม์ ผู้พัฒนาควรลากและวางไฟล์ต้นฉบับลงใน GameObject (ซึ่งเป็น “เอนทิตี” ของรูปแบบเอนทิตี-คอมโพเนนต์) หรือใช้คำสั่ง AddComponent<YourMonobehaviour>()
หลังจากนี้ สคริปต์จะถูกสร้างอินสแตนซ์และพร้อมใช้งานระหว่างการดำเนินการ
ในการเริ่มต้น เรากำหนดคลาสแอปพลิเคชัน ("A" ใน AMVCC) ซึ่งจะเป็นคลาสหลักที่มีการอ้างอิงถึงองค์ประกอบเกมที่สร้างอินสแตนซ์ทั้งหมด นอกจากนี้ เราจะสร้างคลาสพื้นฐานของตัวช่วยที่เรียกว่า Element
ซึ่งช่วยให้เราเข้าถึงอินสแตนซ์ของแอปพลิเคชันและอินสแตนซ์ MVC ย่อยของแอปพลิเคชันได้
ด้วยเหตุนี้ เรามากำหนดคลาส Application
("A" ใน AMVCC) ซึ่งจะมีอินสแตนซ์ที่ไม่ซ้ำกัน ภายในนั้น ตัวแปรสามตัว model
, view
และ controller
จะให้จุดเข้าใช้งานสำหรับอินสแตนซ์ MVC ทั้งหมดระหว่างรันไทม์ ตัวแปรเหล่านี้ควรเป็น MonoBehaviour
ที่มีการอ้างอิง public
ถึงสคริปต์ที่ต้องการ
จากนั้น เราจะสร้างคลาสพื้นฐานของตัวช่วยที่เรียกว่า Element
ซึ่งช่วยให้เราเข้าถึงอินสแตนซ์ของแอปพลิเคชันได้ การเข้าถึงนี้จะอนุญาตให้ทุกคลาส MVC เข้าถึงกันได้
โปรดทราบว่าทั้งสองคลาสขยาย MonoBehaviour
พวกเขาคือ "ส่วนประกอบ" ที่จะแนบมากับ "เอนทิตี" ของ GameObject
// BounceApplication.cs // Base class for all elements in this application. public class BounceElement : MonoBehaviour { // Gives access to the application and all instances. public BounceApplication app { get { return GameObject.FindObjectOfType<BounceApplication>(); }} } // 10 Bounces Entry Point. public class BounceApplication : MonoBehaviour { // Reference to the root instances of the MVC. public BounceModel model; public BounceView view; public BounceController controller; // Init things here void Start() { } }
จาก BounceElement
เราสามารถสร้างคลาสหลัก MVC BounceModel
, BounceView
และ BounceController
มักจะทำหน้าที่เป็นคอนเทนเนอร์สำหรับอินสแตนซ์ที่เชี่ยวชาญมากขึ้น แต่เนื่องจากนี่เป็นเพียงตัวอย่างง่ายๆ เท่านั้น View จึงมีโครงสร้างที่ซ้อนกัน โมเดลและคอนโทรลเลอร์สามารถทำได้ในหนึ่งสคริปต์สำหรับแต่ละ:

// BounceModel.cs // Contains all data related to the app. public class BounceModel : BounceElement { // Data public int bounces; public int winCondition; }
// BounceView .cs // Contains all views related to the app. public class BounceView : BounceElement { // Reference to the ball public BallView ball; }
// BallView.cs // Describes the Ball view and its features. public class BallView : BounceElement { // Only this is necessary. Physics is doing the rest of work. // Callback called upon collision. void OnCollisionEnter() { app.controller.OnBallGroundHit(); } }
// BounceController.cs // Controls the app workflow. public class BounceController : BounceElement { // Handles the ball hit event public void OnBallGroundHit() { app.model.bounces++; Debug.Log(“Bounce ”+app.model.bounce); if(app.model.bounces >= app.model.winCondition) { app.view.ball.enabled = false; app.view.ball.GetComponent<RigidBody>().isKinematic=true; // stops the ball OnGameComplete(); } } // Handles the win condition public void OnGameComplete() { Debug.Log(“Victory!!”); } }
ด้วยสคริปต์ทั้งหมดที่สร้างขึ้น เราสามารถดำเนินการแนบและกำหนดค่าได้
เลย์เอาต์ลำดับชั้นควรเป็นดังนี้:
- application [BounceApplication] - model [BounceModel] - controller [BounceController] - view [BounceView] - ... - ball [BallView] - ...
การใช้ BounceModel
เป็นตัวอย่าง เราสามารถเห็นลักษณะที่ปรากฏในตัวแก้ไขของ Unity:
BounceModel
พร้อมฟิลด์ bounces
และ winCondition
ด้วยสคริปต์ทั้งหมดที่ตั้งไว้และเกมกำลังทำงาน เราควรได้รับผลลัพธ์นี้ใน Console Panel
การแจ้งเตือน
ดังที่แสดงในตัวอย่างด้านบน เมื่อลูกบอลกระทบพื้น มุมมองของมันจะเรียกใช้ app.controller.OnBallGroundHit()
ซึ่งเป็นวิธีการ ไม่ผิด แต่อย่างใดสำหรับการแจ้งเตือนทั้งหมดในแอปพลิเคชัน อย่างไรก็ตาม จากประสบการณ์ของฉัน ฉันได้รับผลลัพธ์ที่ดีขึ้นโดยใช้ระบบการแจ้งเตือนอย่างง่ายซึ่งใช้งานในคลาสแอปพลิเคชัน AMVCC
ในการดำเนินการดังกล่าว ให้อัปเดตเลย์เอาต์ของ BounceApplication
เป็น:
// BounceApplication.cs class BounceApplication { // Iterates all Controllers and delegates the notification data // This method can easily be found because every class is “BounceElement” and has an “app” // instance. public void Notify(string p_event_path, Object p_target, params object[] p_data) { BounceController[] controller_list = GetAllControllers(); foreach(BounceController c in controller_list) { c.OnNotification(p_event_path,p_target,p_data); } } // Fetches all scene Controllers. public BounceController[] GetAllControllers() { /* ... */ } }
ต่อไป เราจำเป็นต้องมีสคริปต์ใหม่ที่นักพัฒนาทั้งหมดจะเพิ่มชื่อกิจกรรมการแจ้งเตือน ซึ่งสามารถจัดส่งได้ในระหว่างการดำเนินการ
// BounceNotifications.cs // This class will give static access to the events strings. class BounceNotification { static public string BallHitGround = “ball.hit.ground”; static public string GameComplete = “game.complete”; /* ... */ static public string GameStart = “game.start”; static public string SceneLoad = “scene.load”; /* ... */ }
วิธีนี้ช่วยให้อ่านโค้ดได้ชัดเจนขึ้นโดยง่าย เนื่องจากนักพัฒนาไม่จำเป็นต้องค้นหาซอร์สโค้ดทั้งหมดสำหรับ controller.OnSomethingComplexName
วิธี OnSomethingComplexName เพื่อให้เข้าใจว่าการดำเนินการประเภทใดสามารถเกิดขึ้นได้ระหว่างการดำเนินการ การตรวจสอบเพียงไฟล์เดียวทำให้สามารถเข้าใจลักษณะการทำงานโดยรวมของแอปพลิเคชันได้
ตอนนี้ เราต้องปรับ BallView
และ BounceController
เพื่อจัดการกับระบบใหม่นี้เท่านั้น
// BallView.cs // Describes the Ball view and its features. public class BallView : BounceElement { // Only this is necessary. Physics is doing the rest of work. // Callback called upon collision. void OnCollisionEnter() { app.Notify(BounceNotification.BallHitGround,this); } }
// BounceController.cs // Controls the app workflow. public class BounceController : BounceElement { // Handles the ball hit event public void OnNotification(string p_event_path,Object p_target,params object[] p_data) { switch(p_event_path) { case BounceNotification.BallHitGround: app.model.bounces++; Debug.Log(“Bounce ”+app.model.bounce); if(app.model.bounces >= app.model.winCondition) { app.view.ball.enabled = false; app.view.ball.GetComponent<RigidBody>().isKinematic=true; // stops the ball // Notify itself and other controllers possibly interested in the event app.Notify(BounceNotification.GameComplete,this); } break; case BounceNotification.GameComplete: Debug.Log(“Victory!!”); break; } } }
โครงการที่ใหญ่กว่าจะมีการแจ้งเตือนมากมาย ดังนั้น เพื่อหลีกเลี่ยงการสร้างโครงสร้างเคสสวิตช์ขนาดใหญ่ ขอแนะนำให้สร้างคอนโทรลเลอร์ที่แตกต่างกัน และทำให้พวกเขาจัดการขอบเขตการแจ้งเตือนที่แตกต่างกัน
AMVCC ในโลกแห่งความเป็นจริง
ตัวอย่างนี้แสดงกรณีการใช้งานอย่างง่ายสำหรับรูปแบบ AMVCC การปรับวิธีคิดในแง่ขององค์ประกอบสามประการของ MVC และการเรียนรู้การมองเห็นเอนทิตีเป็นลำดับชั้นเป็นทักษะที่ควรปรับปรุง
ในโครงการที่ใหญ่ขึ้น นักพัฒนาจะต้องเผชิญกับสถานการณ์ที่ซับซ้อนมากขึ้นและสงสัยว่าบางสิ่งควรเป็น View หรือ Controller หรือหากคลาสที่กำหนดควรแยกอย่างละเอียดยิ่งขึ้นในคลาสที่เล็กกว่า
กฎของหัวแม่มือ (โดย Eduardo)
ไม่มี "คู่มือสากลสำหรับการเรียงลำดับ MVC" ทุกที่ แต่มีกฎง่ายๆ บางอย่างที่ฉันมักจะปฏิบัติตามเพื่อช่วยในการพิจารณาว่าจะกำหนดบางสิ่งเป็น Model, View หรือ Controller และเมื่อแบ่งคลาสที่กำหนดออกเป็นชิ้นเล็ก ๆ
โดยปกติ สิ่งนี้จะเกิดขึ้นเองตามธรรมชาติในขณะที่ฉันคิดถึงสถาปัตยกรรมซอฟต์แวร์หรือระหว่างการเขียนสคริปต์
การเรียงลำดับชั้นเรียน
โมเดล
- เก็บข้อมูลหลักและสถานะของแอปพลิเคชัน เช่น
health
ผู้เล่น หรือammo
- ทำให้เป็นอนุกรม ดีซีเรียลไลซ์ และ/หรือแปลงระหว่างประเภท
- โหลด/บันทึกข้อมูล (ในเครื่องหรือบนเว็บ)
- แจ้งผู้ควบคุมความคืบหน้าของการดำเนินงาน
- จัดเก็บสถานะเกมสำหรับเครื่องไฟไนต์สเตทแมชชีนของเกม
- ไม่เคยเข้าถึง Views
มุมมอง
- สามารถรับข้อมูลจาก Models เพื่อแสดงสถานะเกมที่ทันสมัยแก่ผู้ใช้ ตัวอย่างเช่น วิธีการดู
player.Run()
สามารถใช้model.speed
ภายในเพื่อแสดงความสามารถของผู้เล่น - ไม่ควรกลายพันธุ์โมเดล
- ใช้ฟังก์ชันของคลาสอย่างเคร่งครัด ตัวอย่างเช่น:
-
PlayerView
ไม่ควรใช้การตรวจจับอินพุตหรือแก้ไขสถานะเกม - มุมมองควรทำหน้าที่เป็นกล่องดำที่มีอินเทอร์เฟซและแจ้งเหตุการณ์สำคัญ
- ไม่จัดเก็บข้อมูลหลัก (เช่น ความเร็ว สุขภาพ ชีวิต…)
-
คอนโทรลเลอร์
- อย่าเก็บข้อมูลหลัก
- บางครั้งสามารถกรองการแจ้งเตือนจากมุมมองที่ไม่ต้องการได้
- อัปเดตและใช้ข้อมูลของโมเดล
- จัดการเวิร์กโฟลว์ฉากของ Unity
ลำดับชั้นของคลาส
ในกรณีนี้ ผมทำตามขั้นตอนไม่มาก โดยปกติ ฉันคิดว่าบางคลาสจำเป็นต้องแยกออกเมื่อตัวแปรเริ่มแสดง "คำนำหน้า" มากเกินไป หรือองค์ประกอบเดียวกันจำนวนมากเกินไปเริ่มปรากฏขึ้น (เช่น คลาสของ Player
ใน MMO หรือประเภท Gun
ใน FPS)
ตัวอย่างเช่น Model
เดียวที่มีข้อมูล Player จะมี playerDataA, playerDataB,...
หรือ Controller
ที่จัดการการแจ้งเตือนของ Player จะมี OnPlayerDidA,OnPlayerDidB,...
เราต้องการลดขนาดสคริปต์และกำจัดตัว player
และคำนำหน้าของ OnPlayer
ให้ฉันสาธิตการใช้คลาส Model
เพราะมันง่ายกว่าที่จะเข้าใจโดยใช้ข้อมูลเท่านั้น
ระหว่างการเขียนโปรแกรม ฉันมักจะเริ่มต้นด้วยคลาส Model
เดียวที่เก็บข้อมูลทั้งหมดสำหรับเกม
// Model.cs class Model { public float playerHealth; public int playerLives; public GameObject playerGunPrefabA; public int playerGunAmmoA; public GameObject playerGunPrefabB; public int playerGunAmmoB; // Ops Gun[CDE ...] will appear... /* ... */ public float gameSpeed; public int gameLevel; }
เป็นเรื่องง่ายที่จะเห็นว่ายิ่งเกมมีความซับซ้อนมากเท่าไร ตัวแปรก็จะยิ่งมีมากขึ้นเท่านั้น ด้วยความซับซ้อนมากพอ เราอาจจบลงด้วยคลาสยักษ์ที่มีตัวแปร model.playerABCDFoo
องค์ประกอบการซ้อนจะทำให้การกรอกโค้ดง่ายขึ้น และยังให้พื้นที่ในการสลับไปมาระหว่างรูปแบบต่างๆ ของข้อมูล
// Model.cs class Model { public PlayerModel player; // Container of the Player data. public GameModel game; // Container of the Game data. }
// GameModel.cs class GameModel { public float speed; // Game running speed (influencing the difficulty) public int level; // Current game level/stage loaded }
// PlayerModel.cs class PlayerModel { public float health; // Player health from 0.0 to 1.0. public int lives; // Player “retry” count after he dies. public GunModel[] guns; // Now a Player can have an array of guns to switch ingame. }
// GunModel.cs class GunModel { public GunType type; // Enumeration of Gun types. public GameObject prefab; // Template of the 3D Asset of the weapon. public int ammo; // Current number of bullets public int clips; // Number of reloads possible }
ด้วยการกำหนดค่าคลาสนี้ นักพัฒนาสามารถนำทางในซอร์สโค้ดอย่างสังหรณ์ใจได้ทีละแนวคิด สมมุติว่าเป็นเกมยิงมุมมองบุคคลที่หนึ่ง ซึ่งอาวุธและรูปแบบต่างๆ ของพวกมันจะมีมากมายมหาศาล ความจริงที่ว่า GunModel
มีอยู่ในคลาสทำให้สามารถสร้างรายการ Prefabs
(GameObjects ที่กำหนดค่าไว้ล่วงหน้าเพื่อทำซ้ำและนำกลับมาใช้ใหม่ในเกมได้อย่างรวดเร็ว) สำหรับแต่ละหมวดหมู่และเก็บไว้เพื่อใช้ในภายหลัง
ในทางตรงกันข้าม หากข้อมูลปืนทั้งหมดถูกจัดเก็บไว้ด้วยกันในคลาส GunModel
เดียว ในตัวแปรเช่น gun0Ammo
, gun1Ammo
, gun0Clips
เป็นต้น เมื่อผู้ใช้ต้องเผชิญกับความต้องการเก็บข้อมูล Gun
จะต้องเก็บข้อมูลทั้งหมด Model
รวมถึงข้อมูล Player
ที่ไม่ต้องการ ในกรณีนี้ จะเห็นได้ชัดว่าคลาส GunModel
ใหม่จะดีกว่า
เช่นเดียวกับทุกสิ่ง เหรียญมีสองด้าน บางครั้งอาจแบ่งช่องมากเกินไปโดยไม่จำเป็นและเพิ่มความซับซ้อนของโค้ดได้ เฉพาะประสบการณ์เท่านั้นที่สามารถฝึกฝนทักษะของคุณได้เพียงพอที่จะค้นหาการจัดเรียง MVC ที่ดีที่สุดสำหรับโครงการของคุณ
บทสรุป
มีรูปแบบซอฟต์แวร์มากมาย ในโพสต์นี้ ฉันพยายามแสดงสิ่งที่ช่วยฉันได้มากที่สุดในโครงการที่ผ่านมา นักพัฒนาควรซึมซับความรู้ใหม่อยู่เสมอ แต่ก็มักจะตั้งคำถามด้วยเช่นกัน ฉันหวังว่าบทช่วยสอนนี้จะช่วยให้คุณเรียนรู้สิ่งใหม่ ๆ และในขณะเดียวกันก็ทำหน้าที่เป็นบันไดขั้นเมื่อคุณพัฒนาสไตล์ของคุณเอง
นอกจากนี้ ฉันยังสนับสนุนให้คุณค้นคว้ารูปแบบอื่นๆ และค้นหารูปแบบที่เหมาะสมกับคุณที่สุด จุดเริ่มต้นที่ดีประการหนึ่งคือบทความ Wikipedia นี้ ซึ่งมีรายการรูปแบบและคุณลักษณะที่ยอดเยี่ยม
ถ้าคุณชอบรูปแบบ AMVCC และต้องการทดสอบ อย่าลืมลองใช้ไลบรารีของฉัน Unity MVC ซึ่งมีคลาสหลักทั้งหมดที่จำเป็นในการเริ่มแอปพลิเคชัน AMVCC
อ่านเพิ่มเติมในบล็อก Toptal Engineering:
- การพัฒนา Unity AI: บทช่วยสอนเกี่ยวกับเครื่องจักรที่มีสถานะจำกัด