Oyun mimarisi / tasarım sorusu - küresel örneklerden kaçınırken verimli bir motor oluşturma (C ++ oyun)


28

Oyun mimarisi hakkında bir sorum vardı: Farklı bileşenlerin birbirleriyle iletişim kurmasının en iyi yolu nedir?

Bu soruyu milyonlarca kez sorduğum için gerçekten özür dilerim, ancak tam olarak aradığım türden bir bilgi bulamıyorum.

Sıfırdan bir oyun kurmaya çalışıyordum (eğer önemliyse C ++) ve ilham kaynağı olan bazı açık kaynaklı yazılımları (Super Maryo Chronicles, OpenTTD ve diğerleri) gözlemledim. Bu oyun tasarımlarının birçoğunun (her yerde kuyruk oluşturma, işletme yöneticileri, video yöneticileri ve benzeri şeyler için) küresel örnekleri ve / veya tekilleri kullandığını fark ettim. Global örneklerden ve tekilliklerden kaçınmaya ve mümkün olduğunca gevşek bir şekilde birleştirilmiş bir motor oluşturmaya çalışıyorum, ancak etkili tasarımdaki deneyimsizliğime borçlu olduğum bazı engellere çarpıyorum. (Bu proje için motivasyonun bir kısmı buna değiniyor :))

GameCoreDiğer projelerde gördüğüm dünyaya benzer üyelere sahip bir ana nesneye sahip olduğum bir tasarım yaptım (yani, bir giriş yöneticisi, video yöneticisi, GameStagetüm varlıkları ve oyun oynamayı kontrol eden bir nesne var) şu anda hangi aşamada yüklü olursa olsun vb. Sorun şu ki, GameCorenesnede her şey merkezileştiği için, farklı bileşenlerin birbirleriyle iletişim kurması için kolay bir yolum yok.

Örneğin, Super Maryo Chronicles'a bakıldığında, örneğin, oyunun bir bileşeni başka bir bileşenle iletişim kurmaya ihtiyaç duyduğunda (yani bir düşman nesnesi kendini oluşturma aşamasında çizilecek işleme sırasına eklemek ister), sadece genel örnek.

Benim için oyun nesnelerimin ilgili bilgiyi GameCorenesneye geri göndermesini sağlamalıyım , böylece GameCorenesne bu bilgiyi ihtiyaç duyduğu sistemin diğer bileşenlerine aktarabilir (örneğin: yukarıdaki durum için her düşman nesnesi) oluşturma bilgilerini GameStagenesneye geri iletir, bu da hepsini toplar ve geri iletir GameCore, bu da onu dönüşüm için video yöneticisine iletir). Bu, gerçekten korkunç bir tasarım gibi geliyor ve bunun için bir çözüm bulmaya çalışıyordum. Olası tasarımlar üzerine düşüncelerim:

  1. Global örnekler (Süper Maryo Chronicles, OpenTTD, vb. Tasarımı)
  2. Sahip GameCoretüm nesneler alışveriş içine girdiği bir aracı olarak hedef hareket (mevcut tasarım yukarıda tarif edilmiştir)
  3. Konuşacakları tüm diğer bileşenlere bileşen göstericileri verin (örn. Yukarıdaki Maryo örneğinde, düşman sınıfının konuşması gereken video nesnesinin bir göstergesi olacak)
  4. Oyunu alt sistemlere ayırma - Örneğin, alt sistemlerindeki nesneler arasındaki GameCoreiletişimi yürüten nesnede yönetici nesnelerine sahip olma
  5. (Diğer seçenekler? ....)

Yukarıdaki seçenek 4'ü en iyi çözüm olarak hayal ediyorum, ancak bunu tasarlamakta zorlanıyorum ... belki de globals kullanan gördükleri tasarımlar hakkında düşündüm. Mevcut tasarımımda var olan aynı sorunu alıyorum ve her alt sistemde çoğaltıyorum, daha küçük bir ölçekte. Örneğin, GameStageyukarıda açıklanan nesne biraz buna teşebbüs etmektir, ancak GameCorenesne hala sürece katılmaktadır.

Burada herhangi bir tasarım önerisi sunan var mı?

Teşekkürler!


1
Singletons'ın mükemmel bir tasarım olmadığını içgüdünüzü anlıyorum. Deneyimlerime göre, sistemler arasındaki iletişimi yönetmenin en basit yolu onlardı
Emmett Butler

4
En iyi uygulama olup olmadığını bilmediğimden bir yorum olarak eklemek. GirişSistemi, GrafikSistemi, vb. Bu noktada, GameManager referansı üzerinden erişerek başka bir sisteme başvurabilirim.
Inisheer

Etiketleri değiştirdim çünkü bu soru oyun tasarımı değil kodla ilgilidir.
Klaim

bu iş parçacığı biraz eski, ama tamamen aynı sorun var. OGRE kullanıyorum ve en iyi yolu kullanmaya çalışıyorum, bence seçenek # 4 en iyi yaklaşım. Gelişmiş Ogre Framework gibi bir şey inşa ettim, ancak bu çok modüler değil. Sanırım sadece klavye vuruşlarını ve fare hareketlerini alan bir alt sistem giriş işlemine ihtiyacım var. Benim çözemediğim şey ise, alt sistemler arasında nasıl böyle bir "iletişim" yöneticisi yaratabilirim?
Dominik2000

1
Merhaba @ Dominik2000, burası bir soru ve cevap sitesi, forum değil. Bir sorunuz varsa, mevcut bir soruya cevap değil gerçek bir soru göndermelisiniz. Daha fazla ayrıntı için sss'e bakın.
Josh,

Yanıtlar:


19

Oyunlarımızda küresel verilerimizi düzenlemek için kullandığımız bir şey ServiceLocator tasarım modelidir. Bu kalıbın Singleton kalıbına kıyasla avantajı, global verilerinizin uygulamasının uygulama süresi boyunca değişebilmesidir. Ayrıca, genel nesneler çalışma zamanında da değiştirilebilir. Diğer bir avantajı, özellikle C ++ 'ta çok önemli olan global nesnelerinizin başlangıç ​​sırasını yönetmenin daha kolay olmasıdır.

örneğin (C ++ veya Java'ya kolayca çevrilebilen C # kodu)

Diyelim ki, işleme için bazı genel işlemleri olan bir işleme arka arayüzü var.

public interface IRenderBackend
{
    void Draw();
}

Ve varsayılan render arka uç uygulamasına sahip olmanız

public class DefaultRenderBackend : IRenderBackend
{
    public void Draw()
    {
        //do default rendering stuff.
    }
}

Bazı tasarımlarda, global olarak render arka ucuna erişebilmek yasal görünüyor. In Singleton deseni her vasıta IRenderBackend uygulaması benzersiz küresel örneği olarak uygulanmalıdır. Ancak ServiceLocator şablonunu kullanmak buna gerek duymaz.

İşte nasıl:

public class ServiceLocator<T>
{
    private static T currGlobalInstance;

    public static T Service
    {
        get { return currGlobalInstance; }
        set { currGlobalInstance = value; }
    }
}

Global nesnenize erişebilmek için önce onu küçültmeniz gerekir.

//somewhere during program initialization
ServiceLocator<IRenderBackend>.Service = new DefaultRenderBackend();

//somewhere else in the code
IRenderBackend currentRenderBackend = ServiceLocator<IRenderBackend>.Service;

Uygulamaların çalışma zamanı boyunca nasıl değişebileceğini göstermek için, oyununuzda görüntülemenin izometrik olduğu bir mini oyunu olduğunu ve bir IsometricRenderBackend uyguladığını söyleyelim .

public class IsometricRenderBackend : IRenderBackend
{
    void draw()
    {
        //do rendering using an isometric view
    }
}

Mevcut durumdan mini oyun durumuna geçtiğinizde, servis bulucu tarafından sağlanan global render arka ucunu değiştirmeniz yeterlidir.

ServiceLocator<IRenderBackend>.Service = new IsometricRenderBackend();

Başka bir avantaj da boş servisleri kullanabilmenizdir. Biz olsaydı Örneğin, ISoundManager hizmet ve kullanıcı yüzden ayarlayarak, sadece kendi yöntemleri çağrıldığında hiçbir şey yapmaz bir NullSoundManager uygulamak sesini kapatmak istedi ServiceLocator a 'nın hizmet nesnesi NullSoundManager biz elde edebiliriz nesne bu sonuçta neredeyse hiç çalışma yapılmamaktadır.

Özetlemek gerekirse, bazen küresel verileri ortadan kaldırmak mümkün olmayabilir, ancak bu onları düzgün şekilde ve nesne yönelimli şekilde düzenleyemeyeceğiniz anlamına gelmez.


Bunu daha önce inceledim, ancak tasarımlarımın hiçbirine uygulayamadım. Bu sefer planlıyorum. Thank you :)
Awesomania

3
@Erevis Yani, temelde, polimorfik nesneye global referansı tarif ediyorsunuz. Buna karşılık, bu sadece çift ​​dolaylıdır (pointer -> interface -> application). C ++ 'da olduğu gibi kolayca uygulanabilir std::unique_ptr<ISomeService>.
Yağmurda Gölgeler

1
Başlatma stratejisini "ilk erişimde ilklendirme" olarak değiştirebilir ve bazı harici kod dizilerinin yer belirleme ve hizmetleri bulucuya itme zorunluluğundan kaçınabilirsiniz. Hizmetlere "bağlı" bir liste ekleyebilirsiniz, böylece bir ilk başlatıldığında diğer hizmetleri otomatik olarak ayarlayacaktır ve main.cpp'de birisinin bunu hatırladığı için dua etmemelisiniz. Gelecekteki ayarlamalar için esneklik ile iyi bir cevap.
Patrick Hughes,

4

Bir oyun motoru tasarlamanın pek çok yolu var ve bunların hepsi tercihe bağlı.

Temelleri ortadan kaldırmak için, bazı geliştiriciler, bir dizi alt sistemi yaratan, bunlara sahip olan ve başlatan bir çekirdek, çekirdek veya çerçeve sınıfı olarak adlandırılan bazı üst çekirdek sınıfların olduğu bir piramit gibi tasarlamayı tercih ederler. ses, grafik, ağ, fizik, AI ve görev, varlık ve kaynak yönetimi. Genel olarak, bu alt sistemler bu çerçeve sınıfı tarafından size açıktır ve genellikle bu çerçeve sınıfını, uygun durumlarda yapıcı argüman olarak kendi sınıflarınıza geçirirsiniz.

4. seçeneği düşündüğün zaman doğru yolda olduğuna inanıyorum.

İletişimin kendisinde, doğrudan bir fonksiyon çağrısının kendisini ima etmesi gerekmediğine dikkat edin. Kullanmanın Signal and Slotsya da kullanmanın bazı dolaylı yöntemlerle yapılmasına rağmen iletişimin birçok dolaylı yolu vardır Messages.

Bazen oyunlarda, kare hızlarının çıplak gözle akışkan olması için oyun döngüsümüzü olabildiğince hızlı hareket ettirmek için eylemlerin zaman uyumsuz olarak gerçekleşmesine izin vermek önemlidir. Oyuncular yavaş ve dalgalı sahnelerden hoşlanmıyorlar ve bu yüzden onlar için akan şeyleri sürdürmek için yollar bulup mantık akışını devam ettirmekle birlikte kontrol etmeleri ve sipariş etmeleri gerekiyor. Eşzamansız işlemler kendi yerlerine sahip olsalar da her işlemin cevabı da onlar değildir.

Sadece hem senkron hem de asenkron iletişimin bir karışımına sahip olduğunuzu bilin. Uygun olanı seçin, ancak alt sistemlerinizdeki her iki stili de desteklemeniz gerekeceğini biliyorum. Her ikisi için de destek tasarımı size gelecekte iyi hizmet edecektir.


1

Sadece ters veya döngüsel bağımlılık olmadığından emin olmalısınız. Bir sınıfı varsa Örneğin, Coreve bu Corebir sahiptir Levelve Levelbir listesi vardır Entity, o zaman bağımlılık ağacı gibi görünmelidir:

Core --> Level --> Entity

Yani, bu ilk bağımlılık ağacını göz önüne alındığında, sen vermemelilerdi Entitybağlıdır Levelveya Coreve Levelgüvenmemelisiniz Core. Ya olursa Levelveya Entityihtiyaç bağımlılık ağacında daha yukarı var verilere erişim için, bu referans ile bir parametre olarak iletilmelidir.

Aşağıdaki kodu göz önünde bulundurun (C ++):

class Core;
class Entity;
class Level;

class Level
{
    public:
        Level(Core& coreIn) : core(coreIn) {}

        Core& core;
}

class Entity
{
    public:
        Entity(Level& levelIn) : level(levelIn) {}

        Level& level;
}

Bu tekniği kullanarak, her görebiliriz Entityerişimi olan Levelve Levelerişimi vardır Core. Her birinin Entityaynı Level, boşa harcanan belleği sakladığına dikkat edin . Bunu fark ettikten sonra, her birinin Entitygerçekten erişime ihtiyacı olup olmadığını sorgulamalısınız Level.

Tecrübelerime göre, ya A) ters bağımlılıklardan kaçınmak için gerçekten bariz bir çözüm, ya da B) Küresel olaylardan ve tekillerden kaçınmanın olası bir yolu yok.


Bir şey mi eksik? 'Hiçbir zaman Seviyeye bağlı olmamanız gerektiğini' söylüyorsunuz, ancak daha sonra onu 'Varlık (Seviye ve Seviye') olarak tanımlıyorsunuz. Bağımlılığın ref tarafından iletildiğini anlıyorum ama bu hala bir bağımlılık.
Adam Naylor

@AdamNaylor Mesele şu ki, bazen gerçekten ters bağımlılıklara ihtiyaç duyuyorsunuz ve referansları geçerek dünyalardan kaçınabilirsiniz. Genel olarak, bu bağımlılıklardan tamamen kaçınmak en iyisidir ve bunun nasıl yapılacağı her zaman net değildir.
Elmalar

0

Yani, temelde, küresel değişken durumdan kaçınmak ister misiniz ? Yerel, değişmez ya da hiç devlet yapamazsınız. İkincisi, en verimli ve esnektir, imo. Bu ipucu gizleme olarak bilinir.

class ISomeComponent // abstract base class
{
    //...
};

extern ISomeComponent & g_SomeComponent; // will be defined somewhere else;

0

Asıl soru, performanstan ödün vermeden bağlantıyı nasıl azaltacağı ile ilgili. Tüm küresel nesneler (hizmetler) genellikle oyunun çalışma süresi boyunca değişken olan bir tür bağlam oluşturur. Bu anlamda, hizmet bulma modeli, içeriğin farklı kısımlarını uygulamanın farklı kısımlarına yayar, ki bu da istediğiniz gibi olabilir veya olmayabilir. Bir başka gerçek dünya yaklaşımı da böyle bir yapı ilan etmek olacaktır:

struct sEnvironment
{
    owning<iAudio*> m_Audio;
    owning<iRenderer*> m_Renderer;
    owning<iGameLevel*> m_GameLevel;
    ...
}

Ve onu sahip olmayan bir ham işaretçi olarak dolaştır sEnvironment*. Burada işaretçiler arayüzleri işaret eder, böylece kuplaj, servis bulucuya kıyasla benzer şekilde azalır. Bununla birlikte, tüm hizmetler tek bir yerdedir (ki bu iyi olabilir veya olmayabilir). Bu sadece başka bir yaklaşım.

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.