การจัดระเบียบระบบเอนทิตีด้วยตัวจัดการคอมโพเนนต์ภายนอก


13

ฉันออกแบบเอ็นจิ้นเกมสำหรับเกมยิง 2D แบบผู้เล่นหลายคนจากบนลงล่างซึ่งฉันต้องการใช้ซ้ำได้อย่างมีเหตุผลสำหรับเกมยิงจากบนลงล่างอื่น ๆ ในขณะนี้ฉันกำลังคิดว่าควรออกแบบระบบเอนทิตี้ของมันอย่างไร ครั้งแรกที่ฉันคิดเกี่ยวกับเรื่องนี้:

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

EntityManager เป็นเจ้าของรายการของวัตถุประเภท BaseEntity แต่ละเอนทิตีเป็นเจ้าของรายการของส่วนประกอบเช่น EntityModel (การนำเสนอที่เป็นไปได้ของเอนทิตี), EntityNetworkInterface และ EntityPhysicalBody

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

การสร้างเอนทิตีทำเช่นนี้: EntityManager ใช้เมธอด RegisterEntity (entityClass, factory) ซึ่งลงทะเบียนวิธีสร้างเอนทิตีของคลาสนั้น นอกจากนี้ยังใช้เมธอด CreateEntity (entityClass) ซึ่งจะส่งคืนออบเจ็กต์ประเภท BaseEntity

มาถึงปัญหาของฉันแล้ว: การอ้างอิงไปยังส่วนประกอบจะลงทะเบียนกับตัวจัดการส่วนประกอบอย่างไร ฉันไม่รู้ว่าฉันจะอ้างอิงผู้จัดการส่วนประกอบจากโรงงาน / ปิดได้อย่างไร


ฉันไม่รู้ว่านี่อาจหมายถึงระบบไฮบริดหรือไม่ แต่ดูเหมือนว่า "ผู้จัดการ" ของคุณเป็นสิ่งที่ฉันมักจะได้ยินว่าเรียกว่า "ระบบ" เช่นเอนทิตีคือ abstract-ID Component คือกลุ่มของข้อมูล และสิ่งที่คุณเรียกว่า "ผู้จัดการ" คือสิ่งที่เรียกว่า "ระบบ" โดยทั่วไป ฉันตีความคำศัพท์ถูกต้องหรือไม่
BRPocock


ดูที่gamadu.com/artemisและดูว่าวิธีการของพวกเขาตอบคำถามของคุณหรือไม่
Patrick Hughes

1
ไม่มีวิธีใดวิธีหนึ่งในการออกแบบระบบเอนทิตีเนื่องจากมีฉันทามติน้อยกว่าคำจำกัดความของมัน สิ่งที่ @ BRPocock อธิบายและสิ่งที่อาร์ทิมิสใช้ให้ได้รับการอธิบายเพิ่มเติมในเชิงลึกในบล็อกนี้: t-machine.org/index.php/category/entity-systemsพร้อมกับ wiki: entity-systems.wikidot.com
8363

คำตอบ:


6

ระบบควรเก็บคู่ค่าคีย์ของ Entity to Component ไว้ใน Map, Dictionary Object หรือ Associative Array (ขึ้นอยู่กับภาษาที่ใช้) นอกจากนี้เมื่อคุณสร้างเอนทิตีวัตถุของฉันฉันจะไม่กังวลเกี่ยวกับการจัดเก็บในผู้จัดการเว้นแต่คุณจะต้องสามารถถอนการลงทะเบียนจากระบบใด ๆ เอนทิตีเป็นส่วนประกอบของส่วนประกอบ แต่ไม่ควรจัดการกับการอัพเดทองค์ประกอบใด ๆ ที่ควรจัดการโดยระบบ ให้ถือว่าเอนทิตีของคุณเป็นคีย์ที่แมปกับส่วนประกอบทั้งหมดที่มีอยู่ในระบบรวมทั้งเป็นศูนย์กลางการสื่อสารสำหรับส่วนประกอบเหล่านั้นเพื่อพูดคุยกัน

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

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

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

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

ในระยะสั้น:

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

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

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

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

หวังว่านี่จะช่วยได้ มันเป็นนรกของรูปแบบการออกแบบ แต่มันมีพลังอย่างน่าขันถ้าทำถูกต้อง


0

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

class Component
{
    private uint ID;
    // etc...
}

class Entity
{
    List<Component> Components;
    // etc...
    public bool Contains(Type type)
    {
        foreach(Component comp in Components)
        {
            if(typeof(comp) == type)
                return true;
        }
        return false;
    }
}

class EntityManager
{
    List<Entity> Entities;
    // etc...
    public List<Entity> GetEntitiesOfType(Type type)
    {
        List<Entity> results = new List<Entity>();
        foreach(Entity entity in Entities)
        {
            if(entity.Contains(type))
                results.Add(entity);
        }
        return results;
    }
}

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


3
Caveat emptor: ณ จุดที่เอนทิตีของคุณมีข้อมูลมันเป็นอ็อบเจกต์ไม่ใช่เอนทิตี้…หนึ่งสูญเสียประโยชน์ส่วนใหญ่ของการปรับเทียบ (sic?) ของ ECS ในโครงสร้างนี้ "บริสุทธิ์" ระบบ E / C / S นั้นเป็นแบบเชิงสัมพันธ์ไม่ใช่แบบเชิงวัตถุ…ไม่ใช่ว่ามันจำเป็นต้อง "เลวร้าย" สำหรับบางกรณี แต่แน่นอนว่า "ทำลายโมเดลเชิงสัมพันธ์"
BRPocock

2
ฉันไม่แน่ใจว่าฉันเข้าใจคุณ ความเข้าใจของฉัน (และโปรดแก้ไขให้ถูกต้องหากฉันทำผิด) คือ Entity-Component-System พื้นฐานมีคลาสเอนทิตีซึ่งมีส่วนประกอบและอาจมี ID ชื่อหรือตัวระบุบางอย่าง ฉันคิดว่าเราอาจมีความเข้าใจผิดในสิ่งที่ฉันหมายถึงโดย "ประเภท" ของนิติบุคคล เมื่อฉันพูดถึงเอนทิตี้ "ประเภท" ฉันหมายถึงประเภทของคอมโพเนนต์ กล่าวคือ Entity เป็นประเภท "Sprite" หากมีส่วนประกอบ Sprite
Mike Cluck

1
ในระบบเอนทิตี / ส่วนประกอบที่บริสุทธิ์เอนทิตีมักจะเป็นอะตอม: เช่นtypedef long long int Entity; Component คือเร็กคอร์ด (อาจถูกนำไปใช้เป็นคลาสอ็อบเจ็กต์หรือเพียง a struct) ที่มีการอ้างอิงไปยังเอนทิตีที่ถูกแนบ และระบบจะเป็นวิธีการหรือการรวบรวมวิธีการ แบบจำลอง ECS นั้นเข้ากันไม่ได้กับแบบจำลอง OOP แม้ว่าชิ้นส่วนจะเป็นวัตถุ (เฉพาะส่วนใหญ่) ข้อมูลเท่านั้นและระบบเป็นวัตถุแบบซิงเกิลโค้ดที่มีรหัสเท่านั้น พบบ่อยกว่า "บริสุทธิ์" พวกเขาสูญเสียผลประโยชน์โดยธรรมชาติมากมาย
BRPocock

2
@BRPocock ระบบเอนทิตี "บริสุทธิ์" อีกครั้ง ฉันคิดว่าเอนทิตีในฐานะที่เป็นวัตถุนั้นดีมากมันไม่จำเป็นต้องเป็นไอดีธรรมดา สิ่งหนึ่งคือการนำเสนอแบบอนุกรมอีกรูปแบบหนึ่งในหน่วยความจำของวัตถุ / แนวคิด / เอนทิตี ตราบใดที่คุณสามารถบำรุงรักษาข้อมูลได้คุณไม่ควรเชื่อมโยงกับรหัสที่ไม่ใช้สำนวนเพราะเป็นวิธีที่ "บริสุทธิ์"
เรน

1
@ BRPocock นี่เป็นคำเตือนที่ยุติธรรม แต่สำหรับระบบเอนทิตีเหมือน "t-machine" ฉันเข้าใจว่าทำไม แต่นั่นไม่ใช่วิธีเดียวในการสร้างแบบจำลองเอนทิตีที่อิงองค์ประกอบ นักแสดงเป็นทางเลือกที่น่าสนใจ ฉันมักจะชื่นชมพวกเขามากขึ้น
Raine

0

1) วิธีโรงงานของคุณควรผ่านการอ้างอิงไปยัง EntityManager ที่เรียกว่า (ฉันจะใช้ C # เป็นตัวอย่าง):

delegate BaseEntity EntityFactory(EntityManager manager);

2) ให้ CreateEntity รับ ID (เช่นสตริงจำนวนเต็มขึ้นอยู่กับคุณ) นอกเหนือจากคลาส / ประเภทของเอนทิตีและลงทะเบียนเอนทิตีที่สร้างบนพจนานุกรมโดยใช้รหัสนั้นโดยอัตโนมัติ:

class EntityManager
{
    // Rest of class omitted

    BaseEntity CreateEntity(string id, Type entityClass)
    {
        BaseEntity entity = factories[entityClass](this);
        registry.Add(id, entity);
        return entity;
    }

    Dictionary<Id, BaseEntity> registry;
}

3) เพิ่ม Getter ให้กับ EntityManager เพื่อรับเอนทิตี้ใด ๆ ตาม ID:

class EntityManager
{
    // Rest of class omitted

    BaseEntity GetEntity(string id)
    {
        return registry[id];
    }
}

และนั่นคือทั้งหมดที่คุณต้องอ้างอิง ComponentManager ใด ๆ จากภายในวิธีการโรงงานของคุณ ตัวอย่างเช่น

BaseEntity CreateSomeSortOfEntity(EntityManager manager)
{
    // Create and configure entity
    BaseEntity entity = new BaseEntity();
    RenderComponent renderComponent = new RenderComponent();
    entity.AddComponent(renderComponent);

    // Get a reference to the render manager and register component
    RenderEntityManager renderer = manager.GetEntity("RenderEntityManager") as RenderEntityManager;
    if(renderer != null)
        renderer.Register(renderComponent)

    return entity;
}

นอกจาก Id คุณยังสามารถใช้คุณสมบัติ Type บางประเภท (enum แบบกำหนดเองหรือพึ่งระบบชนิดของภาษา) และสร้าง getter ที่ส่งกลับ BaseEntities ทั้งหมดของประเภทที่แน่นอน


1
เพื่อไม่ให้เชื่องช้า แต่อีกครั้ง ... ในระบบ Entity (relational) ที่บริสุทธิ์เอนทิตี้ไม่มีประเภทยกเว้นสิ่งเหล่านั้นที่ถูกส่งมอบให้พวกเขาโดยอาศัยส่วนประกอบของพวกเขา ...
BRPocock

@BRPocock: คุณสามารถสร้างตัวอย่างที่ตามหลังคุณธรรมบริสุทธิ์ได้หรือไม่?
Zolomon

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

2
ในการอ้างอิง: ระบบเอนทิตี“ บริสุทธิ์”: รหัสเอนทิตีมักจะมีลักษณะดังนี้: typedef unsigned long long int EntityID;; อุดมคติคือแต่ละระบบสามารถอยู่บน CPU หรือโฮสต์แยกต่างหากและต้องการเรียกส่วนประกอบที่เกี่ยวข้องกับ / ใช้งานในระบบนั้นเท่านั้น ด้วยวัตถุ Entity หนึ่งอาจต้องยกตัวอย่างวัตถุ Entity เดียวกันในแต่ละโฮสต์ทำให้การปรับขนาดยากขึ้น โมเดลเอนทิตีคอมโพเนนต์ - ระบบบริสุทธิ์จะแยกการประมวลผลระหว่างโหนด (กระบวนการ, ซีพียูหรือโฮสต์) ตามระบบแทนที่จะเป็นโดยเอนทิตี
BRPocock

1
@DavidGouveia พูดถึง“ การเพิ่มประสิทธิภาพ…ค้นหาเอนทิตีตาม ID” ในความเป็นจริงระบบ (ไม่กี่) ที่ฉันได้ใช้งานด้วยวิธีนี้มักจะไม่ทำเช่นนั้น บ่อยกว่านั้นให้เลือกส่วนประกอบตามรูปแบบที่ระบุว่าพวกเขาสนใจระบบใดระบบหนึ่งโดยใช้เอนทิตี (ID) สำหรับการรวมข้ามองค์ประกอบ
BRPocock
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.