Sistem Entitas dan rendering

11

Okey, apa yang saya ketahui sejauh ini; Entitas mengandung komponen (penyimpanan data) yang menyimpan informasi seperti; - Tekstur / sprite - Shader - dll

Dan kemudian saya memiliki sistem renderer yang menarik semua ini. Tapi yang tidak saya mengerti adalah bagaimana renderer harus dirancang. Haruskah saya memiliki satu komponen untuk setiap "tipe visual". Satu komponen tanpa shader, satu dengan shader, dll?

Hanya perlu beberapa masukan tentang apa "cara yang benar" untuk melakukan ini. Kiat dan perangkap yang harus diperhatikan.

Hayer
sumber
2
Cobalah untuk tidak membuat hal-hal yang terlalu umum. Tampaknya aneh memiliki entitas dengan komponen Shader dan bukan komponen Sprite, jadi mungkin Shader harus menjadi bagian dari komponen Sprite. Secara alami Anda hanya perlu satu sistem rendering.
Jonathan Connell

Jawaban:

8

Ini adalah pertanyaan yang sulit dijawab karena setiap orang memiliki ide sendiri tentang bagaimana sistem komponen entitas harus disusun. Hal terbaik yang dapat saya lakukan adalah membagikan kepada Anda beberapa hal yang menurut saya paling berguna bagi saya.

Kesatuan

Saya mengambil pendekatan kelas lemak untuk ECS, mungkin karena saya menemukan metode pemrograman ekstrim menjadi sangat tidak efisien (dalam hal produktivitas manusia). Untuk itu, entitas bagi saya adalah kelas abstrak untuk diwarisi oleh kelas yang lebih khusus. Entitas memiliki sejumlah properti virtual dan bendera sederhana yang memberi tahu saya apakah entitas ini harus ada atau tidak. Jadi relatif terhadap pertanyaan Anda tentang sistem render, seperti inilah Entitytampilannya:

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; }
}

Komponen

Komponen "bodoh" karena mereka tidak melakukan atau tahu apa - apa. Mereka tidak memiliki referensi ke komponen lain, dan mereka biasanya tidak memiliki fungsi (saya bekerja di C #, jadi saya menggunakan properti untuk menangani getter / setter - jika mereka memiliki fungsi, mereka didasarkan pada pengambilan data yang mereka pegang).

Sistem

Sistem kurang "bodoh", tetapi masih merupakan robot bodoh. Mereka tidak memiliki konteks sistem keseluruhan, tidak memiliki referensi ke sistem lain dan tidak memiliki data kecuali untuk beberapa buffer yang mereka mungkin perlu melakukan pemrosesan masing-masing. Bergantung pada sistemnya, ia mungkin memiliki spesialisasi Update, atau Drawmetode, atau dalam beberapa kasus, keduanya.

Antarmuka

Antarmuka adalah struktur utama dalam sistem saya. Mereka digunakan untuk mendefinisikan apa yang Systembisa diproses, dan apa yang Entitymampu dilakukan. Antarmuka yang relevan untuk rendering adalah: IRenderabledan IAnimatable.

Antarmuka hanya memberitahu sistem komponen mana yang tersedia. Misalnya, sistem rendering perlu mengetahui kotak pembatas entitas dan gambar yang akan digambar. Dalam kasus saya, itu akan menjadi SpatialComponentdan ImageComponent. Jadi sepertinya ini:

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

Sistem Rendering

Jadi bagaimana sistem rendering menarik entitas? Ini sebenarnya cukup sederhana, jadi saya hanya akan menunjukkan kepada Anda kelas telanjang untuk memberi Anda ide:

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);
        }
    }
}

Melihat kelas, sistem render bahkan tidak tahu apa Entityitu. Yang ia tahu hanyalah IRenderabledan hanya diberi daftar mereka untuk menggambar.

Bagaimana Ini Semua Bekerja

Mungkin juga membantu untuk memahami bagaimana saya membuat objek game baru dan bagaimana saya memasukkannya ke sistem.

Menciptakan Entitas

Semua objek game mewarisi dari Entity, dan semua antarmuka yang berlaku yang menjelaskan apa yang bisa dilakukan objek game itu. Semua yang dianimasikan di layar terlihat seperti ini:

public class MyAnimatedWidget : Entity, IRenderable, IAnimatable {}

Memberi Makan Sistem

Saya menyimpan daftar semua entitas yang ada di dunia game dalam satu daftar bernama List<Entity> gameObjects. Setiap frame, saya kemudian menyaring daftar itu dan menyalin referensi objek ke lebih banyak daftar berdasarkan jenis antarmuka, seperti List<IRenderable> renderableObjects, dan List<IAnimatable> animatableObjects. Dengan cara ini, jika sistem yang berbeda perlu memproses entitas yang sama, mereka bisa. Kemudian saya hanya menyerahkan daftar itu ke masing-masing sistem Updateatau Drawmetode dan membiarkan sistem melakukan pekerjaan mereka.

Animasi

Anda mungkin ingin tahu bagaimana sistem animasi bekerja. Dalam kasus saya, Anda mungkin ingin melihat antarmuka IAnimatable:

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

Hal utama yang perlu diperhatikan di sini adalah ImageComponentaspek IAnimatableantarmuka tidak hanya baca; ia memiliki setter .

Seperti yang sudah Anda duga, komponen animasi hanya menyimpan data tentang animasi; daftar frame (yang merupakan komponen gambar), frame saat ini, jumlah frame per detik yang akan diambil, waktu yang berlalu sejak peningkatan frame terakhir, dan opsi lainnya.

Sistem animasi mengambil keuntungan dari sistem rendering dan hubungan komponen gambar. Itu hanya mengubah komponen gambar entitas karena ia menambah bingkai animasi. Dengan begitu, animasi diberikan secara tidak langsung oleh sistem rendering.

Nol
sumber
Saya mungkin harus mencatat bahwa saya tidak benar-benar tahu apakah ini bahkan dekat dengan apa yang orang sebut sistem entitas-komponen . Dalam upaya saya untuk mengimplementasikan desain berbasis komposisi, saya menemukan diri saya jatuh ke dalam pola ini.
Cypher
Menarik! Saya tidak terlalu tertarik pada kelas abstrak untuk Entitas Anda tetapi antarmuka IRenderable adalah ide yang bagus!
Jonathan Connell
5

Lihat jawaban ini untuk melihat jenis sistem yang saya bicarakan.

Komponen harus berisi detail untuk apa yang akan menggambar dan cara menggambarnya. Sistem rendering akan mengambil detail tersebut dan menggambar entitas dengan cara yang ditentukan oleh komponen. Hanya jika Anda menggunakan teknologi gambar yang sangat berbeda maka Anda memiliki komponen terpisah untuk gaya yang berbeda.

MichaelHouse
sumber
3

Alasan utama untuk memisahkan logika menjadi komponen adalah untuk membuat satu set data yang bila digabungkan dalam suatu entitas mereka menghasilkan perilaku yang berguna dan dapat digunakan kembali. Misalnya memisahkan Entitas ke dalam PhysicsComponent dan RenderComponent masuk akal karena kemungkinan tidak semua entitas memiliki Physics dan beberapa entitas mungkin tidak memiliki Sprite.

Untuk menjawab pertanyaan Anda, Anda perlu melihat arsitektur Anda dan bertanya pada diri sendiri dua pertanyaan:

  1. Apakah masuk akal untuk memiliki Shader tanpa Tekstur
  2. Akankah memisahkan Shader dari Tekstur memungkinkan saya menghindari duplikasi kode?

Ketika membagi komponen, penting untuk menanyakan pertanyaan ini, jika jawaban untuk 1. adalah ya, maka Anda mungkin memiliki kandidat yang baik untuk membuat dua komponen terpisah, satu dengan Shader dan satu dengan Texture. Jawaban 2. biasanya ya untuk komponen seperti Posisi di mana banyak komponen dapat menggunakan posisi.

Misalnya, Fisika dan Audio mungkin menggunakan posisi yang sama, daripada kedua komponen menyimpan posisi duplikat Anda refactor mereka menjadi satu PositionComponent dan mengharuskan entitas yang menggunakan PhysicsComponent / AudioComponent juga memiliki PositionComponent.

Berdasarkan informasi yang Anda berikan kepada kami, sepertinya RenderComponent Anda tidak cocok untuk dipecah menjadi TextureComponent dan ShaderComponent karena shader sepenuhnya bergantung pada Texture dan tidak ada yang lain.

Dengan asumsi Anda menggunakan sesuatu yang mirip dengan T-Machine: Entity Systems contoh implementasi dari RenderComponent & RenderSystem di C ++ akan terlihat seperti ini:

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;
}
Jake Woods
sumber
Itu sepenuhnya salah. Inti dari komponen adalah untuk memisahkan mereka dari entitas, bukan membuat sistem render mencari melalui entitas untuk menemukannya. Sistem rendering harus sepenuhnya mengontrol data mereka sendiri. PS Jangan meletakkan std :: vector (terutama dengan data contoh) di loop, itu mengerikan (lambat) C ++.
snake5
@ snake5 Anda benar dalam kedua hal. Saya mengetik kode di atas kepala saya dan ada beberapa masalah, terima kasih telah menunjukkannya. Saya telah memperbaiki kode yang terpengaruh agar tidak terlalu lambat dan menggunakan idiom sistem entitas dengan benar.
Jake Woods
2
@ snake5 Anda tidak menghitung ulang data setiap frame, getComponents mengembalikan vektor yang dimiliki oleh m_manager yang sudah dikenal dan hanya berubah ketika Anda menambah / menghapus komponen. Ini merupakan keuntungan ketika Anda memiliki sistem yang ingin menggunakan beberapa komponen dari entitas yang sama, misalnya PhysicsSystem yang ingin menggunakan PositionComponent dan PhysicsComponent. Sistem lain mungkin akan menginginkan posisi dan dengan memiliki PositionComponent Anda tidak memiliki data duplikat. Terutama itu memecahkan masalah bagaimana komponen berkomunikasi.
Jake Woods
5
@ snake5 Pertanyaannya bukan tentang bagaimana sistem EC harus ditata atau kinerjanya. Pertanyaannya adalah tentang mengatur sistem render. Ada beberapa cara untuk menyusun sistem EC, jangan terjebak pada masalah kinerja satu sama lain di sini. OP kemungkinan menggunakan struktur EC yang sama sekali berbeda dari salah satu jawaban Anda. Kode yang disediakan dalam jawaban ini hanya dimaksudkan untuk menunjukkan contoh dengan lebih baik, bukan untuk dikritik karena kinerjanya. Jika pertanyaannya adalah tentang kinerja daripada mungkin ini akan membuat jawaban "tidak berguna", tetapi itu tidak.
MichaelHouse
2
Saya lebih suka desain yang dijabarkan dalam jawaban ini daripada di Cyphers. Ini sangat mirip dengan yang saya gunakan. Komponen yang lebih kecil lebih baik imo, bahkan jika mereka hanya memiliki satu atau dua variabel. Mereka harus mendefinisikan aspek suatu entitas, seperti komponen "Damagable" saya yang memiliki 2, mungkin 4 variabel (maks dan arus untuk setiap kesehatan dan pelindung). Komentar ini semakin panjang, mari kita beralih ke obrolan jika Anda ingin membahas lebih lanjut.
John McDonald
2

Jebakan # 1: kode overdesigned. Pikirkan apakah Anda benar-benar membutuhkan setiap hal yang Anda terapkan karena Anda harus hidup dengannya untuk beberapa waktu.

Lubang # 2: terlalu banyak objek. Saya tidak akan menggunakan sistem dengan objek terlalu banyak (satu untuk setiap jenis, subtipe dan apa pun) karena hanya membuat pemrosesan otomatis lebih sulit. Menurut pendapat saya, itu jauh lebih baik untuk memiliki setiap objek mengontrol set fitur tertentu (sebagai lawan dari satu fitur). Misalnya, membuat komponen untuk setiap bit data yang termasuk dalam rendering (komponen tekstur, komponen shader) terlalu dibagi - Anda biasanya tetap harus memiliki semua komponen itu bersama-sama, bukankah Anda setuju?

Kesalahan # 3: kontrol eksternal yang terlalu ketat. Lebih suka mengubah nama menjadi objek shader / tekstur karena objek dapat berubah dengan renderer / tipe tekstur / format shader / apa pun. Nama-nama adalah pengidentifikasi sederhana - tergantung pada penyaji untuk memutuskan apa yang dihasilkan dari itu. Suatu hari Anda mungkin ingin memiliki bahan alih-alih shader polos (tambahkan shader, tekstur dan mode campuran dari data, misalnya). Dengan antarmuka berbasis teks, lebih mudah untuk mengimplementasikannya.

Adapun renderer, itu bisa berupa antarmuka sederhana yang membuat / menghancurkan / memelihara / membuat objek yang dibuat oleh komponen. Representasi paling primitif dari itu bisa menjadi seperti ini:

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

Ini akan memungkinkan Anda untuk mengelola objek-objek ini dari komponen Anda dan menjaganya cukup jauh untuk memungkinkan Anda membuat mereka dengan cara apa pun yang Anda inginkan.

snake5
sumber