ระบบและการแสดงผล


11

Okey สิ่งที่ฉันรู้จนถึงตอนนี้; เอนทิตีประกอบด้วยส่วนประกอบ (data-storage) ซึ่งเก็บข้อมูลเช่น; - พื้นผิว / สไปรต์ - Shader - ฯลฯ

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

เพียงแค่ต้องป้อนข้อมูลบางอย่างใน "วิธีที่ถูกต้อง" เพื่อทำสิ่งนี้ เคล็ดลับและข้อผิดพลาดที่ต้องระวัง


2
พยายามอย่าทำสิ่งที่ธรรมดาเกินไป ดูเหมือนจะแปลกที่มีเอนทิตีที่มีองค์ประกอบ Shader และไม่ใช่องค์ประกอบ Sprite ดังนั้นบางที Shader ควรเป็นส่วนหนึ่งขององค์ประกอบ Sprite โดยปกติคุณจะต้องใช้ระบบการแสดงผลเพียงระบบเดียวเท่านั้น
Jonathan Connell

คำตอบ:


8

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

เอกลักษณ์

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

public abstract class Entity {
    public bool IsAlive = true;
    public virtual SpatialComponent   Spatial   { get; set; }
    public virtual ImageComponent     Image     { get; set; }
    public virtual AnimationComponent Animation { get; set; }
    public virtual InputComponent     Input     { get; set; }
}

ส่วนประกอบ

ส่วนประกอบคือ "โง่" โดยที่พวกเขาไม่ได้ทำหรือไม่รู้อะไรเลย พวกเขาไม่มีการอ้างอิงถึงส่วนประกอบอื่น ๆ และโดยทั่วไปแล้วพวกเขาไม่มีฟังก์ชั่น (ฉันทำงานใน C # ดังนั้นฉันจึงใช้คุณสมบัติเพื่อจัดการ getters / setters - ถ้าพวกมันมีฟังก์ชั่น

ระบบ

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

อินเตอร์เฟซ

การเชื่อมต่อเป็นโครงสร้างสำคัญในระบบของฉัน พวกเขาจะใช้เพื่อกำหนดสิ่งที่Systemกระบวนการสามารถและสิ่งที่มีEntityความสามารถ อินเตอร์เฟซที่เกี่ยวข้องกับการแสดงผลมีดังนี้: และIRenderableIAnimatable

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

public interface IRenderable {
    SpatialComponent Component { get; }
    ImageComponent   Image     { get; }
}

RenderingSystem

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

public class RenderSystem {
    private SpriteBatch batch;
    public RenderSystem(SpriteBatch batch) {
        this.batch = batch;
    }
    public void Draw(List<IRenderable> list) {
        foreach(IRenderable obj in list) {
            this.batch.draw(
                obj.Image.Texture,
                obj.Spatial.Position,
                obj.Image.Source,
                Color.White);
        }
    }
}

มองไปที่ระดับที่ทำให้ระบบไม่ได้รู้ว่าสิ่งที่Entityเป็น สิ่งที่รู้เกี่ยวกับIRenderableมันและมันก็แค่ให้รายชื่อพวกมันวาด

มันทำงานอย่างไร

มันอาจช่วยให้เข้าใจว่าฉันสร้างวัตถุเกมใหม่และวิธีป้อนข้อมูลเหล่านั้นไปยังระบบได้อย่างไร

การสร้างเอนทิตี

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

public class MyAnimatedWidget : Entity, IRenderable, IAnimatable {}

การให้อาหารระบบ

List<Entity> gameObjectsฉันเก็บรายชื่อของทุกหน่วยงานที่มีอยู่ในโลกของเกมในรายการเดียวที่เรียกว่า แต่ละเฟรมจากนั้นผมก็ลอดผ่านรายการนั้นและคัดลอกอ้างอิงวัตถุไปยังรายการอื่น ๆ ขึ้นอยู่กับชนิดของอินเตอร์เฟซเช่นและList<IRenderable> renderableObjects List<IAnimatable> animatableObjectsด้วยวิธีนี้หากระบบที่แตกต่างกันจำเป็นต้องประมวลผลเอนทิตีเดียวกันพวกเขาสามารถทำได้ จากนั้นฉันก็แค่ส่งรายการเหล่านั้นไปยังแต่ละระบบUpdateหรือDrawวิธีการแล้วปล่อยให้ระบบทำงานได้

นิเมชั่น

คุณอาจสงสัยว่าระบบภาพเคลื่อนไหวทำงานอย่างไร ในกรณีของฉันคุณอาจต้องการดูอินเทอร์เฟซ IAnimatable:

public interface IAnimatable {
    public AnimationComponent Animation { get; }
    public ImageComponent Image         { get; set; }
}

สิ่งสำคัญที่ควรสังเกตที่นี่คือImageComponentลักษณะของIAnimatableส่วนต่อประสานนั้นไม่ได้เป็นแบบอ่านอย่างเดียว ก็มีหมา

ตามที่คุณอาจเดาเอาไว้ส่วนประกอบของแอนิเมชันจะเก็บข้อมูลเกี่ยวกับแอนิเมชัน รายการของเฟรม (ซึ่งเป็นส่วนประกอบของภาพ), เฟรมปัจจุบัน, จำนวนเฟรมต่อวินาทีที่จะวาด, เวลาที่ผ่านไปตั้งแต่การเพิ่มเฟรมล่าสุดและตัวเลือกอื่น ๆ

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


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

! ที่น่าสนใจ ฉันไม่กระตือรือร้นที่จะเรียนนามธรรมของ Entity แต่อินเทอร์เฟซ IRenderable เป็นความคิดที่ดี!
Jonathan Connell

5

ดูคำตอบนี้เพื่อดูระบบที่ฉันกำลังพูดถึง

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


3

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

เพื่อที่จะตอบคำถามของคุณคุณต้องดูสถาปัตยกรรมของคุณและถามตัวเองด้วยคำถามสองข้อ:

  1. มันสมเหตุสมผลหรือไม่ที่จะมี Shader ที่ไม่มีพื้นผิว
  2. การแยก Shader ออกจากพื้นผิวจะอนุญาตให้ฉันหลีกเลี่ยงการทำซ้ำรหัสหรือไม่

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

ตัวอย่างเช่นทั้ง Physics และ Audio อาจใช้ตำแหน่งเดียวกันจากนั้นทั้งสององค์ประกอบจะเก็บตำแหน่งที่ซ้ำกันที่คุณ refactor ไว้ในหนึ่ง PositionComponent และต้องการให้เอนทิตีที่ใช้ PhysicsComponent / AudioComponent นั้นมี PositionComponent ด้วย

จากข้อมูลที่คุณให้กับเราดูเหมือนว่า RenderComponent ของคุณจะเป็นตัวเลือกที่ดีสำหรับการแยกเป็น TextureComponent และ ShaderComponent เนื่องจาก Shader นั้นขึ้นอยู่กับ Texture และไม่มีอะไรอื่น

สมมติว่าคุณกำลังใช้สิ่งที่คล้ายกับT-Machine: Entity Systemsตัวอย่างการนำไปใช้ของ RenderComponent & RenderSystem ใน C ++ จะมีลักษณะดังนี้:

struct RenderComponent {
    Texture* textureData;
    Shader* shaderData;
};

class RenderSystem {
    public:
        RenderSystem(EntityManager& manager) :
            m_manager(manager) {
            // Initialize Window, rendering context, etc...
        }

        void update() {
            // Get all the entities with RenderComponent
            std::vector<RenderComponent>& components = m_manager.getComponents<RenderComponent>();

            for(auto component = components.begin(); entity != components.end(); ++components) {
                // Do something with the texture
                doSomethingWithTexture(component->textureData);

                // Do something with the shader if it's not null
                if(component->shaderData != nullptr) {
                    doSomethingWithShader(component->shaderData);
                }
            }
        }
    private:
        EntityManager& m_manager;
}

มันผิดอย่างสมบูรณ์ จุดทั้งหมดของส่วนประกอบคือการแยกพวกมันออกจากเอนทิตีไม่ใช่ทำให้ระบบเรนเดอร์ค้นหาผ่านเอนทิตีเพื่อค้นหาพวกมัน ระบบการแสดงผลควรควบคุมข้อมูลของตัวเองอย่างเต็มที่ PS อย่าใส่ std :: vector (โดยเฉพาะกับข้อมูลอินสแตนซ์) ในลูปนั่นคือ C ++ ที่แย่มาก
snake5

@ snake5 คุณถูกต้องทั้งคู่ ฉันพิมพ์รหัสที่ด้านบนของหัวของฉันและมีปัญหาบางอย่างขอบคุณที่ชี้ให้พวกเขา ฉันได้แก้ไขรหัสที่ได้รับผลกระทบให้ช้าลงและใช้สำนวนระบบนิติบุคคลอย่างถูกต้อง
Jake Woods

2
@ snake5 คุณไม่ได้คำนวณข้อมูลใหม่ทุกเฟรม getComponents จะส่งคืนเวกเตอร์ที่ m_manager เป็นเจ้าของซึ่งเป็นที่รู้จักแล้วและจะเปลี่ยนเฉพาะเมื่อคุณเพิ่ม / ลบส่วนประกอบ มันเป็นข้อได้เปรียบเมื่อคุณมีระบบที่ต้องการใช้หลายองค์ประกอบของเอนทิตีเดียวกันตัวอย่างเช่น PhysicsSystem ที่ต้องการใช้ PositionComponent และ PhysicsComponent ระบบอื่น ๆ อาจต้องการตำแหน่งและการมี PositionComponent ทำให้คุณไม่มีข้อมูลซ้ำซ้อน ส่วนใหญ่จะแก้ปัญหาว่าส่วนประกอบสื่อสารอย่างไร
Jake Woods

5
@ snake5 คำถามไม่ได้เกี่ยวกับการวางระบบ EC หรือประสิทธิภาพ คำถามเกี่ยวกับการตั้งค่าระบบการเรนเดอร์ มีหลายวิธีในการจัดโครงสร้างระบบ EC ไม่ติดปัญหาเรื่องประสิทธิภาพของอีกระบบหนึ่งที่นี่ OP น่าจะใช้โครงสร้าง EC ที่แตกต่างจากคำตอบของคุณ รหัสที่ให้ไว้ในคำตอบนี้มีไว้เพื่อแสดงตัวอย่างที่ดีกว่าเพื่อไม่ให้ถูกวิจารณ์สำหรับประสิทธิภาพการทำงาน หากคำถามเกี่ยวกับประสิทธิภาพมากกว่าอาจจะทำให้คำตอบ "ไม่มีประโยชน์" แต่ไม่ใช่
MichaelHouse

2
ฉันชอบการออกแบบที่วางไว้ในคำตอบนี้มากกว่าใน Cyphers มันคล้ายกับที่ฉันใช้ ส่วนประกอบที่เล็กกว่าเป็น IMO ที่ดีกว่าแม้ว่าจะมีเพียงหนึ่งหรือสองตัวแปร พวกเขาควรกำหนดแง่มุมของเอนทิตีเช่นองค์ประกอบ "Damagable" ของฉันจะมี 2, อาจจะ 4 ตัวแปร (สูงสุดและปัจจุบันสำหรับแต่ละสุขภาพและเกราะ) ความคิดเห็นเหล่านี้ใช้เวลานานลองย้ายไปสนทนาถ้าคุณต้องการพูดคุยเพิ่มเติม
John McDonald

2

ผิดพลาด # 1: รหัสออกแบบเกิน ลองคิดดูว่าคุณต้องการทุกสิ่งที่คุณใช้จริงหรือไม่เพราะคุณจะต้องใช้ชีวิตอยู่กับมันสักพัก

ผิดพลาด # 2: วัตถุมากเกินไป ฉันจะไม่ใช้ระบบที่มีวัตถุมากเกินไป (อย่างใดอย่างหนึ่งสำหรับแต่ละประเภทย่อยและอะไรก็ตาม) เพราะมันทำให้การประมวลผลอัตโนมัติยากขึ้น ในความคิดของฉันมันดีกว่าที่จะให้แต่ละออบเจ็กต์ควบคุมชุดคุณลักษณะบางอย่าง (ตรงข้ามกับคุณลักษณะหนึ่ง) ตัวอย่างเช่นการสร้างคอมโพเนนต์สำหรับแต่ละบิตของข้อมูลที่รวมอยู่ในการแสดงผล (องค์ประกอบพื้นผิวส่วนประกอบของ shader) ถูกแบ่งออกมากเกินไป - โดยปกติคุณจะต้องมีส่วนประกอบเหล่านั้นทั้งหมดด้วยกันอย่างไรก็ตามคุณจะไม่เห็นด้วยไหม

ผิดพลาด # 3: การควบคุมภายนอกที่เข้มงวดเกินไป ต้องการเปลี่ยนชื่อเป็นวัตถุ shader / texture เนื่องจากวัตถุสามารถเปลี่ยนแปลงได้ด้วยโหมด renderer / texture type / shader format / อะไรก็ตาม ชื่อเป็นตัวระบุอย่างง่าย - มันขึ้นอยู่กับตัวแสดงผลในการตัดสินใจว่าจะทำอะไรจากสิ่งเหล่านั้น วันหนึ่งคุณอาจต้องการมีวัสดุแทนเฉดสีธรรมดา (เพิ่มเฉดสีพื้นผิวและผสมผสานโหมดจากข้อมูลเป็นต้น) ด้วยอินเทอร์เฟซแบบข้อความทำให้ง่ายต่อการนำไปใช้

สำหรับ renderer มันสามารถเป็นอินเตอร์เฟสที่เรียบง่ายที่สร้าง / ทำลาย / รักษา / สร้างวัตถุที่สร้างขึ้นโดยส่วนประกอบ การเป็นตัวแทนดั้งเดิมของมันอาจเป็นแบบนี้:

class Renderer {
    function Draw() { ... }
    function AddSprite( ... ) { ... return sprite; }
    function RemoveSprite( sprite ) { ... }
    ...
};

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

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