Bagaimana seharusnya objek game saling menyadari satu sama lain?


18

Saya merasa sulit untuk menemukan cara untuk mengatur objek permainan sehingga mereka polimorfik tetapi pada saat yang sama tidak polimorfik.

Berikut ini sebuah contoh: dengan asumsi bahwa kita ingin semua objek kita update()dan draw(). Untuk melakukan itu kita perlu mendefinisikan kelas dasar GameObjectyang memiliki dua metode murni virtual dan membiarkan polimorfisme muncul:

class World {
private:
    std::vector<GameObject*> objects;
public:
    // ...
    update() {
        for (auto& o : objects) o->update();
        for (auto& o : objects) o->draw(window);
    }
};

Metode pembaruan seharusnya menjaga keadaan apa pun yang perlu diperbarui oleh objek kelas tertentu. Faktanya adalah bahwa setiap objek perlu tahu tentang dunia di sekitar mereka. Sebagai contoh:

  • Tambang perlu diketahui jika seseorang bertabrakan dengannya
  • Seorang prajurit harus tahu apakah tentara tim lain berada dalam jarak dekat
  • Seorang zombie harus tahu di mana otak terdekat, dalam radius, adalah

Untuk interaksi pasif (seperti yang pertama) saya berpikir bahwa deteksi tabrakan dapat mendelegasikan apa yang harus dilakukan dalam kasus spesifik tabrakan ke objek itu sendiri dengan a on_collide(GameObject*).

Sebagian besar informasi lainnya (seperti dua contoh lainnya) hanya dapat ditanyakan oleh dunia game yang diteruskan ke updatemetode. Sekarang dunia tidak membedakan objek berdasarkan tipenya (ia menyimpan semua objek dalam wadah polimorfik tunggal), jadi apa yang sebenarnya akan kembali dengan ideal world.entities_in(center, radius)adalah wadah GameObject*. Tapi tentu saja prajurit itu tidak ingin menyerang prajurit lain dari timnya dan zombie tidak peduli dengan zombie lainnya. Jadi kita perlu membedakan perilaku. Sebuah solusi bisa jadi sebagai berikut:

void TeamASoldier::update(const World& world) {
    auto list = world.entities_in(position, eye_sight);
    for (const auto& e : list)
        if (auto enemy = dynamic_cast<TeamBSoldier*>(e))
            // shoot towards enemy
}

void Zombie::update(const World& world) {
    auto list = world.entities_in(position, eye_sight);
    for (const auto& e : list)
        if (auto enemy = dynamic_cast<Human*>(e))
            // go and eat brain
}

tapi tentu saja jumlah dynamic_cast<>per frame bisa sangat tinggi, dan kita semua tahu betapa lambatnya dynamic_castbisa. Masalah yang sama juga berlaku untuk on_collide(GameObject*)delegasi yang telah kita bahas sebelumnya.

Jadi apa itu cara ideal untuk mengatur kode sehingga objek dapat mengetahui objek lain dan dapat mengabaikannya atau mengambil tindakan berdasarkan tipenya?


1
Saya pikir Anda sedang mencari implementasi kustom C ++ RTTI yang fleksibel. Namun demikian, pertanyaan Anda tampaknya bukan hanya tentang mekanisme RTTI yang bijaksana. Hal-hal yang Anda minta diharuskan oleh hampir semua middleware yang akan digunakan gim (sistem animasi, fisika, dan beberapa nama). Bergantung pada daftar kueri yang didukung, Anda bisa menipu RTTI menggunakan ID dan indeks dalam array, atau Anda akhirnya akan merancang protokol lengkap untuk mendukung alternatif yang lebih murah daripada dynamic_cast dan type_info.
teodron

Saya menyarankan agar tidak menggunakan sistem tipe untuk logika game. Sebagai contoh, alih-alih bergantung pada hasil dynamic_cast<Human*>, mengimplementasikan sesuatu seperti bool GameObject::IsHuman(), yang mengembalikan falsesecara default tetapi diganti untuk kembali truedi Humankelas.
congusbongus

tambahan: Anda hampir tidak pernah mengirim satu ton objek ke satu sama lain entitas yang mungkin tertarik pada mereka. Itu optimasi yang jelas Anda harus benar-benar mempertimbangkan.
teodron

@congusbongus Menggunakan vtable dan custom IsAoverrides terbukti hanya sedikit lebih baik daripada casting dinamis dalam praktek untuk saya. Hal terbaik yang harus dilakukan adalah bagi pengguna untuk, jika memungkinkan, mengurutkan daftar data alih-alih mengulangi secara membabi buta seluruh kumpulan entitas.
teodron

4
@ Jeffffrey: idealnya Anda tidak menulis kode tipe khusus. Anda menulis antarmuka -Kode spesifik ("antarmuka" dalam arti umum). Logika Anda untuk TeamASoldierdan TeamBSoldierbenar-benar identik - menembak siapa pun di tim lain. Semua yang dibutuhkan oleh entitas lain adalah GetTeam()metode yang paling spesifik dan, dengan contoh congusbongus, yang dapat diabstraksi lebih jauh menjadi IsEnemyOf(this)semacam antarmuka. Kode tidak perlu peduli dengan klasifikasi taksonomi prajurit, zombie, pemain, dll. Fokus pada interaksi, bukan tipe.
Sean Middleditch

Jawaban:


11

Alih-alih mengimplementasikan pengambilan keputusan masing-masing entitas itu sendiri, Anda juga bisa menggunakan pola pengontrol. Anda akan memiliki kelas pengontrol pusat yang menyadari semua objek (yang penting bagi mereka) dan mengontrol perilaku mereka.

MovementController akan menangani pergerakan semua objek yang dapat bergerak (lakukan pencarian rute, perbarui posisi berdasarkan vektor pergerakan saat ini).

MineBehaviorController akan memeriksa semua ranjau dan semua prajurit, dan memerintahkan ranjau untuk meledak ketika seorang prajurit terlalu dekat.

ZombieBehaviorController akan memeriksa semua zombie dan tentara di sekitarnya, memilih target terbaik untuk setiap zombie, dan memerintahkannya untuk bergerak ke sana dan menyerangnya (langkah itu sendiri ditangani oleh MovementController).

Seorang SoldierBehaviorController akan menganalisis seluruh situasi dan kemudian datang dengan instruksi taktis untuk semua tentara (Anda pindah ke sana, Anda menembak ini, Anda menyembuhkan orang itu ...). Eksekusi sebenarnya dari perintah tingkat yang lebih tinggi ini juga akan ditangani oleh pengontrol tingkat yang lebih rendah. Ketika Anda berupaya, Anda bisa membuat AI mampu membuat keputusan kerja sama yang cukup cerdas.


1
Mungkin ini juga dikenal sebagai "sistem" yang mengelola logika untuk beberapa jenis komponen dalam arsitektur Entity-Component.
teodron

Itu terdengar seperti solusi gaya-C. Komponen dikelompokkan dalam std::maps dan entitas hanya ID dan kemudian kita harus membuat semacam sistem tipe (mungkin dengan komponen tag, karena renderer akan perlu tahu apa yang harus menggambar); dan jika kita tidak ingin melakukan itu kita akan perlu memiliki komponen gambar: tetapi perlu komponen posisi untuk mengetahui di mana harus ditarik, jadi kami membuat dependensi antara komponen yang kami pecahkan dengan sistem pesan yang sangat kompleks. Apakah ini yang Anda sarankan?
Sepatu

1
@ Jeffffrey "Kedengarannya seperti solusi gaya-C" - bahkan ketika itu akan benar, mengapa itu selalu menjadi hal yang buruk? Kekhawatiran lain mungkin valid, tetapi ada solusi untuk mereka. Sayangnya, sebuah komentar terlalu pendek untuk mengalaminya dengan benar.
Philipp

1
@Jefffrey Menggunakan pendekatan di mana komponen itu sendiri tidak memiliki logika dan "sistem" bertanggung jawab untuk menangani semua logika tidak membuat ketergantungan antara komponen juga tidak memerlukan sistem pesan yang sangat kompleks (setidaknya, tidak hampir serumit) . Lihat misalnya: gamadu.com/artemis/tutorial.html

1

Pertama-tama, cobalah untuk mengimplementasikan fitur-fitur agar objek tetap independen satu sama lain, jika memungkinkan. Terutama Anda ingin melakukan itu untuk multi threading. Dalam contoh kode pertama Anda, set semua objek dapat dibagi dalam set yang sesuai dengan jumlah core CPU dan diperbarui dengan sangat efisien.

Tetapi seperti yang Anda katakan, interaksi dengan objek lain diperlukan untuk beberapa fitur. Itu berarti bahwa keadaan semua objek harus disinkronkan di beberapa titik. Dengan kata lain, aplikasi Anda harus menunggu semua tugas paralel selesai terlebih dahulu, dan kemudian menerapkan perhitungan yang melibatkan interaksi. Adalah baik untuk mengurangi jumlah titik sinkronisasi ini, karena selalu menyiratkan bahwa beberapa utas harus menunggu yang lain selesai.

Oleh karena itu, saya menyarankan buffering informasi tentang objek yang diperlukan dari dalam objek lain. Dengan adanya buffer global, Anda dapat memperbarui semua objek yang tidak tergantung satu sama lain, tetapi hanya bergantung pada diri mereka sendiri dan buffer global, yang lebih cepat dan lebih mudah dirawat. Pada stempel waktu tetap, katakan setelah setiap frame, perbarui buffer dengan status objek saat ini.

Jadi apa yang Anda lakukan sekali per frame adalah 1. buffer negara objek saat ini secara global, 2. perbarui semua objek berdasarkan diri mereka sendiri dan buffer, 3. gambar objek Anda dan mulai lagi dengan memperbarui buffer.


1

Gunakan sistem berbasis komponen, di mana Anda memiliki barebones GameObject yang berisi 1 atau lebih komponen yang menentukan perilaku mereka.

Misalnya, beberapa objek seharusnya bergerak ke kiri dan ke kanan sepanjang waktu (platform), Anda dapat membuat komponen seperti itu dan melampirkannya ke GameObject.

Sekarang katakanlah objek game seharusnya perlahan-lahan berputar sepanjang waktu, Anda bisa membuat komponen terpisah yang melakukan hal itu dan melampirkannya ke GameObject.

Bagaimana jika Anda ingin memiliki platform bergerak yang juga diputar, dalam hirarki kelas tradisional yang menjadi sulit dilakukan tanpa duplikasi kode.

Keindahan dari sistem ini, adalah bahwa alih-alih memiliki kelas Rotatable atau MovingPlatform, Anda melampirkan kedua komponen tersebut ke GameObject dan sekarang Anda memiliki MovingPlatform yang AutoRotates.

Semua komponen memiliki properti, 'membutuhkan Pembaruan' yang sementara benar, GameObject akan memanggil metode 'pembaruan' pada komponen tersebut. Misalnya, Anda memiliki komponen Draggable, komponen ini pada tetikus-ke-bawah (jika melebihi GameObject) dapat mengatur 'requireUpdate' menjadi true, dan kemudian pada tetikus-atas tetapkan ke false. Mengizinkannya mengikuti mouse hanya saat mouse turun.

Salah satu pengembang Tony Hawk Pro Skater memiliki defacto yang menuliskannya, dan layak dibaca: http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/


1

Komposisi nikmat daripada warisan.

Saran terkuat saya selain dari ini adalah: Jangan terjebak dalam pola pikir "Saya ingin ini menjadi sangat fleksibel". Fleksibilitas memang bagus, tetapi ingat bahwa pada tingkat tertentu, dalam sistem apa pun yang terbatas seperti permainan, ada bagian atom yang digunakan untuk membangun keseluruhan. Dengan satu atau lain cara, pemrosesan Anda bergantung pada tipe atom yang telah ditentukan sebelumnya. Dengan kata lain, melayani jenis data "apa saja" (jika itu mungkin) tidak akan membantu Anda dalam jangka panjang, jika Anda tidak memiliki kode untuk memprosesnya. Pada dasarnya, semua kode harus mem-parsing / memproses data berdasarkan spesifikasi yang diketahui ... yang berarti sekumpulan tipe yang telah ditentukan. Seberapa besar set itu? Terserah kamu.

Artikel ini menawarkan wawasan tentang prinsip Komposisi atas Warisan dalam pengembangan game melalui arsitektur entitas-komponen yang kuat dan berkinerja.

Dengan membangun entitas dari (berbeda) himpunan bagian dari superset dari komponen yang telah ditentukan, Anda menawarkan AI Anda cara-cara konkret, sedikit demi sedikit untuk memahami dunia dan para aktor di sekitarnya, dengan membaca keadaan 'komponen' aktor-aktor tersebut.


1

Secara pribadi saya merekomendasikan menjaga fungsi draw dari kelas Object itu sendiri. Saya bahkan merekomendasikan menjaga lokasi Objek / koordinat keluar dari Objek itu sendiri.

Metode draw () akan berurusan dengan rendering API tingkat rendah baik OpenGL, OpenGL ES, Direct3D, layer pembungkus Anda pada API tersebut atau API engine. Mungkin Anda harus menukar antara waktu itu (Jika Anda ingin mendukung OpenGL + OpenGL ES + Direct3D misalnya.

GameObject itu seharusnya hanya berisi informasi dasar tentang penampilan visualnya seperti Mesh atau mungkin bundel yang lebih besar termasuk input shader, keadaan animasi, dan sebagainya.

Anda juga akan menginginkan pipa grafis yang fleksibel. Apa yang terjadi jika Anda ingin memesan objek berdasarkan jaraknya ke kamera. Atau jenis bahannya. Apa yang terjadi jika Anda ingin menggambar objek yang 'dipilih' dengan warna yang berbeda. Bagaimana jika alih-alih benar-benar rending sama seperti ketika Anda memanggil fungsi draw pada suatu objek, alih-alih memasukkannya ke dalam daftar perintah tindakan yang harus diambil render (mungkin diperlukan untuk threading). Anda dapat melakukan hal semacam itu dengan sistem lain tetapi itu adalah PITA.

Apa yang saya sarankan adalah daripada menggambar secara langsung, Anda mengikat semua objek yang Anda inginkan ke struktur data lain. Penjilidan itu hanya benar-benar perlu memiliki referensi ke lokasi objek dan informasi render.

Level / chunk / area / peta / hub / wholeworld Anda / apa pun yang diberikan indeks spasial, ini berisi objek dan mengembalikannya berdasarkan permintaan koordinat dan bisa berupa daftar sederhana atau sesuatu seperti Oktree. Itu juga bisa menjadi pembungkus untuk sesuatu yang diimplementasikan oleh mesin fisika pihak ke-3 sebagai adegan fisika. Hal ini memungkinkan Anda untuk melakukan hal-hal seperti "Permintaan semua objek yang ada dalam pandangan kamera dengan beberapa area tambahan di sekitar mereka", atau untuk permainan yang lebih sederhana di mana Anda bisa membuat semuanya mengambil seluruh daftar.

Indeks spasial tidak harus mengandung informasi posisi yang sebenarnya. Mereka bekerja dengan menyimpan objek dalam struktur pohon dalam kaitannya dengan lokasi objek lain. Mereka dapat dianggap sebagai semacam cache lossy yang memungkinkan pencarian cepat objek berdasarkan posisinya. Tidak perlu menduplikasi koordinat X, Y, Z Anda yang sebenarnya. Setelah mengatakan bahwa Anda bisa jika Anda ingin tetap

Bahkan objek game Anda bahkan tidak perlu mengandung informasi lokasi mereka sendiri. Misalnya objek yang belum dimasukkan ke level seharusnya tidak memiliki koordinat x, y, z, itu tidak masuk akal. Anda dapat memuatnya di indeks khusus. Jika Anda perlu mencari koordinat objek berdasarkan referensi aktualnya maka Anda ingin memiliki ikatan antara objek dan grafik adegan (grafik adegan adalah untuk mengembalikan objek berdasarkan koordinat tetapi lambat mengembalikan koordinat berdasarkan objek) .

Ketika Anda menambahkan Obyek ke Tingkat. Ini akan melakukan hal berikut:

1) Buat Struktur Lokasi:

 class Location { 
     float x, y, z; // Or a special Coordinates class, or a vec3 or whatever.
     SpacialIndex& spacialIndex; // Note this could be the area/level/map/whatever here
 };

Ini juga bisa menjadi referensi ke objek dalam mesin fisika pihak ke-3. Atau bisa juga merupakan koordinat offset dengan referensi ke lokasi lain (untuk kamera pelacakan atau objek atau contoh yang dilampirkan). Dengan polimorfisme itu bisa tergantung pada apakah itu objek statis atau dinamis. Dengan menyimpan referensi ke indeks spasial di sini ketika koordinat diperbarui indeks spasial juga bisa.

Jika Anda khawatir tentang alokasi memori dinamis, gunakan kumpulan memori.

2) Ikatan / tautan antara objek Anda, lokasi dan grafik adegan.

typedef std::pair<Object, Location> SpacialBinding.

3) Binding ditambahkan ke indeks spasial di dalam level pada titik yang sesuai.

Saat Anda bersiap untuk membuat.

1) Dapatkan kamera (Ini hanya akan menjadi objek lain, kecuali lokasi itu akan melacak karakter pemain dan penyaji Anda akan memiliki referensi khusus untuk itu, pada kenyataannya hanya itu yang benar-benar dibutuhkan).

2) Dapatkan SpacialBinding kamera.

3) Dapatkan indeks spasial dari pengikatan.

4) Permintaan objek yang (mungkin) terlihat oleh kamera.

5A) Anda perlu memproses informasi visual. Tekstur diunggah ke GPU dan sebagainya. Ini akan lebih baik dilakukan di muka (seperti pada beban level) tetapi mungkin bisa dilakukan saat runtime (untuk dunia terbuka, Anda dapat memuat barang saat Anda mendekati bongkahan tetapi harus tetap dilakukan di muka).

5B) Secara opsional membangun pohon render cache, jika Anda ingin kedalaman / mengurutkan bahan atau melacak objek di dekatnya yang mungkin terlihat di lain waktu. Kalau tidak, Anda bisa menanyakan indeks spasial setiap kali itu tergantung pada persyaratan permainan / kinerja Anda.

Penyaji Anda kemungkinan akan membutuhkan objek RenderBinding yang akan menautkan antara Objek, koordinat

class RenderBinding {
    Object& object;
    RenderInformation& renderInfo;
    Location& location // This could just be a coordinates class.
}

Kemudian ketika Anda membuat, jalankan saja daftar.

Saya telah menggunakan referensi di atas tetapi bisa berupa pointer pintar, pointer mentah, pegangan objek dan sebagainya.

EDIT:

class Game {
    weak_ptr<Camera> camera;
    Level level1;

    void init() {
        Camera camera(75.0_deg, 1.025_ratio, 1000_meters);
        auto template_player = loadObject("Player.json")
        auto player = level1.addObject(move(player), Position(1.0, 2.0, 3.0));
        level1.addObject(move(camera), getRelativePosition(player));

        auto template_bad_guy = loadObject("BadGuy.json")
        level1.addObject(template_bad_guy, {10, 10, 20});
        level1.addObject(template_bad_guy, {10, 30, 20});
        level1.addObject(move(template_bad_guy), {50, 30, 20});
    }

    void render() {
        camera->getFrustrum();
        auto level = camera->getLocation()->getLevel();
        auto object = level.getVisible(camera);
        for(object : objects) {
            render(objects);
        }
    }

    void render(Object& object) {
        auto ri = object.getRenderInfo();
        renderVBO(ri.getVBO());
    }

    Object loadObject(string file) {
        Object object;
        // Load file from disk and set the properties
        // Upload mesh data, textures to GPU. Load shaders whatever.
        object.setHitPoints(// values from file);
        object.setRenderInfo(// data from 3D api);
    }
}

class Level {
    Octree octree;
    vector<ObjectPtr> objects;
    // NOTE: If your level is mesh based there might also be a BSP here. Or a hightmap for an openworld
    // There could also be a physics scene here.
    ObjectPtr addObject(Object&& object, Position& pos) {
        Location location(pos, level, object);
        objects.emplace_back(object);
        object->setLocation(location)
        return octree.addObject(location);
    }
    vector<Object> getVisible(Camera& camera) {
        auto f = camera.getFtrustrum();
        return octree.getObjectsInFrustrum(f);
    }
    void updatePosition(LocationPtr l) {
        octree->updatePosition(l);
    }
}

class Octree {
    OctreeNode root_node;
    ObjectPtr add(Location&& object) {
        return root_node.add(location);
    }
    vector<ObjectPtr> getObjectsInRadius(const vec3& position, const float& radius) { // pass to root_node };
    vector<ObjectPtr> getObjectsinFrustrum(const FrustrumShape frustrum;) {//...}
    void updatePosition(LocationPtr* l) {
        // Walk up from l.octree_node until you reach the new place
        // Check if objects are colliding
        // l.object.CollidedWith(other)
    }
}

class Object {
    Location location;
    RenderInfo render_info;
    Properties object_props;
    Position getPosition() { return getLocation().position; }
    Location getLocation() { return location; }
    void collidedWith(ObjectPtr other) {
        // if other.isPickup() && object.needs(other.pickupType()) pick it up, play sound whatever
    }
}

class Location {
    Position position;
    LevelPtr level;
    ObjectPtr object;
    OctreeNote octree_node;
    setPosition(Position position) {
        position = position;
        level.updatePosition(this);
    }
}

class Position {
    vec3 coordinates;
    vec3 rotation;
}

class RenderInfo {
    AnimationState anim;
}
class RenderInfo_OpenGL : public RenderInfo {
    GLuint vbo_object;
    GLuint texture_object;
    GLuint shader_object;
}

class Camera: public Object {
    Degrees fov;
    Ratio aspect;
    Meters draw_distance;
    Frustrum getFrustrum() {
        // Use above to make a skewed frustum box
    }
}

Adapun hal-hal yang 'disadari' satu sama lain. Itu deteksi tabrakan. Mungkin akan diterapkan di Octree. Anda perlu memberikan beberapa panggilan balik di objek utama Anda. Hal ini paling baik ditangani oleh mesin fisika yang tepat seperti Bullet. Dalam hal ini, ganti saja Octree dengan PhysicsScene dan Position dengan tautan ke sesuatu seperti CollisionMesh.getPosition ().


Wow, ini terlihat sangat bagus. Saya pikir saya sudah memahami ide dasar, tetapi tanpa lebih banyak contoh saya tidak bisa mendapatkan pandangan luar dari ini. Apakah Anda punya referensi atau contoh langsung mengenai hal ini? (Sementara itu saya akan terus membaca jawaban ini).
Sepatu

Tidak benar-benar memiliki contoh, itu hanya apa yang saya rencanakan untuk dilakukan ketika saya punya waktu. Saya akan menambahkan beberapa kelas secara keseluruhan dan melihat apakah itu membantu, Ada ini dan ini . ini lebih tentang kelas objek daripada bagaimana mereka berhubungan atau rendering. Karena saya belum mengimplementasikannya sendiri mungkin ada jebakan, bit yang perlu bekerja atau hal-hal kinerja tapi saya pikir struktur keseluruhan ok.
David C. Bishop
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.