ฉันจะเข้าถึงส่วนประกอบใน C ++ Entity-Component-Systems ของฉันได้อย่างไร


18

(สิ่งที่ฉันอธิบายจะขึ้นอยู่กับการออกแบบนี้: กรอบงานระบบเอนทิตี้คืออะไรเลื่อนลงมาแล้วคุณจะพบมัน)

ฉันมีปัญหาในการสร้างระบบเอนทิตีส่วนประกอบใน C ++ ฉันมีชั้นองค์ประกอบของฉัน:

class Component { /* ... */ };

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

class SampleComponent : public Component { int foo, float bar ... };

ส่วนประกอบเหล่านี้จะถูกเก็บไว้ในคลาส Entity ซึ่งให้ ID ที่เป็นเอกลักษณ์แต่ละ Entity:

class Entity {
     int ID;
     std::unordered_map<string, Component*> components;
     string getName();
     /* ... */
};

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

ในขณะเดียวกันฉันมี System interface ซึ่งใช้ Node interface อยู่ภายใน คลาส Node ใช้เพื่อจัดเก็บส่วนประกอบของเอนทิตีเดียว (เนื่องจากระบบไม่สนใจใช้ส่วนประกอบทั้งหมดของเอนทิตี) เมื่อระบบมีการupdate()มันจะต้องทำซ้ำผ่านโหนดที่เก็บไว้สร้างขึ้นจากหน่วยงานที่แตกต่างกัน ดังนั้น:

/* System and Node implementations: (not the interfaces!) */

class SampleSystem : public System {
        std::list<SampleNode> nodes; //uses SampleNode, not Node
        void update();
        /* ... */
};

class SampleNode : public Node {
        /* Here I define which components SampleNode (and SampleSystem) "needs" */
        SampleComponent* sc;
        PhysicsComponent* pc;
        /* ... more components could go here */
};

ตอนนี้ปัญหา: สมมติว่าฉันสร้าง SampleNodes โดยส่งเอนทิตีไปยัง SampleSystem SampleNode จะ "ตรวจสอบ" ถ้าเอนทิตีมีส่วนประกอบที่จำเป็นที่จะใช้โดย SampleSystem ปัญหาปรากฏขึ้นเมื่อฉันต้องการเข้าถึงส่วนประกอบที่ต้องการภายใน Entity: ส่วนประกอบถูกเก็บไว้ในComponentคอลเลกชัน (คลาสพื้นฐาน) ดังนั้นฉันจึงไม่สามารถเข้าถึงส่วนประกอบและคัดลอกไปยังโหนดใหม่ ฉันแก้ไขปัญหาชั่วคราวด้วยการชี้Componentลงไปที่ประเภทที่ได้รับมา แต่ฉันต้องการทราบว่ามีวิธีที่ดีกว่าในการทำสิ่งนี้หรือไม่ ฉันเข้าใจว่านี่จะหมายถึงการออกแบบสิ่งที่ฉันมีอยู่แล้วอีกครั้ง ขอบคุณ

คำตอบ:


23

หากคุณจะเก็บComponents ไว้ในกลุ่มทั้งหมดคุณจะต้องใช้คลาสพื้นฐานทั่วไปเป็นประเภทที่เก็บไว้ในคอลเลกชันดังนั้นคุณต้องส่งไปยังประเภทที่ถูกต้องเมื่อคุณพยายามเข้าถึงComponents ในคอลเลกชัน ปัญหาของการพยายามส่งไปยังคลาสที่ไม่ถูกต้องสามารถถูกกำจัดได้ด้วยการใช้แม่แบบและtypeidฟังก์ชันอย่างชาญฉลาดอย่างไรก็ตาม:

ด้วยแผนที่ประกาศเช่น:

std::unordered_map<const std::type_info* , Component *> components;

ฟังก์ชั่น addComponent เช่น:

components[&typeid(*component)] = component;

และ getComponent:

template <typename T>
T* getComponent()
{
    if(components.count(&typeid(T)) != 0)
    {
        return static_cast<T*>(components[&typeid(T)]);
    }
    else 
    {
        return NullComponent;
    }
}

คุณจะไม่ได้รับการเยาะเย้ย นี่เป็นเพราะtypeidจะส่งกลับตัวชี้ไปยังข้อมูลประเภทของประเภทรันไทม์ (ประเภทที่ได้รับมากที่สุด) ของส่วนประกอบ เนื่องจากส่วนประกอบถูกเก็บไว้กับข้อมูลประเภทนั้นเนื่องจากเป็นคีย์การส่งอาจไม่สามารถทำให้เกิดปัญหาได้เนื่องจากประเภทไม่ตรงกัน นอกจากนี้คุณยังจะได้รับการตรวจสอบประเภทรวบรวมเวลาอยู่กับชนิดแม่แบบในขณะที่มันจะต้องมีชนิดที่มาจากตัวแทนหรือประเภทจะได้ไม่ตรงกันstatic_cast<T*>unordered_map

คุณไม่จำเป็นต้องเก็บส่วนประกอบประเภทต่าง ๆ ไว้ในคอลเล็กชันทั่วไป หากคุณละทิ้งความคิดของs Entityที่มีComponentและแทนที่จะมีแต่ละComponentร้านEntity(ในความเป็นจริงมันอาจจะเป็นเพียงจำนวนเต็ม ID) จากนั้นคุณสามารถจัดเก็บแต่ละประเภทส่วนประกอบที่ได้รับมาในคอลเลกชันของตัวเองของประเภทที่ได้รับแทน ประเภทฐานสามัญและค้นหาComponents "เป็นของ" an Entityถึง ID นั้น

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


3
ฉันใช้ C ++ มาเกือบ 6 ปีแล้ว แต่ทุกสัปดาห์ฉันได้เรียนรู้เคล็ดลับใหม่ ๆ
knight666

ขอบคุณสำหรับคำตอบ. ฉันจะลองใช้วิธีแรกก่อนและถ้าฉันอาจภายหลังฉันจะคิดถึงวิธีใช้อีกวิธีหนึ่ง แต่addComponent()วิธีการนั้นไม่จำเป็นต้องเป็นวิธีแม่แบบด้วยหรือ หากฉันกำหนดaddComponent(Component* c)องค์ประกอบย่อยใด ๆ ที่ฉันเพิ่มจะถูกเก็บไว้ในComponentตัวชี้และtypeidจะอ้างถึงComponentคลาสฐานเสมอ
Federico

2
Typeid จะให้ประเภทวัตถุจริงที่ชี้ไปถึงคุณแม้ว่าตัวชี้จะเป็นคลาสพื้นฐานก็ตาม
Chewy Gumball

ฉันชอบคำตอบของเคี้ยวดังนั้นฉันจึงลองใช้มันใน mingw32 ฉันพบปัญหาที่เอ่ยถึงโดย fede rico ที่ addComponent () เก็บทุกอย่างเป็นองค์ประกอบเพราะ typeid ส่งคืนคอมโพเนนต์เป็นประเภทสำหรับทุกสิ่ง ใครบางคนในที่นี้กล่าวว่า typeid ควรให้ประเภทวัตถุจริงที่ชี้ไปแม้ว่าตัวชี้จะเป็นคลาสพื้นฐาน แต่ฉันคิดว่ามันอาจแตกต่างกันไปตามคอมไพเลอร์และอื่น ๆ ใครสามารถยืนยันสิ่งนี้ได้หรือไม่ ฉันใช้ g ++ std = c ++ 11 mingw32 บน windows 7 ฉันลงเอยด้วยการปรับเปลี่ยน getComponent () เป็นเทมเพลตจากนั้นบันทึกประเภทจากนั้นเป็น th
shwoseph

นี่ไม่ใช่คอมไพเลอร์เฉพาะ คุณอาจไม่มีนิพจน์ที่ถูกต้องเป็นอาร์กิวเมนต์ของฟังก์ชัน typeid
Chewy Gumball

17

ชิววี่ถูกต้องแล้ว แต่ถ้าคุณใช้ C ++ 11 คุณมีประเภทใหม่ที่คุณสามารถใช้ได้

แทนการใช้const std::type_info*เป็นกุญแจสำคัญในแผนที่ของคุณคุณสามารถใช้std::type_index( ดู cppreference.com ) std::type_infoซึ่งเป็นเสื้อคลุมรอบ ทำไมคุณจะใช้มัน? std::type_indexจริงเก็บความสัมพันธ์กับstd::type_infoเป็นตัวชี้ แต่ที่เป็นหนึ่งในตัวชี้น้อยสำหรับคุณที่จะต้องกังวลเกี่ยวกับ

หากคุณใช้ C ++ 11 ฉันขอแนะนำให้เก็บข้อมูลComponentอ้างอิงไว้ในตัวชี้สมาร์ท ดังนั้นแผนที่อาจเป็นดังนี้:

std::map<std::type_index, std::shared_ptr<Component> > components

การเพิ่มรายการใหม่สามารถทำได้ดังนี้:

components[std::type_index(typeid(*component))] = component

ที่เป็นประเภทcomponent std::shared_ptr<Component>การดึงข้อมูลอ้างอิงไปยังประเภทที่ระบุComponentอาจมีลักษณะดังนี้:

template <typename T>
std::shared_ptr<T> getComponent()
{
    std::type_index index(typeid(T));
    if(components.count(std::type_index(typeid(T)) != 0)
    {
        return static_pointer_cast<T>(components[index]);
    }
    else
    {
        return NullComponent
    }
}

หมายเหตุยังใช้แทนstatic_pointer_caststatic_cast


1
จริงๆแล้วฉันใช้วิธีนี้ในโครงการของฉันเอง
vijoc

ที่จริงแล้วค่อนข้างสะดวกเพราะฉันได้เรียนรู้ C ++ โดยใช้มาตรฐาน C ++ 11 เป็นข้อมูลอ้างอิง สิ่งหนึ่งที่ผมได้สังเกตเห็น castแต่เป็นว่าทุกระบบนิติบุคคลองค์ประกอบที่ฉันได้พบในเว็บใช้การจัดเรียงของบางอย่าง ฉันเริ่มคิดว่ามันเป็นไปไม่ได้ที่จะนำสิ่งนี้ไปใช้หรือการออกแบบระบบที่คล้ายกันโดยไม่มีการปลดเปลื้อง
Federico

@Fede การจัดเก็บพComponentอยน์เตอร์ในภาชนะเดียวไม่จำเป็นต้องมีการชี้ลงไปตามประเภทที่ได้รับมา แต่อย่าง Chewy ชี้ให้เห็นว่าคุณมีทางเลือกอื่นให้คุณใช้ซึ่งไม่จำเป็นต้องมีการคัดเลือกนักแสดง ตัวฉันเองไม่เห็นอะไรเลย "เลวร้าย" ในการมีนักแสดงประเภทนี้ในการออกแบบเนื่องจากพวกมันค่อนข้างปลอดภัย
vijoc

@vijoc บางครั้งพวกเขาคิดว่าไม่ดีเนื่องจากปัญหาการเชื่อมโยงหน่วยความจำที่พวกเขาอาจแนะนำ
akaltar
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.