Oyuncu ve Dünya arasındaki dairesel bağımlılıklar nasıl önlenir?


60

Yukarı, aşağı, sola ve sağa hareket edebileceğiniz bir 2D oyun üzerinde çalışıyorum. Aslında iki oyun mantığı nesnesi var:

  • Oyuncu: Dünyaya göre pozisyonu var
  • Dünya: Haritayı ve oyuncuyu çizer

Şimdiye kadar, Dünya bağlıdır Player oyuncu karakteri çizmek için nereye anlamaya konumunu gerek, (yani bir referansı vardır) ve haritanın hangi bölümü çizmek için.

Şimdi oyuncunun duvarlar arasında hareket etmesini imkansız hale getirmek için çarpışma tespiti eklemek istiyorum.

Düşünebildiğim en basit yol, Oyuncunun Dünya'ya amaçlanan hareketin mümkün olup olmadığını sormasını sağlamaktır. Fakat bu, Oyuncu ve Dünya arasında dairesel bir bağımlılık yaratacaktır (yani her biri diğerine atıfta bulunur); Ben ile geldi tek yol sahip olmaktır Dünya taşımak Oyuncu , ama bu biraz unintuitive bulabilirsiniz.

En iyi seçeneğim nedir? Yoksa buna değmeyecek dairesel bir bağımlılıktan kaçınıyor mu?


4
Neden dairesel bir bağımlılığın kötü bir şey olduğunu düşünüyorsunuz? stackoverflow.com/questions/1897537/…
Fuhrmanator 14:12

@Fuhrmanator Genelde kötü bir şey olduğunu sanmıyorum, ancak kodumda bir tanesini tanıtmak için işleri biraz daha karmaşık hale getirmek zorunda kalacağım.
futlib

Küçük tartışmalarımızdan bahsettim , yeni bir şey değil: yannbane.com/2012/11/… ...
jcora

Yanıtlar:


61

Dünya kendini çizmemelidir; Renderer dünyayı çizmeli. Oyuncu kendini çizmemelidir; Render, Oyuncu’yu Dünya’ya göre çekmelidir.

Oyuncu dünyaya çarpışma tespiti hakkında sorular sormalıdır; veya belki de çarpışmalar, sadece statik dünyaya karşı değil aynı zamanda diğer oyunculara karşı çarpışma tespitini kontrol edecek ayrı bir sınıf tarafından yapılmalıdır.

Bence Dünya muhtemelen Oyuncudan haberdar olmamalı; Tanrı-nesnesi değil, düşük seviyeli bir ilkel olmalıdır. Oyuncunun muhtemelen, dolaylı olarak (çarpışma tespiti veya etkileşimli nesneleri kontrol etme, vb.) Bazı Dünya yöntemlerini çağırması gerekecektir.


25
@ snake5 - "can" ve "should" arasında bir fark var. Her şey olabilir bir şey çizmek - ama çizim ile ilgilenen kodunu değiştirmek gerektiğinde, bu "Renderer" derse girmek yerine çiziyor "şey" aramak için çok daha kolay. “bölümlendirmeye takıntı”, “uyum” için başka bir kelimedir.
Nate

16
@ Bay Bay, hayır, o değil. İyi tasarımı savunuyor. Her şeyi bir sınıfın bir kargaşasında tıkamak hiç mantıklı gelmiyor.
jcora

23
Vay, böyle bir tepki vereceğini düşünmemiştim :) Cevabınıza ekleyecek hiçbir şeyim yok, ama neden verdiğimi açıklayabilirim - çünkü bunun daha basit olduğunu düşünüyorum. 'Uygun' ya da 'doğru' değil. Öyle görünmesini istemedim. Benim için daha kolay çünkü kendimi çok fazla sorumluluğa sahip sınıflarla uğraşırken bulursam, bölme, varolan kodu okunabilir hale getirmeye zorlamaktan daha hızlıdır. Anlayabildiğim topaklardaki kodu ve futlib'in yaşadığı problemlere tepkisizleştirmeyi seviyorum.
Liosan

12
@ snake5 Daha fazla sınıf eklemek, programcı için ek yükler eklemek, deneyimlerime göre tamamen yanlış. Bana göre bilgilendirici isimleri ve iyi tanımlanmış sorumlulukları olan 10x100 satır sınıfları , programcı için okunması kolay ve genel giderler için tek bir 1000 satır tanrı sınıfından daha kolaydır .
Martin

7
Neyin neyin çekildiğine dair bir not olarak, bir Renderertür gerekli, ancak bu, her bir şeyin nasıl yaratıldığının mantığı anlamına gelmiyor, Rendererçizilmesi gereken her şey, muhtemelen gibi ortak bir arayüzden miras almalı IDrawableveya IRenderable(veya kullandığınız dilde arabirim eşdeğeri). Dünya olabilir Renderer, sanırım, ama bu, özellikle zaten bir IRenderablekendisi olsaydı, sorumluluğunu aşmış gibi görünüyor .
zzzzBov

35

Tipik bir renderleme motorunun aşağıdakileri nasıl idare ettiği:

Bir nesnenin uzayda bulunduğu yer ile nesnenin nasıl çizildiği arasında temel bir ayrım vardır.

  1. Bir nesneyi çizmek

    Genelde bunu yapan bir Renderer sınıfınız vardır. Sadece bir nesneyi alır (Model) ve ekranda çizer. DrawSprite (Sprite), drawLine (..), drawModel (Model), ihtiyaç duyduğunuz her neyse gibi yöntemleri olabilir. Bu bir Renderer yani bütün bunları yapması gerekiyordu. Ayrıca, altında bulunan herhangi bir API'yi kullanır, böylece örneğin OpenGL'yi ve DirectX'i kullanan bir işleyiciye sahip olabilirsiniz. Oyununuzu başka bir platforma taşımak istiyorsanız, sadece yeni bir işleyici yazın ve onu kullanın. Bu kadar kolay.

  2. Bir nesneyi taşıma

    Her nesne bir SceneNode olarak adlandırmayı sevdiğimiz bir şeye bağlıdır . Bunu kompozisyon aracılığıyla elde edersiniz. Bir SceneNode bir nesneyi içerir. Bu kadar. SceneNode nedir? Bir nesnenin (genellikle başka bir SceneNode'a göre) tüm dönüşümlerini (konum, rotasyon, ölçek) asıl nesneyle birlikte içeren basit bir sınıftır.

  3. Nesneleri yönetme

    SceneNodes nasıl yönetilir? Bir SceneManager aracılığıyla . Bu sınıf, sahnenizdeki her SceneNode öğesini oluşturur ve izler. Belirli bir SceneNode (genellikle "Player" veya "Table" gibi bir dize adıyla tanımlanır) veya tüm düğümlerin bir listesini isteyebilirsiniz.

  4. Dünya çizme

    Bu şimdiye kadar oldukça açık olmalı. Sahnedeki her SceneNode'da yürüyün ve Renderer'in doğru yere çizmesini sağlayın. Oluşturucuya, bir nesnenin dönüşümlerini oluşturmadan önce kaydetmesini sağlayarak doğru yerde çizebilirsiniz.

  5. Çarpışma algılama

    Bu her zaman önemsiz değildir. Genellikle sahneyi uzayda hangi nesnenin belirli bir noktada olduğu veya bir ışın hangi nesnelerle kesiştiği hakkında sorgulayabilirsiniz. Bu şekilde, oynatıcınızdan hareket yönünde bir ışın oluşturabilir ve sahne yöneticisine, ışığın kesiştiği ilk nesnenin ne olduğunu sorabilirsiniz. Daha sonra, oynatıcıyı yeni pozisyona getirmeyi seçebilir, onu daha küçük bir miktarda hareket ettirebilir (çarpışan nesnenin yanında tutmak için) veya hiç hareket ettirmeyebilirsiniz. Bu sorguların ayrı sınıflar tarafından ele alındığından emin olun. SceneManager'dan bir SceneNodes listesi istemelidirler, ancak SceneNode'un uzayda bir noktayı kaplayıp kapamadığını veya bir ışınla kesişmesini belirlemek başka bir görevdir. SceneManager'ın sadece düğümler oluşturup sakladığını unutmayın.

Peki, oyuncu nedir ve dünya nedir?

Player, sırayla oluşturulacak modeli içeren bir SceneNode içeren bir sınıf olabilir. Oynatıcıyı sahne düğümünün konumunu değiştirerek hareket ettirirsiniz. Dünya sadece SceneManager'ın bir örneğidir. Tüm nesneleri (SceneNodes aracılığıyla) içerir. Sahnenin şu anki durumu hakkında sorgulamalar yaparak çarpışma algılamayı ele alıyorsunuz.

Bu kadar en motorları içeride ne tam veya doğru bir açıklama olmaktan, ama sen temellerini anlamalarına yardımcı olmalı ve altını çizdiği cepten ilkelere saygı neden önemli olduğunu KATI . Kodunuzu yeniden yapılandırmanın çok zor olduğu veya size gerçekten yardımcı olamayacağı fikrine kendinizi bırakmayın. Kodunuzu dikkatlice tasarlayarak gelecekte çok daha fazla kazanacaksınız.


+1 - Oyun sistemimi böyle bir şey inşa ederken buldum ve oldukça esnek buldum.
Cypher

+1, harika cevap. Benimkinden daha somut ve konuya.
jcora

+1, bu cevaptan çok şey öğrendim ve hatta ilham verici bir son aldı. Thanks @rootlocus
joslinm 13:12

16

Neden bundan kaçınmak istiyorsun? Yeniden kullanılabilir bir sınıf yapmak istiyorsanız, dairesel bağımlılıklardan kaçınılmalıdır. Ancak Oyuncu tekrar kullanılabilir olması gereken bir sınıf değildir. Player'ı hiç bir dünya olmadan kullanmak ister miydiniz? Muhtemelen değil.

Sınıfların işlevsellik koleksiyonlarından başka bir şey olmadığını unutmayın. Asıl soru, işlevselliğin nasıl bölündüğüdür. Ne yapman gerekiyorsa yap. Dairesel bir çöküşe ihtiyacınız varsa, o zaman öyle olsun. (Bu arada herhangi bir OOP özelliği için de geçerli. Bir şeyi bir amaca hizmet edecek şekilde kodlayın, sadece paradigmaları kör bir şekilde takip etmeyin.)

Düzenleme
Tamam, soruyu cevaplamak için: Geri aramaları kullanarak Oyuncunun Dünyayı çarpışma kontrolleri ile tanıştırması gerekebilir.

World::checkForCollisions()
{
  [...]
  foreach(entityA in entityList)
    foreach(entityB in entityList)
      if([... entityA and entityB have collided ...])
         entityA.onCollision(entityB);
}

Player::onCollision(other)
{
  [... react on the collision ...]
}

Eğer soruların içinde tanımladığınız fiziğin türü, varlıkların hızını ortaya çıkarırsanız, dünya tarafından ele alınabilir:

World::calculatePhysics()
{ 
  foreach(entityA in entityList)
    foreach(entityB in entityList)
    {
      [... move entityA according to its velocity as far as possible ...]
      if([... entityA has collided with the world ...])
         entityA.onWorldCollision();
      [... calculate the movement of entityB in order to know if A has collided with B ...]
      if([... entityA and entityB have collided ...])
         entityA.onCollision(entityB);
    }
}

Bununla birlikte, muhtemelen er ya da geç dünyaya bağımlı olmanız gerekeceğini, yani Dünya'nın işlevselliğine ihtiyaç duyduğunuzda: en yakın düşmanın nerede olduğunu bilmek ister misiniz? Bir sonraki çıkıntının ne kadar uzakta olduğunu bilmek ister misin? Bağımlılık öyle.


4
+1 Dairesel bağımlılık burada gerçekten bir sorun değil. Bu aşamada bunun için endişelenmek için bir neden yok. Eğer oyun büyürse ve kod olgunlaşırsa, bu Player ve Dünya sınıflarını yine de alt sınıflarda yeniden canlandırmak, uygun bir bileşen tabanlı sisteme, girdi işleme için sınıflara, belki de Render'e vb. Sahip olmak iyi bir fikir olacaktır. başlangıç, problem yok.
Laurent Couvidou 13:12

4
-1, bu kesinlikle dairesel bağımlılıkları ortaya koymamak için tek neden değil. Onları tanıtarak, sisteminizi genişletmeyi ve değiştirmeyi kolaylaştırırsınız.
jcora

4
@Bane Yapıştırıcı olmadan hiçbir şeyi kodlayamazsınız. Aradaki fark, ne kadar dolaylı aktarım eklediğinizdir. Oyun -> Dünya -> Varlık sınıfları varsa veya Oyun -> Dünya, SoundManager, InputManager, PhysicsEngine, ComponentManager sınıflarına sahipseniz. Tüm (sözdizimsel) ek yükler ve dolaylı karmaşıklık nedeniyle işleri daha az okunabilir kılar. Ve bir noktada, birbirleriyle etkileşime geçmek için bileşenlere ihtiyacınız olacak. Ve bu, bir tutkal sınıfının işleri birçok sınıf arasında bölünmüş her şeyden daha kolay hale getirdiği nokta.
API-Beast,

3
Hayır, hedefleri hareket ettiriyorsun. Elbette bir şeyler aramalı render(World). Tartışma, tüm kodun bir sınıf içinde sıkıştırılması gerekip gerekmediği veya kodun daha sonra bakımı, genişletilmesi ve yönetilmesi daha kolay olan mantıksal ve işlevsel birimlere bölünmesi gerekip gerekmediği ile ilgilidir. BTW, bu bileşen yöneticilerini, fizik motorlarını ve girdi yöneticilerini, tamamen farklılaşmamış ve tamamen birleştiğinde yeniden kullanmanın iyi şansı.
jcora

1
@Bane Nesneleri mantıksal parçalara bölmenin, yeni sınıflar ortaya koymaktan başka yolları vardır, btw. Yeni işlevler ekleyebilir veya dosyalarınızı yorum bloklarıyla ayrılmış birden çok bölüme ayırabilirsiniz. Sadece basit tutmak, kodun bir karışıklık olacağı anlamına gelmez.
API-Beast,

13

Mevcut tasarımınız, SOLID tasarımının ilk prensibine aykırı görünüyor .

“Tek sorumluluk ilkesi” olarak adlandırılan bu ilk prensip, genellikle tasarımınıza daima zarar verecek tek parça, her şeyi yapan nesneler yaratmamak için izlenmesi güzel bir rehberdir.

Somutlaştırmak için, amacınız Worldhem oyun durumunu güncellemekten hem de sürdürmekten ve her şeyi çizmekten sorumludur.

Render kodunuz değişirse / değişirse ne olur? Gerçekleştirmeyle ilgisi olmayan her iki sınıfı da neden güncellemelisiniz? Liosan'ın zaten söylediği gibi, sahip olmalısın Renderer.


Şimdi, asıl sorunuzu cevaplamak için ...

Bunu yapmanın birçok yolu vardır ve bu ayrılmanın sadece bir yoludur:

  1. Dünya bir oyuncunun ne olduğunu bilmiyor.
    • ObjectAncak, oyuncunun bulunduğu bir listeye sahiptir, ancak oyuncu sınıfına bağlı değildir (bunu elde etmek için mirası kullanın).
  2. Oyuncu bazıları tarafından güncellenir InputManager.
  3. Dünya, uygun fiziksel değişiklikleri uygulayarak ve nesnelere güncelleme göndererek hareketi ve çarpışma algılamayı işler.
    • Örneğin, nesne A ve nesne B çarpışırsa, dünya onları bilgilendirir ve sonra kendi başlarına halledebilirler.
    • Dünya hala fiziği idare eder (tasarımınız böyle ise).
    • Sonra, her iki nesne de çarpışmanın kendilerini ilgilenip ilgilenmediğini görebilir. Örneğin, A nesnesi oyuncuysa ve B nesnesi ani bir çiviyse, oyuncu kendisine hasar verebilir.
    • Bununla birlikte, bu başka yollarla da çözülebilir.
  4. RendererTüm nesneleri çizer.

Dünyanın bir oyuncunun ne olduğunu bilmediğini söylüyorsunuz, ancak çarpışan nesnelerden biriyse, oyuncunun özelliklerini bilmesi gereken çarpışma algılamayı işler.
Markus von Broady

Kalıtım, dünya genel olarak tarif edilebilecek bazı nesnelerin farkında olmalıdır. Sorun, dünyanın sadece oyuncuya referans vermesi değil, ona bir sınıf olarak bağlı olabileceği şeklinde değil (yani healthsadece bu örneğinin sahip olduğu alanları kullanın Player).
jcora

Ah, yani dünyanın oyuncuya referansı yok, sadece ICollidable arayüzü uygulayan nesneler var, gerekirse oyuncu ile birlikte.
Markus von Broady

2
+1 İyi cevap. Ancak: "lütfen iyi yazılım tasarımının önemli olmadığını söyleyenleri yoksayın". Yaygın. Bunu kimse söylemedi.
Laurent Couvidou

2
Düzenlenen! Yine de gereksiz görünüyordu ...
jcora

1

Oyuncu, çarpışma tespiti gibi şeyleri Dünya'ya sormalıdır. Döngüsel bağımlılıktan kaçınmanın yolu, Dünya'nın Oyuncuya bağımlı olması değildir. Dünyanın kendisini nerede çizdiğini bilmesi gerekiyor: Muhtemelen daha sonra soyutlanmasını, belki de takip etmesi gereken bazı Varlıklar için bir referans alabilen bir Kamera nesnesine bir referansla.

Dairesel referanslar açısından kaçınmak istediğiniz şey, birbirinize çok fazla başvuru yapmak değil, açıkça kodda birbirlerine atıfta bulunmaktır.


1

İki farklı nesne türü ne zaman birbirlerine sorabilir. Metotlarını çağırmak için diğerine referansta bulunmaları gerektiği için birbirlerine bağlı olacaklar.

Dünyaya Oyuncuya sormasını sağlayarak döngüsel bağımlılığı önleyebilirsiniz, ancak Oyuncu Dünyaya soramaz veya tam tersi. Bu şekilde Dünya Oyunculara atıfta bulunur, ancak oyuncuların Dünya'ya atıfta bulunmaları gerekmez. Ya da tam tersi. Fakat bu sorunu çözmeyecek, çünkü Dünya oyunculara soracak bir şeyleri olup olmadığını sormaları ve bir sonraki çağrıda onlara söylemeleri gerekecek ...

Bu yüzden gerçekten bu "sorun" etrafında çalışamazsınız ve bunun için endişelenmenize gerek olmadığını düşünüyorum. Tasarımı aptalca olabildiğince basit tut.


0

Oyuncu ve dünyayla ilgili detayları çıkarırken, iki nesne arasında dairesel bir bağımlılık ortaya koymak istememenin basit bir vakası vardır (dilinize bağlı bile olsa, Fuhrmanator'un yorumundaki bağlantıya bakınız). Bu ve benzeri sorunlara uygulanabilecek en az iki çok basit yapısal çözüm vardır:

1) tanıtılması tekil Dünyanızı sınıfa deseni . Bu, oyuncunun (ve diğer tüm nesnelerin) dünya nesnesini pahalı aramalar veya kalıcı bağlantılar olmadan kolayca bulmasını sağlar. Bu modelin özü, sadece sınıfın nesnenin başlatılması üzerine kurulu ve silinmesiyle silinen, o sınıfın tek örneği için statik bir referansa sahip olmasıdır.

Geliştirme dilinize ve istediğiniz karmaşıklığa bağlı olarak, bunu bir üst sınıf veya arayüz olarak kolayca uygulayabilir ve projenizde birden fazla olmasını beklemeyeceğiniz birçok ana sınıf için kullanabilirsiniz.

2) Geliştirdiğiniz dil destekliyorsa (çoğu kişi yapar), Zayıf Bir Referans kullanın . Bu, çöp toplama gibi şeyleri etkilemeyen bir referanstır. Tam olarak bu durumlarda kullanışlıdır, referans olarak kullandığınız nesnenin hala var olup olmadığı hakkında herhangi bir varsayımda bulunmamaya dikkat edin.

Özel bir durumda, Oyuncu (lar) dünyaya zayıf bir referans tutabilir. Bunun yararı (singleton'da olduğu gibi), her kareyi bir şekilde dünya nesnesini aramaya gitmenize gerek kalmaması veya çöp toplama gibi dairesel referanslardan etkilenen işlemleri engelleyecek kalıcı bir referansa sahip olmanız gerekmemesidir.


0

Diğerleri söylediler, ben senin düşünüyorum Worldo çalışıyor: çok fazla bir şey yapıyor hem oyun içerirler Map(ayrı bir varlık olmalıdır) ve bir olmak Rendereraynı zamanda.

Böylece yeni bir nesne oluşturun ( GameMapmuhtemelen adı verilir ) ve harita seviyesi verilerini içinde saklayın. Mevcut harita ile etkileşime giren fonksiyonları yazın.

O zaman bir nesneye de ihtiyacınız var Renderer. Sen olabilir bu hale Renderernesnesi hem şey içeriyor GameMap ve Player(aynı zamanda Enemies) ve bunları da çizer.


-6

Değişkenleri üye olarak ekleyerek döngüsel bağımlılıklardan kaçınabilirsiniz. Oynatıcı veya bunun gibi bir şey için statik bir CurrentWorld () işlevi kullanın. Zaten Dünya'da uygulanmış olandan farklı bir arayüz icat etmeyin, bu tamamen gereksizdir.

Dairesel referansların yol açtığı problemleri etkin bir şekilde durdurmak için, oynatıcı nesnesini tahrip etmeden önce / bu esnada referansı imha etmek de mümkündür.


1
Seninleyim. OOP çok abartılıyor. Öğreticiler ve eğitim, temel kontrol akışını öğrendikten sonra hızla OO'ya atlar. OO programları prosedür kodlarından genellikle daha yavaştır, çünkü nesneleriniz arasında bürokrasi olduğundan, birçok önbellek erişimine sahip olursunuz, bu da önbellek eksikliğinin yüklenmesine neden olur. Oyununuz çok yavaş çalışıyor. Önbellek kayıplarını önlemek için her şey için düz küresel diziler ve elle optimize edilmiş, ince ayarlanmış işlevler kullanan gerçek, çok hızlı ve zengin özelliklere sahip oyunlar. Bu da performansta on kat artışa neden olabilir.
Calmarius
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.