Oyun nesneleri birbirinden nasıl haberdar olmalıdır?


18

Oyun nesnelerini polimorfik ancak aynı zamanda polimorfik olmayacak şekilde organize etmenin bir yolunu bulmakta zorlanıyorum.

İşte bir örnek: tüm nesnelerimizin update()ve olmasını istediğimizi varsayarsak draw(). Bunu yapmak için GameObject, bu iki sanal saf yönteme sahip olan ve polimorfizmin devreye girmesine izin veren bir temel sınıf tanımlamamız gerekir :

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

Güncelleme yönteminin, belirli sınıf nesnesinin güncellemesi gereken durumlarla ilgilenmesi gerekir. Gerçek şu ki, her nesnenin etraflarındaki dünyayı bilmesi gerekiyor. Örneğin:

  • Bir mayının birinin onunla çarpışıp çarpışmadığını bilmesi gerekir
  • Bir asker, başka bir takımın askerinin yakında olup olmadığını bilmeli
  • Bir zombi, yarıçap içindeki en yakın beynin nerede olduğunu bilmelidir

Pasif etkileşimler için (birincisi gibi) Çarpışma tespitinin belirli çarpışma durumlarında ne yapılacağını nesnenin kendisine atar on_collide(GameObject*).

Diğer bilgilerin çoğu (diğer iki örnek gibi) updatemetoda iletilen oyun dünyası tarafından sorgulanabilir . Şimdi dünya, nesnelerini türlerine göre ayırt etmiyor (tüm nesneleri tek bir polimorfik kapta saklıyor), bu yüzden aslında bir ideal ile geri dönecek olan world.entities_in(center, radius)bir kaptır GameObject*. Ancak elbette asker, ekibinden diğer askerlere saldırmak istemiyor ve bir zombi diğer zombiler hakkında da dava açmıyor. Bu yüzden davranışı ayırt etmeliyiz. Bir çözüm aşağıdaki olabilir:

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
}

ancak elbette dynamic_cast<>kare başına düşen sayısı çok yüksek olabilir ve hepimiz ne kadar yavaş dynamic_castolabileceğini biliyoruz . Aynı sorun on_collide(GameObject*), daha önce tartıştığımız delege için de geçerlidir .

Peki, nesneleri diğer nesnelerin farkında olmak ve bunları yok sayabilmeleri veya türlerine göre eylemler yapabilmeleri için kodu düzenlemenin ideal yolu nedir?


1
Bence çok yönlü bir C ++ RTTI özel uygulaması arıyorsunuz. Bununla birlikte, sorunuz sadece makul RTTI mekanizmalarıyla ilgili görünmüyor. İstediğiniz şeyler, oyunun kullanacağı neredeyse tüm ara yazılımlar için gereklidir (animasyon sistemi, birkaç isim için fizik). Desteklenen sorgular listesine bağlı olarak, dizilerdeki kimlikleri ve dizinleri kullanarak RTTI'da yolunuzu aldatabilir veya dynamic_cast ve type_info'ya daha ucuz alternatifleri desteklemek için tam teşekküllü bir protokol tasarlayabilirsiniz.
teodron

Oyun mantığı için tür sistemini kullanmamanızı tavsiye ederim. Örneğin, yerine sonucuna bağlı olarak dynamic_cast<Human*>, bir gibi bir şey uygulamak bool GameObject::IsHuman()döner falsedönmek için geçersiz kılınır varsayılan olarak ama trueiçinde Humansınıfa.
congusbongus

ekstra: neredeyse hiçbir zaman birbirleriyle ilgilenebilecek tonlarca nesne göndermezsiniz. Bu gerçekten dikkate almanız gereken bariz bir optimizasyon.
teodron

@congusbongus Vtable ve özel IsAgeçersiz kılmalar kullanmanın benim için pratikte dinamik dökümden çok az daha iyi olduğu kanıtlandı. Yapılacak en iyi şey, tüm varlık havuzu üzerinde körü körüne yineleme yapmak yerine, mümkün olan yerlerde, sıralı veri listelerine sahip olmaktır.
teodron

4
@ Jefffrey: ideal olarak türe özgü kod yazmazsınız. Arayüz -özel kod (genel anlamda "arayüz") yazarsınız . A için mantığınız TeamASoldierve TeamBSoldiergerçekten aynı - diğer takımdaki herhangi birine vuruluyor. Diğer varlıkların ihtiyacı olan GetTeam()tek şey, en spesifik haliyle ve congusbongus'un örneğine göre, daha da fazla bir IsEnemyOf(this)arayüze eklenebilen bir yöntemdir . Kodun askerlerin, zombilerin, oyuncuların vb. Taksonomik sınıflandırmalarını önemsemesi gerekmez. Türlere değil etkileşime odaklanın.
Sean Middleditch

Yanıtlar:


11

Her bir varlığın karar vermesini kendi başına uygulamak yerine, alternatif olarak denetleyici desenini tercih edebilirsiniz. Tüm nesnelerin (onlar için önemli olan) farkında olan ve davranışlarını kontrol eden merkezi denetleyici sınıflarınız olacaktır.

Bir MovementController hareket edebilen tüm nesnelerin hareketini idare eder (rota bulma, mevcut hareket vektörlerine dayalı pozisyonları güncelleme).

Bir MineBehaviorController tüm mayınları ve tüm askerleri kontrol eder ve bir asker çok yaklaştığında bir mayına patlamasını emreder.

Bir ZombieBehaviorController etrafındaki tüm zombileri ve askerleri kontrol eder, her zombi için en iyi hedefi seçer ve oraya hareket etmesini ve saldırmasını emreder (hareketin kendisi MovementController tarafından idare edilir).

Bir SoldierBehaviorController tüm durumu analiz eder ve sonra tüm askerler için taktik talimatlar bulur (oraya taşınırsınız, vurursunuz, o adamı iyileştirirsiniz ...). Bu üst düzey komutların gerçek yürütülmesi, alt düzey denetleyiciler tarafından da ele alınacaktır. Biraz çaba harcadığınızda, AI'yı oldukça akıllı işbirliği kararları alabilecek hale getirebilirsiniz.


1
Muhtemelen bu, bir Varlık-Bileşen mimarisindeki belirli bileşen türleri için mantığı yöneten "sistem" olarak da bilinir.
teodron

Bu C tarzı bir çözüm gibi geliyor. Bileşenler std::maps olarak gruplandırılmıştır ve varlıklar sadece kimliklerdir ve daha sonra bir tür yazı sistemi yapmak zorundayız (belki bir etiket bileşeniyle, çünkü oluşturucunun ne çizeceğini bilmesi gerekir); ve bunu yapmak istemiyorsak, bir çizim bileşenine ihtiyacımız olacak: ancak nereye çekileceğini bilmek için pozisyon bileşenine ihtiyacı var, bu yüzden süper karmaşık bir mesajlaşma sistemi ile çözdüğümüz bileşenler arasında bağımlılıklar yaratıyoruz. Önerdiğin bu mu?
Ayakkabı

1
@Jefffrey "Bu C tarzı bir çözüm gibi geliyor" - bu doğru olsa bile, neden mutlaka kötü bir şey olsun ki? Diğer endişeler geçerli olabilir, ancak onlar için çözümler var. Maalesef bir yorum her birine uygun şekilde hitap etmek için çok kısa.
Philipp

1
@Jefffrey Bileşenlerin kendilerinin herhangi bir mantığı olmadığı ve "sistemlerin" tüm mantığın ele alınmasından sorumlu olduğu yaklaşımın kullanılması, bileşenler arasında bağımlılık yaratmaz veya süper karmaşık mesajlaşma sistemi gerektirmez (en azından neredeyse karmaşık değil) . Örneğin bakınız: gamadu.com/artemis/tutorial.html

1

Her şeyden önce, mümkün olduğunca nesnelerin birbirinden bağımsız kalması için özellikleri uygulamaya çalışın. Özellikle bunu çoklu diş çekme için yapmak istersiniz. İlk kod örneğinizde, tüm nesnelerin kümesi CPU çekirdeği sayısıyla eşleşen kümelere bölünebilir ve çok verimli bir şekilde güncellenebilir.

Ancak dediğin gibi, bazı özellikler için diğer nesnelerle etkileşim gereklidir. Bu, tüm nesnelerin durumunun bazı noktalarda senkronize edilmesi gerektiği anlamına gelir. Başka bir deyişle, uygulamanız önce tüm paralel görevlerin bitmesini beklemeli ve sonra etkileşim içeren hesaplamaları uygulamalıdır. Her zaman bazı iş parçacıklarının diğerlerinin bitmesini beklemesi gerektiğini ima ettiklerinden, bu senkronizasyon noktalarının sayısını azaltmak iyidir.

Bu nedenle, diğer nesnelerin içinden ihtiyaç duyulan nesneler hakkında bu bilgileri arabelleğe almanızı öneririm. Böyle bir genel arabellek göz önüne alındığında, tüm nesnelerinizi birbirinden bağımsız olarak güncelleyebilirsiniz, ancak yalnızca kendilerine ve hem daha hızlı hem de bakımı daha kolay olan genel ara belleğe bağlıdır. Sabit bir zaman aralığında, örneğin her kareden sonra, arabelleği geçerli nesnelerin durumu ile güncelleyin.

Yani her kare için bir kez yaptığınız şey 1. mevcut nesnelerin durumunu global olarak tamponlayın, 2. tüm nesneleri kendilerine ve ara belleğe göre güncelleyin, 3. nesnelerinizi çizin ve sonra tamponu yenileyerek başlayın.


1

GameObject öğesinin davranışlarını tanımlayan 1 veya daha fazla bileşen içeren bir barebone'unuzun bulunduğu bileşen tabanlı bir sistem kullanın.

Örneğin, bazı nesnelerin her zaman sola ve sağa hareket etmesi gerektiğini varsayalım (bir platform), böyle bir bileşen oluşturabilir ve bir GameObject öğesine ekleyebilirsiniz.

Şimdi bir oyun nesnesinin her zaman yavaşça dönmesi gerektiğini söyleyin, bunu yapan ayrı bir bileşen oluşturabilir ve bunu GameObject'e ekleyebilirsiniz.

Ya da, kodu çoğaltmadan yapmak zorlaşan geleneksel bir sınıf heirarchy'de de dönen bir hareketli platforma sahip olmak istiyorsanız.

Bu sistemin güzelliği, bir Dönebilen veya MovingPlatform sınıfına sahip olmak yerine, bu bileşenlerin her ikisini de GameObject'e bağlamanız ve şimdi AutoRotates olan bir MovingPlatform'a sahip olmanızdır.

Tüm bileşenlerin bir özelliği vardır, 'requiredUpdate', bu değer doğru olsa da GameObject adı geçen bileşen üzerindeki 'update' yöntemini çağırır. Örneğin, Sürüklenebilir bir bileşene sahip olduğunuzu varsayalım, fareyle üzerine gelindiğinde (GameObject öğesindeyse) bu bileşen 'gerektirirGüncelleme' seçeneğini true olarak ayarlayabilir ve ardından fare-up'da bunu false olarak ayarlayabilir. Yalnızca fare kapalıyken fareyi izlemesine izin verme.

Tony Hawk Pro Skater geliştiricilerinden biri üzerine yazmak için defacto var ve okumaya değer: http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/


1

Kompozisyonu kalıtım yerine tercih edin.

Bunun dışında benim en güçlü tavsiyem şudur: "Bunun son derece esnek olmasını istiyorum" anlayışına girmeyin. Esneklik harika, ancak bir düzeyde, bir oyun gibi herhangi bir sonlu sistemde , bütünü oluşturmak için kullanılan atom parçaları olduğunu unutmayın. Şu ya da bu şekilde, işleminiz önceden tanımlanmış bu atomik türlere dayanır. Başka bir deyişle, "herhangi bir" veri türünü (bu mümkün olsaydı) karşılamak, işlemek için kodunuz yoksa uzun vadede size yardımcı olmaz. Temel olarak, tüm kodların bilinen spesifikasyonlara göre verileri ayrıştırması / işlemesi gerekir ... Bu, önceden tanımlanmış bir türler kümesi anlamına gelir. Bu set ne kadar büyük? Sana bağlı.

Bu makale , sağlam ve performanslı bir varlık bileşeni mimarisi aracılığıyla oyun geliştirmede Kalıtım Üzerine Kompozisyon ilkesi hakkında bilgi vermektedir.

Önceden tanımlanmış bileşenlerin bazı üst kümelerinin (farklı) alt kümelerinden varlıklar oluşturarak, AI'larınıza bu aktörlerin bileşenlerinin durumlarını okuyarak dünyayı ve etraflarındaki aktörleri anlamanın somut, parçalı yollarını sunarsınız.


1

Şahsen draw fonksiyonunu Object sınıfının dışında tutmanızı tavsiye ederim. Hatta Nesnelerin konumunu / koordinatlarını Nesnenin kendisinden uzak tutmanızı öneririm.

Bu draw () yöntemi, OpenGL, OpenGL ES, Direct3D, bu API'lardaki sarma katmanınız veya bir motor API'sı gibi düşük düzeyli oluşturma API'sı ile ilgilenecektir. O zaman (örneğin OpenGL + OpenGL ES + Direct3D'yi desteklemek istiyorsanız) arasında geçiş yapmanız gerekebilir.

Bu GameObject, Mesh gibi görsel görünümüyle ilgili temel bilgileri veya gölgelendirici girdileri, animasyon durumu vb.Dahil olmak üzere daha büyük bir paket içermelidir.

Ayrıca esnek bir grafik boru hattı isteyeceksiniz. Nesneleri kameraya olan mesafelerine göre sipariş etmek isterseniz ne olur? Veya malzeme türleri. 'Seçili' bir nesneyi farklı bir renk çizmek isterseniz ne olur? Bir nesne üzerinde bir çizim işlevi çağırırken gerçekte soo yapmak yerine, işlemenin gerçekleştirilmesi için eylemlerin bir komut listesine koyarsa (iş parçacığı için gerekli olabilir) ne olur? Bu tür bir şeyi diğer sistemle yapabilirsiniz ama bu bir PITA.

Önerdiğim doğrudan çizim yapmak yerine, istediğiniz tüm nesneleri başka bir veri yapısına bağlarsınız. Bu bağlamanın gerçekten nesnelerin konumuna ve oluşturma bilgilerine referans olması gerekir.

Seviyeleriniz / parçalarınız / alanlarınız / haritalarınız / hub'larınız / tüm dünya / bir uzaysal indeks ne olursa olsun, bu nesneleri içerir ve bunları koordinat sorgularına dayalı olarak döndürür ve basit bir liste veya bir Octree gibi bir şey olabilir. Ayrıca, bir fizik sahnesi olarak 3. taraf bir fizik motoru tarafından uygulanan bir şey için bir sarıcı olabilir. "Kamera görünümündeki tüm nesneleri etraflarında fazladan bir alanla sorgula" veya her şeyi listenin tamamını kaplayabileceğiniz daha basit oyunlar için yapmanızı sağlar.

Özel Dizinlerin gerçek konumlandırma bilgilerini içermesi gerekmez. Nesneleri ağaç yapılarında diğer nesnelerin konumuna göre depolayarak çalışırlar. Bir nesnenin konumuna göre hızlı bir şekilde aranmasına izin veren bir tür kayıplı önbellek olarak düşünülebilirler. Gerçek X, Y, Z koordinatlarınızı çoğaltmanıza gerek yoktur. Tutmak istersen yapabileceğini söyledi

Aslında oyun nesnelerinizin kendi konum bilgilerini içermesi bile gerekmez. Örneğin, bir düzeye yerleştirilmemiş bir nesnenin x, y, z koordinatları olmamalı, bu hiç mantıklı değil. Bunu özel dizine ekleyebilirsiniz. Gerçek referansına göre nesnenin koordinatlarına bakmanız gerekiyorsa, nesne ile sahne grafiği arasında bir bağ kurmak istersiniz (sahne grafikleri, nesneleri koordinatlara göre döndürmek içindir, ancak nesnelere dayalı koordinatları döndürmede yavaştır) .

Seviyeye bir Nesne eklediğinizde. Aşağıdakileri yapacaktır:

1) Konum Yapısı Oluşturun:

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

Bu aynı zamanda üçüncü taraf fizik motorlarındaki bir nesneye referans olabilir. Veya başka bir konuma referansla ofset koordinatları olabilir (bir izleme kamerası veya ekli bir nesne veya örnek için). Polimorfizmde, statik veya dinamik bir nesne olmasına bağlı olabilir. Koordinatlar güncellendiğinde burada uzaysal dizine bir referans tutarak uzaysal dizin de olabilir.

Dinamik bellek ayırma konusunda endişeleriniz varsa bir bellek havuzu kullanın.

2) Nesneniz, konumu ve sahne grafiği arasında bir bağlama / bağlantı.

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

3) Bağlama, seviyenin içindeki boşluk indeksine uygun noktada eklenir.

Oluşturmaya hazırlanırken.

1) Kamerayı al (Sadece başka bir nesne olacak, ancak konumu oyuncuların karakterini izleyecek ve oluşturucunun özel bir referansı olacak, aslında hepsi gerçekten gerekli).

2) Kameranın SpacialBinding özelliğini edinin.

3) Boşluk dizinini ciltlemeden alın.

4) Kamera tarafından görülebilen (muhtemelen) nesneleri sorgulayın.

5A) Görsel bilgilerin işlenmesi gerekir. GPU'ya yüklenen dokular vb. Bu en iyi şekilde önceden (seviye yükünde olduğu gibi) yapılabilir, ancak belki de çalışma zamanında yapılabilir (açık bir dünya için, bir parçaya yaklaştığınızda bir şeyler yükleyebilirsiniz, ancak yine de önceden yapılması gerekir).

5B) İsteğe bağlı olarak önbelleğe alınmış bir oluşturma ağacı oluşturun, derinlik / malzeme sıralaması yapmak veya yakındaki nesneleri takip etmek istiyorsanız, daha sonra görülebilir. Aksi takdirde, oyun / performans gereksinimlerinize bağlı olacağı her zaman boşluk dizinini sorgulayabilirsiniz.

Oluşturucunuz büyük olasılıkla Nesne, koordinatlar arasında bağlantı kuracak bir RenderBinding nesnesine ihtiyaç duyacaktır.

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

Sonra render, sadece liste üzerinden çalıştırın.

Yukarıda referanslar kullandım ama akıllı işaretçiler, ham işaretçiler, nesne tutamakları vb.

DÜZENLE:

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

İşleri birbirinden 'haberdar' hale getirmek için. Bu çarpışma tespiti. Muhtemelen Octree'de uygulanacaktır. Ana nesnenizde bir geri arama sağlamanız gerekir. Bu şey en iyi Bullet gibi uygun bir fizik motoru tarafından ele alınır. Bu durumda Octree yerine PhysicsScene ve Position yerine CollisionMesh.getPosition () gibi bir bağlantı ekleyin.


Vay canına, bu çok iyi görünüyor. Sanırım temel fikri anladım, ancak daha fazla örnek olmadan bunun dış görünümünü tam olarak anlayamıyorum. Bununla ilgili başka referanslarınız veya canlı örnekleriniz var mı? (Bu arada bir süre bu cevabı okumaya devam edeceğim).
Ayakkabı

Gerçekten örnek yok, sadece zaman bulduğumda yapmayı planladığım şey bu. Genel sınıfların birkaçını daha ekleyeceğim ve bunun yardımcı olup olmadığını göreceğim, İşte bu ve bu . nesne sınıflarıyla ilgili oldukları veya oluşturdukları şeylerden daha çok. Kendim uygulamadığım için, tuzaklar, çalışma gerektiren parçalar veya performans malzemeleri olabilir, ancak genel yapının iyi olduğunu düşünüyorum.
David
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.