Entity-Component-System (ECS) เป็นรูปแบบการออกแบบสถาปัตยกรรมแบบกระจายและจัดองค์ประกอบซึ่งส่วนใหญ่ใช้ในการพัฒนาเกม ช่วยให้สามารถแยกพฤติกรรมเฉพาะโดเมนได้อย่างยืดหยุ่น ซึ่งเอาชนะข้อเสียหลายประการของการสืบทอดเชิงวัตถุแบบดั้งเดิม
Elixir คือภาษาแบบไดนามิกและใช้งานได้จริงซึ่งสร้างขึ้นจาก Erlang VM ซึ่งออกแบบมาเพื่อสร้างแอปพลิเคชันที่ปรับขนาดได้และบำรุงรักษาได้
ในบทความนี้ ค้นพบว่าเราสามารถใช้ทั้ง ECS และ Elixir ในแนวทางใหม่เพื่อจัดโครงสร้างโปรแกรมของเราให้เหนือกว่ากระบวนทัศน์การสืบทอดตามคลาสได้อย่างไร
ซอร์สโค้ดสำหรับการใช้งาน ECS ของฉันใน Elixir เป็นโอเพ่นซอร์สบน Github
ข้อเสียของการสืบทอดตามคลาส
วิธีดั้งเดิมในการสร้างโลกของเกมคือการมีลำดับชั้นเชิงวัตถุของวัตถุในเกมที่สร้างแบบจำลองโลก อย่างไรก็ตาม แม้แต่ออบเจ็กต์ธรรมดาก็สามารถจบลงด้วยชุดฟังก์ชันที่ไม่ได้ใช้จำนวนมากได้ ลองพิจารณาตัวอย่างด้านล่าง:
เรากำลังสร้างเอ็นจิ้นเกม และเราพบว่าตัวเองอยู่ในลำดับชั้นด้านล่าง:
เรามีฐาน GameObject
ซึ่งถูกจัดประเภทย่อยด้วย Animal
Animal
ถูกจัดประเภทย่อยด้วย Bunny
และ Whale
ซึ่งแต่ละรายการมีลักษณะการทำงานพิเศษของตัวเอง hop()
และ swim()
ตามลำดับ นอกจากนี้เรายังมี Killer Whale
ซึ่งเป็นคลาสย่อยของ Whale
ซึ่งมากกว่า kill()
มาลองแนะนำสัตว์ชนิดใหม่ให้กับโลกของเรา:
เราต้องการให้
Killer Bunny
สามารถhop()
และkill()
ได้ แต่คลาสใดที่Killer Bunny
ควรสืบทอดมาจาก?
สำหรับภาษา/แพลตฟอร์มที่มีการสืบทอดเพียงรายการเดียว เราโชคไม่ดี เราจะต้องย้ายทั้ง hop()
และ kill()
ไปยังซูเปอร์คลาสบางตัว เช่น Animal
ที่ Killer Bunny
สามารถสืบทอดได้ อย่างไรก็ตาม คลาสย่อยอื่นๆ ทั้งหมดของ Animal
จะสืบทอดสิ่งที่พวกเขาไม่ต้องการ Whale
สืบทอด hop()
; Bunny
สืบทอด swim()
และ kill()
เมื่อเวลาผ่านไป Animal
จะกลายเป็นวัตถุเทพที่มีพฤติกรรมมากมาย
การสืบทอดหลายรายการก็ไม่ได้ทำเช่นกัน สมมติว่า Killer Bunny
สืบทอดมาจากทั้ง Bunny
และ Killer Whale
Killer Bunny
จะสืบทอด swim()
ซึ่งเป็นฟังก์ชันที่ไม่จำเป็น
เราเผชิญกับปัญหาอื่นๆ อีกหลายประการ:
ฟังก์ชันการทำงานที่เข้มงวด: มีเพียง Killer Whale
เท่านั้นที่สามารถ kill()
เราไม่สามารถเปลี่ยนใจในภายหลังและทำให้สัตว์อื่น kill()
เป็นเรื่องง่ายมาก พฤติกรรมใช้ได้เฉพาะกับคลาสที่ได้รับการเข้ารหัสโดยเฉพาะเพื่อสนับสนุนพฤติกรรมนั้น เมื่อจำนวนเอนทิตีเกมเพิ่มมากขึ้น เราก็เผชิญกับความยากลำบากมากขึ้นในการหาจุดในลำดับชั้นเพื่อวางเอนทิตีใหม่ไว้ข้างใต้
ปัญหาเพชร: "ปัญหาเพชร" (บางครั้งเรียกว่า "เพชรมรณะ") เป็นความคลุมเครือที่เกิดขึ้นเมื่อคลาส B และ C สองชั้นสืบทอดมาจาก A และคลาส D สืบทอดมาจากทั้งสองคลาส B และ C หากมีวิธีการใน A ที่ B และ C ได้เขียนทับไว้ และ D ไม่ได้แทนที่มัน แล้ว D สืบทอดเวอร์ชันใด: ของ B หรือของ C?
รูปแบบต่อต้าน Blob: ด้วยการสืบทอด เกมจะจบลงด้วยคลาสรูทเดี่ยวขนาดใหญ่หรือลีฟโหนดอื่นที่มีฟังก์ชันการทำงานจำนวนมาก คลาสย่อยมีภาระมากเกินไปกับฟังก์ชันการทำงานที่ไม่จำเป็น
สรุป
ปัญหาที่กล่าวมาข้างต้นรบกวนผู้พัฒนาเกมมาเป็นเวลานาน และ Entity Component Systems พยายามที่จะแก้ไขสิ่งรบกวนเหล่านี้ เราจะเรียนรู้เกี่ยวกับ ECS ในหัวข้อถัดไป
ระบบส่วนประกอบเอนทิตี
มีนามธรรมที่สำคัญสามประการใน ECS: เอนทิตี ส่วนประกอบ และระบบ
เราจะตรวจสอบแต่ละรายการโดยละเอียด เริ่มตั้งแต่ Component
ส่วนประกอบ
คุณสมบัติหรือลักษณะของเอนทิตี
ส่วนประกอบคือออบเจ็กต์ข้อมูลขั้นต่ำที่สามารถนำกลับมาใช้ซ้ำได้ซึ่งเสียบเข้ากับเอนทิตีเพื่อรองรับพฤติกรรมบางอย่าง ส่วนประกอบแท็กเอนทิตีที่มีคุณภาพเดียว ส่วนประกอบนั้นไม่มีพฤติกรรม โดยปกติแล้วจะนำไปใช้เป็นโครงสร้างหรือพจนานุกรม
ลองนึกภาพเรามีเอนทิตี Bunny
ในโลกของเรา:
เราสามารถนิยามกระต่ายว่าไม่มีอะไรมากไปกว่าการรวมตัว/การรวบรวมส่วนประกอบอิสระ ในตัวอย่างข้างต้น Bunny จะ 'ประกอบด้วย' ส่วนประกอบต่างๆ เช่น Physical
และ Seeing
แต่ละองค์ประกอบสนับสนุนพฤติกรรมบางอย่าง เพื่อแสดงให้เห็น Seeing
มีคุณลักษณะ sight_radius
เพื่อรองรับพฤติกรรมการมองเห็น อย่างไรก็ตาม โปรดทราบว่า Component เองก็ไม่มีพฤติกรรมใดๆ แต่ละส่วนประกอบเป็นเพียงออบเจ็กต์ข้อมูลขั้นต่ำ
เอนทิตี
การรวมกลุ่มหรือคอนเทนเนอร์ของส่วนประกอบ
เอนทิตีเป็นเพียงผลรวมของส่วนประกอบเท่านั้น เอนทิตีถูกนำมาใช้เป็นรหัสที่ไม่ซ้ำกันทั่วโลกซึ่งเชื่อมโยงกับคอลเลกชันของส่วนประกอบ โปรดทราบว่าเอนทิตีเองไม่มีข้อมูลหรือพฤติกรรมที่แท้จริง แต่ละองค์ประกอบให้ข้อมูลเอนทิตีเพื่อสนับสนุนพฤติกรรมบางอย่าง
ลองดูที่ Bunny
ของเราอีกครั้ง:
เห็นกล่องประรอบๆ ส่วนประกอบของเราไหม นั่นคือเอนทิตี Bunny
- ไม่มีอะไรมากไปกว่าคอนเทนเนอร์ของส่วนประกอบ เราสามารถกำหนดเอนทิตีเป็นการรวมชุดย่อยของส่วนประกอบใดๆ เช่นนี้ Carrot
:
และนี่คือ Ghost
:
เอนทิตีเป็นมากกว่าการรวบรวมส่วนประกอบเพียงเล็กน้อย
การใช้งาน ECS บางอย่างทำให้คุณสามารถแก้ไขการรวบรวมส่วนประกอบของเอนทิตีขณะรันไทม์ได้ สิ่งนี้ช่วยให้คุณ "กลายพันธุ์" เอนทิตีได้ทันที ตัวอย่างเช่น เราอาจมีคอมโพเนนต์
Poisoned
ที่ทำให้เอนทิตีที่แท็กด้วยคอมโพเนนต์นี้สูญเสียสุขภาพเมื่อเวลาผ่านไป เราสามารถเพิ่มและลบส่วนประกอบนี้แบบไดนามิกเพื่อสร้างและรักษาพิษได้ คุณอาจมีเอฟเฟกต์สถานะ 'ตาบอด' ซึ่งจะลบองค์ประกอบSeeing
ของเอนทิตีที่ได้รับผลกระทบ
จนถึงจุดนี้ เราไม่ได้แตะต้องตรรกะหรือพฤติกรรมใดๆ เลย เอนทิตีเป็นเพียงการรวมส่วนประกอบต่างๆ ส่วนประกอบเป็นเพียงวัตถุข้อมูล พฤติกรรมใน ECS มาจากไหน? พวกเขามาจากระบบ
ระบบ
ระบบทำให้เอนทิตีและส่วนประกอบต่างๆ มีชีวิตขึ้นมา
ระบบจะแจกแจงส่วนประกอบหรือกลุ่มของส่วนประกอบ โดยอัปเดตสถานะตามกฎภายในหรือเหตุการณ์ภายนอก วิธีคิดเกี่ยวกับพฤติกรรมคือการเปลี่ยนแปลงจากสถานะหนึ่งไปอีกสถานะหนึ่ง ลองดูตัวอย่าง:
พฤติกรรม: “กระต่ายบนต้นไม้ตกลงมาเนื่องจากแรงโน้มถ่วง”
เราจะนำพฤติกรรมข้างต้นไปใช้อย่างไร? เราสามารถทำได้เพื่อให้ Placeable
Components ที่มีค่า z
มากกว่า 0 ลดลงเมื่อเวลาผ่านไปเป็น 0
พฤติกรรม: “อายุของสิ่งมีชีวิต”
เราจะนำพฤติกรรมข้างต้นไปใช้อย่างไร? เราสามารถทำได้เพื่อให้ Living
Components มีค่าเพิ่มขึ้น age
เมื่อเวลาผ่านไป
เราสร้างระบบเฉพาะสำหรับแต่ละพฤติกรรมที่เราต้องการสนับสนุน A GravitySystem
ระบุส่วนประกอบ Placeable
ทั้งหมด a TimeSystem
ระบุส่วนประกอบ Living
ทั้งหมด โปรดทราบว่าระบบดำเนินการกับส่วนประกอบ ไม่ใช่เอนทิตี
การไหลของข้อมูลในเอนทิตี-ส่วนประกอบ-ระบบ
เพื่อให้คุณเข้าใจรูปแบบนี้มากขึ้น เรามาดูกระแสข้อมูลทั่วไปในสถาปัตยกรรมนี้กันดีกว่า:
แต่ละระบบจะรับฟังสตรีมเหตุการณ์บางอย่าง เช่น เวลาหรืออินพุตของผู้เล่น และอัปเดตสถานะของส่วนประกอบเพื่อตอบสนองต่อเหตุการณ์นั้นและกฎภายในบางอย่าง สถานะที่เปลี่ยนแปลงอย่างต่อเนื่องเหล่านี้สามารถเข้าถึงได้โดยหน่วยงานที่ตนเป็นส่วนหนึ่งของ และส่งผลให้เกิดพฤติกรรม
อีกตัวอย่างหนึ่ง: สมมติว่าผู้เล่นกดปุ่ม "ย้ายไปทางซ้าย" PlayerInputSystem ดำเนินการและตรวจจับการกดปุ่ม โดยอัปเดตองค์ประกอบ Motion MotionSystem ดำเนินการและ "มองเห็น" การเคลื่อนที่ของเอนทิตีที่อยู่ทางซ้าย โดยใช้แรงฟิสิกส์ไปทางซ้าย RenderSystem ดำเนินการและอ่านตำแหน่งปัจจุบันของเอนทิตี และวาดตามคำจำกัดความเชิงพื้นที่ (ซึ่งอาจรวมถึงข้อมูลพื้นที่พื้นผิว / ภาพเคลื่อนไหว)
การเปรียบเทียบสเปรดชีต
วิธีคิด ECS อีกวิธีหนึ่งก็คือเป็นตารางเชิงสัมพันธ์ เช่นเดียวกับสเปรดชีต:
ระบบส่วนประกอบเอนทิตีสามารถแสดงภาพเป็นตารางที่มีคอลัมน์ของส่วนประกอบและแถวของเอนทิตี ในการทำงานกับส่วนประกอบเดียว เราเลือกคอลัมน์และดูที่แต่ละเซลล์ เพื่อดำเนินการกับเอนทิตี เราจะเลือกแถวและดูแต่ละเซลล์
ข้อดีของอีซีเอส
ตอนนี้เรามีความเข้าใจที่ดีขึ้นเกี่ยวกับสถาปัตยกรรม Entity-Component-System แล้ว ลองมาคิดว่าวิธีการนี้เปรียบเทียบกับการสืบทอดตามคลาสอย่างไร
การแยกส่วนที่ดี หลักการความรับผิดชอบเดียว: แต่ละพฤติกรรมหรือโดเมนจะถูกแยกออกจากกันในองค์ประกอบและ/หรือระบบที่เป็นอิสระ แตกต่างจากอ็อบเจ็กต์พระเจ้าเสาหินของเราในการสืบทอดตามคลาส เราสามารถแยกชุดย่อยของฟังก์ชันการทำงานและประกอบเข้าด้วยกันในรูปแบบใดก็ได้ ECS ยังสนับสนุนอินเทอร์เฟซขนาดเล็กอีกด้วย
ความสามารถในการเรียบเรียงและคำจำกัดความของออบเจ็กต์รันไทม์: คุณสามารถสร้างออบเจ็กต์เกมประเภทใดก็ได้โดยการเพิ่มส่วนประกอบที่ถูกต้องให้กับเอนทิตี นอกจากนี้ยังช่วยให้นักพัฒนาสามารถเพิ่มคุณสมบัติของออบเจ็กต์ประเภทหนึ่งไปยังอีกประเภทหนึ่งได้อย่างง่ายดาย โดยไม่มีปัญหาการพึ่งพาใดๆ ตัวอย่างเช่น เราสามารถทำ Entity.build([FlyingComponent, SeeingComponent])
ขณะรันไทม์ได้
ทดสอบได้: แต่ละส่วนประกอบและระบบเป็นหน่วยตามคำจำกัดความ นอกจากนี้เรายังสามารถทดแทนส่วนประกอบด้วยส่วนประกอบจำลองหรือสาธิตสำหรับการทดสอบ
สามารถขนานกันได้: ในการใช้งาน ECS ในโลกแห่งความเป็นจริง เช่น MMO ระบบจะถูกนำไปใช้เป็นระบบแบบกระจายหรือกลุ่มผู้ปฏิบัติงานที่สามารถกระจายงานระหว่างกันได้ สิ่งนี้ช่วยให้เราปรับขนาดการจำลองในแนวนอนโดยการเพิ่มจำนวนผู้ปฏิบัติงานระบบในกลุ่มของเรา
การแยกข้อมูลและพฤติกรรม: ส่วนประกอบเก็บข้อมูล ระบบเก็บข้อมูลพฤติกรรม ไม่มีการผสมผสานระหว่างทั้งสอง คุณสมบัตินี้ช่วยให้คุณสามารถเสียบปลั๊กและเล่นพฤติกรรมที่แตกต่างกันเพื่อนำไปใช้กับข้อมูลเดียวกันได้
ความท้าทายของ ECS
แม้ว่าสิ่งนี้จะทำให้เรามีความยืดหยุ่น แต่ ECS ก็นำเสนอความท้าทายที่ไม่สำคัญหลายประการ:
ECS เป็นรูปแบบที่ค่อนข้างไม่เป็นที่รู้จัก: เนื่องจากรูปแบบการออกแบบนี้ส่วนใหญ่จำกัดอยู่ที่การพัฒนาเกม การพูดคุยถึงวิธีใช้ ECS สำหรับโดเมนภายนอก เช่น การสร้างเว็บแอปอาจเป็นเรื่องที่ท้าทาย มีทรัพยากรเพียงเล็กน้อยสำหรับการนำรูปแบบนี้ไปใช้กับโดเมนอื่นๆ ถ้ามี
การจัดการการสื่อสารระหว่างกระบวนการ: เราจะจัดการการสื่อสารระหว่างระบบและส่วนประกอบอย่างไร เราต้องการบัสข้อความหรือระบบเผยแพร่และสมัครสมาชิกเพื่อให้บางส่วนของ ECS ของเราพูดคุยกันได้ ขึ้นอยู่กับภาษาหรือแพลตฟอร์มที่ใช้ ECS สิ่งนี้อาจทำให้เกิดความซับซ้อนที่เพิ่มขึ้นอย่างรวดเร็ว ค่าใช้จ่ายในการทำซ้ำผ่านส่วนประกอบและเอนทิตีอาจส่งผลให้ประสิทธิภาพลดลง
การสื่อสารระหว่างองค์ประกอบ: จะเกิดอะไรขึ้นเมื่อระบบต้องการเข้าถึงและแก้ไขข้อมูลในหลายองค์ประกอบ ส่วนประกอบอาจจำเป็นต้องแบ่งปันสถานะกับส่วนประกอบอื่น ๆ และสื่อสารระหว่างกันก่อนที่จะสื่อสารกับระบบ ตัวอย่างเช่น สมมติว่าเรามีองค์ประกอบ Position
และ Sound
ในเอนทิตี เราอาจมี PositionalSoundSystem
ที่ต้องสื่อสารกับทั้งสององค์ประกอบ เราอาจต้องมีช่องทางแยกต่างหากสำหรับการสื่อสารระหว่างองค์ประกอบเพื่อรองรับกรณีการใช้งานนี้
การสื่อสารระหว่างระบบ: จะเกิดอะไรขึ้นเมื่อสองระบบจำเป็นต้องเข้าถึงและแก้ไขส่วนประกอบเดียวกัน สมมติว่าเรามีสองระบบ: ระบบหนึ่งคูณแอตทริบิวต์ x
ด้วย -1
และอีกระบบหนึ่งบวก x
ด้วย 10
ผลลัพธ์ที่ได้จะแตกต่างกันขึ้นอยู่กับลำดับการใช้งานของทั้งสองระบบ เว้นแต่การดำเนินการจะเชื่อมโยงกัน เราอาจจำเป็นต้องแนะนำวิธีการเพื่อให้แน่ใจว่าลำดับของระบบถูกต้อง
ไม่ได้กำหนดอย่างเป็นรูปธรรมเท่ากับรูปแบบการออกแบบอื่นๆ เช่น MVC: มีหลายวิธีในการนำ ECS ไปใช้ แต่ละภาษาหรือแพลตฟอร์มจะมีบทคัดย่อที่แตกต่างกัน ซึ่งส่งผลให้ ECS มีรสชาติที่แตกต่างกัน
ECS ในโลกแห่งความเป็นจริง
นอกเหนือจากการเป็นสถาปัตยกรรมยอดนิยมสำหรับวิดีโอเกมแล้ว แอปพลิเคชันปัจจุบันของ ECS ยังใช้สำหรับการจำลองแบบกระจายขนาดใหญ่ ซึ่งรวมถึงการจราจรในเมืองแบบเรียลไทม์ เครือข่ายโทรคมนาคมทางอินเทอร์เน็ต และการจำลองทางฟิสิกส์ นอกจากนี้ยังใช้ในการสร้างแบ็กเอนด์ที่มีผู้เล่นหลายคนจำนวนมากสำหรับวิดีโอเกมที่มีเอนทิตีจำนวนมาก
สตาร์ทอัพรายหนึ่งกำลังสร้างบริการ ECS-as-a-a ที่เรียกว่า "SpatialOS"
การใช้งาน ECS ใน Elixir
ในส่วนนี้ เราจะมาดูการใช้งาน Entity-Component-System ที่เป็นไปได้ใน Elixir ฉันจะเริ่มด้วยการพูดถึงสั้นๆ ว่าทำไม Elixir (หรือ Erlang) และการทำงานพร้อมกันแบบดั้งเดิมจึงเหมาะสมกับรูปแบบ ECS
จากนี้ไป เนื่องจาก Elixir คอมไพล์เป็นโค้ดไบต์ Erlang เมื่อฉันพูดว่า Elixir ฉันก็หมายถึง Erlang ด้วย
นักแสดงรุ่น
สิ่งสำคัญประการหนึ่งของ Elixir คือกระบวนการ ซึ่งคล้ายกับนักแสดงที่เป็นนางแบบ นักแสดงเป็นเอนทิตีการคำนวณที่สามารถ:
- ส่งข้อความ
- รับข้อความ
- วางไข่นักแสดงหน้าใหม่
ในแผนภาพด้านบน นักแสดง A
ส่งข้อความ 1
และ 2
ไปยังนักแสดง C
ซึ่งได้รับ เพื่อตอบสนองต่อข้อความเหล่านี้ นักแสดง C สามารถส่งข้อความใหม่ หรือวางไข่นักแสดงใหม่และรอรับข้อความจากนักแสดงเหล่านั้น
# Try running this in an Elixir interpreter (iex)
# We first spawn a new actor that listens for messages, returning a process id (pid)
jeff = spawn(fn ->
receive do
{sender, message} -> IO.puts "Received '#{message}' from process #{inspect sender}"
end
end)
# We send a message to jeff's pid, adding our own pid in the message
send jeff, {self(), “Hello world”}
# send, receive, and spawn are built-in Elixir primitives
Elixir ยังมีนามธรรมระดับสูงกว่าสำหรับการสร้างนักแสดงที่เรียกว่า GenServers
:
defmodule Stack do
use GenServer
# Callbacks
def handle_call(:pop, _from, [h | t]) do
{:reply, h, t}
end
def handle_cast({:push, item}, state) do
{:noreply, [item | state]}
end
end
# Start the server
{:ok, pid} = GenServer.start_link(Stack, [:hello])
# This is the client
GenServer.call(pid, :pop)
#=> :hello
GenServer.cast(pid, {:push, :world})
#=> :ok
GenServer.call(pid, :pop)
#=> :world
พิจารณาว่าคุณจะนำ ECS ไปใช้อย่างไรโดยได้รับความช่วยเหลือจากนักแสดง
ตัวอย่างการใช้งาน
ต่อไปนี้คือลักษณะการใช้งานของเราเมื่อใช้งาน:
# Instantiates a new entity with a set of parameterized components
# TimeComponent is a component that counts up
> bunny = ECS.Entity.build([TimeComponent.new(%{age: 0})])
# We trigger the TimeSystem to enumerate over all TimeComponents
# In the real world this could be in response to an event stream such as player input
> TimeSystem.process
# We pull the latest state of the components
> bunny = ECS.Entity.reload(bunny)
# We can repeat this process
> TimeSystem.process
> bunny = ECS.Entity.reload(bunny)
# Modifies an existing entity at runtime by adding a new component to it
bunny = ECS.Entity.add(bunny, TimeComponent.new(%{age: 10}))
# We can repeat this process, and both TimeComponents will receive state updates
TimeSystem.process
bunny = ECS.Entity.reload(bunny)
การใช้งาน ECS ของฉันใน Elixir นั้นเป็นโอเพ่นซอร์สบน Github คุณสามารถโคลนและเรียกใช้ผ่าน iex -S mix
จากโฟลเดอร์รูท คุณต้องติดตั้ง Elixir ไว้ในเครื่องของคุณ
การนำไปปฏิบัติ
เอนทิตีคือโครงสร้างที่มีสตริงสุ่ม id
และรายการส่วนประกอบ เราสามารถสร้างเอนทิตีและขยายได้โดยการเพิ่มส่วนประกอบ สามารถทำได้ทั้งสองอย่างในขณะรันไทม์
defmodule ECS.Entity do
@moduledoc """
A base for creating new Entities.
"""
defstruct [:id, :components]
@type id :: String.t
@type components :: list(ECS.Component)
@type t :: %ECS.Entity{
id: String.t,
components: components
}
@doc "Creates a new entity"
@spec build(components) :: t
def build(components) do
%ECS.Entity{
id: ECS.Crypto.random_string(64),
components: components
}
end
@doc "Add components at runtime"
def add(%ECS.Entity{ id: id, components: components}, component) do
%ECS.Entity{
id: id,
components: components ++ [component]
}
end
@doc "Pulls the latest component states"
@spec reload(t) :: t
def reload(%ECS.Entity{ id: _id, components: components} = entity) do
updated_components = components
|> Enum.map(fn %{id: pid} ->
ECS.Component.get(pid)
end)
%{entity | components: updated_components}
end
end
ด้านล่างนี้เป็นเอนทิตีจริง Bunny
:
# A bunny prefab
defmodule Bunny do
def new do
ECS.Entity.build([TimeComponent.new(%{age: 0})])
end
end
โค้ดด้านบนแนะนำแนวคิดของ 'รูปแบบสำเร็จรูป' ซึ่งเป็นโรงงานที่สะดวกสบายสำหรับเอนทิตีที่มีชุดส่วนประกอบร่วมกัน การใช้รูปแบบสำเร็จรูปช่วยให้คุณไม่ต้องพิมพ์มากเกินไป และทำหน้าที่เป็นส่วนหน้า
ส่วนประกอบ
โมดูล Component
และ Component.Agent
อำนวยความสะดวกในการรับและตั้งค่าสถานะ แต่ละองค์ประกอบได้รับการสนับสนุนโดยนักแสดง (ตัวแทน - GenServer ชนิดหนึ่ง) ส่วนประกอบเช่น TimeComponent
ใช้พฤติกรรม Component
(อินเทอร์เฟซ)
ddefmodule ECS.Component do @moduledoc """ A base for creating new Components. """ defstruct [:id, :state] @type id :: pid() @type component_type :: String.t @type state :: map() @type params :: map() @type t :: %ECS.Component{ id: id, # Component Agent ID state: state } @callback new(state) :: t # Component interface defmacro __using__(_options) do quote do @behaviour ECS.Component # Require Components to implement interface end end @doc "Create a new agent to keep the state" @spec new(component_type, state) :: t def new(component_type, initial_state) do {:ok, pid} = ECS.Component.Agent.start_link(initial_state) ECS.Registry.insert(component_type, pid) # Register component for systems to reference %{ id: pid, state: initial_state } end @doc "Retrieves state" @spec get(id) :: t def get(pid) do state = ECS.Component.Agent.get(pid) %{ id: pid, state: state } end @doc "Updates state" @spec update(id, state) :: t def update(pid, new_state) do ECS.Component.Agent.set(pid, new_state) %{ id: pid, state: new_state } end end
defmodule ECS.Component.Agent do @moduledoc """ Create a simple Agent that gets and sets. Each component instantiates one to keep state. """ @doc "Starts a new bucket. Returns {:status, pid}" def start_link(initial_state \\ %{}, opts \\ []) do Agent.start_link((fn -> initial_state end), opts) end @doc "Gets entire state from pid" def get(pid) do Agent.get(pid, &(&1)) end @doc "Gets a value from the `pid` by `key`" def get(pid, key) do Agent.get(pid, &Map.get(&1, key)) end @doc "Overwrites state with new_state." def set(pid, new_state) do Agent.update(pid, &Map.merge(&1, new_state)) end @doc "Updates the `value` for the given `key` in the `pid`" def set(pid, key, value) do Agent.update(pid, &Map.put(&1, key, value)) end end
ด้านล่างนี้เป็นองค์ประกอบจริง TimeComponent
ซึ่งใช้พฤติกรรม Component
:
defmodule TimeComponent do
@moduledoc """
A component for keeping an age for something.
{id: pid, state: state} = TimeComponent.new(%{age: 1})
"""
use ECS.Component
@component_type __MODULE__
@doc "Initializes and validates state"
def new(%{age: _age} = initial_state) do
ECS.Component.new(@component_type, initial_state)
end
end
ระบบและรีจิสทรี
ระบบจะแจกแจงส่วนประกอบทุกประเภท
defmodule TimeSystem do
@moduledoc """
Increments ages of TimeComponents
"""
def process do
components()
|> Enum.each(fn (pid) -> dispatch(pid, :increment) end)
end
# dispatch() is a pure reducer that takes in a state and an action and returns a new state
defp dispatch(pid, action) do
%{id: _pid, state: state} = ECS.Component.get(pid)
new_state = case action do
:increment ->
Map.put(state, :age, state.age + 1)
:decrement ->
Map.put(state, :age, state.age - 1)
_ ->
state
end
IO.puts("Updated #{inspect pid} to #{inspect new_state}")
ECS.Component.update(pid, new_state)
end
defp components do
ECS.Registry.get(:"Elixir.TimeComponent")
end
end
บางสิ่งที่ควรทราบ:
dispatch
ดำเนินการภายนอก ประเมินตามกฎภายใน และส่งคืนสถานะใหม่ ส่วนนี้ได้รับแรงบันดาลใจส่วนใหญ่จากประสบการณ์ของฉันกับตัวลด Redux และ Elm'supdate
- เมธอด
components
ส่งคืนชุดส่วนประกอบที่ระบบนี้จะระบุ เมื่อใดก็ตามที่ส่วนประกอบถูกสร้างอินสแตนซ์ ส่วนประกอบจะลงทะเบียนเอเจนต์ไปที่Registry
ซึ่งจะติดตามส่วนประกอบที่ใช้งานอยู่ทั้งหมด Registry เองก็เป็นนักแสดงดังที่แสดงด้านล่าง:
defmodule ECS.Registry do
@moduledoc """
Component registry.
iex> {:ok, r} = ECS.Registry.start
iex> ECS.Registry.insert("test", r)
:ok
iex> ECS.Registry.get("test")
[#PID<0.87.0>]
"""
def start do
Agent.start_link(fn -> %{} end, name: __MODULE__)
end
def insert(component_type, component_pid) do
Agent.update(__MODULE__, fn(registry) ->
components = (Map.get(registry, component_type, []) ++ [component_pid])
Map.put(registry, component_type, components)
end)
end
def get(component_type) do
Agent.get(__MODULE__, fn(registry) ->
Map.get(registry, component_type, [])
end)
end
end
และนั่นมัน!
การนำ ECS ไปใช้นี้อาจมีความหยาบเล็กน้อยในส่วน Edge ECS มีหลากหลายรูปแบบ และนี่ไม่ใช่วิธีเดียวที่จะทำให้ ECS ทำงานได้อย่างแน่นอน ยินดีรับฟังความคิดเห็นของคุณ!
ในการปิด
ECS เป็นรูปแบบสถาปัตยกรรมที่ถูกมองข้าม ซึ่งเอาชนะข้อบกพร่องบางประการของการสืบทอดแบบ OOP และเหมาะอย่างยิ่งสำหรับระบบแบบกระจาย
การแตกแขนงออกไปในโดเมนที่ไม่คุ้นเคย (เช่น การพัฒนาเกม) เป็นแหล่งความคิดและรูปแบบใหม่ๆ ที่มีผลสำเร็จในการเขียนซอฟต์แวร์ที่ดีขึ้น
ขอบคุณที่อ่าน! ฉันหวังว่าคุณจะพบว่าบทความนี้มีประโยชน์หรือน่าสนใจ แจ้งให้เราทราบความคิดของคุณผ่านความคิดเห็นด้านล่าง!
เผยแพร่ครั้งแรกที่ yos.io เมื่อวันที่ 17 กันยายน 2016