2010-09-04 8 views
11

Jestem trochę wybredny: mówię, że tworzę prostą, 2D, podobną do Zelda grę. Gdy zderzają się dwa obiekty, każdy powinien otrzymać wynikową akcję. Jednakże, gdy główny bohater zderza się z czymś, jego reakcja zależy wyłącznie od rodzaju obiektu, z którym się zderzył. Jeśli to potwór, powinien się odbić, jeśli jest to ściana, nic nie powinno się wydarzyć, jeśli to magiczne niebieskie pudełko z wstążkami, powinien go leczyć, itd. (To tylko przykłady).Jak napisać elegancki mechanizm do obsługi kolizji?

Muszę również zauważyć, że OBUŻE rzeczy są częścią kolizji, to znaczy zderzenia kolizji powinny mieć miejsce zarówno dla postaci, jak i potwora, a nie tylko dla jednego i drugiego.

Jak napisać kod w ten sposób? Mogę wymyślić wiele niesamowicie nieeleganckich sposobów, na przykład posiadanie funkcji wirtualnych w globalnej klasie WorldObject, w celu identyfikacji atrybutów - na przykład funkcji GetObjectType() (zwraca znaki, char * s, wszystko, co identyfikuje obiekt jako Monster , Box lub Wall), a następnie w klasach o większej liczbie atrybutów, powiedzmy Monster, może być więcej funkcji wirtualnych, powiedzmy GetSpecies().

to jednak staje się irytujące do utrzymania, i prowadzi do dużego przełącznika kaskadowego (lub) oświadczenie obsługi kolizji

MainCharacter::Handler(Object& obj) 
{ 
    switch(obj.GetType()) 
    { 
     case MONSTER: 
     switch((*(Monster*)&obj)->GetSpecies()) 
     { 
      case EVILSCARYDOG: 
      ... 
      ... 
     } 
     ... 
    } 

} 

Istnieje również możliwość korzystania z plików, a pliki miałby takie rzeczy :

Object=Monster 
Species=EvilScaryDog 
Subspecies=Boss 

A następnie kod może pobrać atrybuty bez potrzeby funkcji wirtualnych zaśmiecających wszystko. Nie rozwiązuje to jednak problemu kaskadowego Jeśli jednak.

I NASTĘPNIE istnieje opcja posiadania funkcji dla każdego przypadku, powiedzmy CollideWall(), CollideMonster(), CollideHealingThingy(). To jest osobiście moja najmniej ulubiona (choć daleko im do sympatii), ponieważ wydaje się najbardziej nieporęczna w utrzymaniu.

Czy ktoś mógłby podać wgląd w bardziej eleganckie rozwiązania tego problemu? Dzięki za wszelką pomoc!

+0

Newton miałby dla ciebie kilka słów :) –

Odpowiedz

11

Zrobiłbym to na odwrót - ponieważ jeśli postać koliduje z obiektem, obiekt również koliduje z postacią. W ten sposób można mieć obiektu klasy bazowej, tak:

class Object { 
    virtual void collideWithCharacter(MainCharacter&) = 0; 
}; 

class Monster : public Object { 
    virtual void collideWithCharacter(MainCharacter&) { /* Monster collision handler */ } 
}; 

// etc. for each object 

Generalnie w projektowaniu OOP funkcje wirtualne są tylko „poprawne” rozwiązanie dla spraw tak:

switch (obj.getType()) { 
    case A: /* ... */ break; 
    case B: /* ... */ break; 
} 

EDIT:
Po wyjaśnieniu będziesz musiał nieco zmienić powyższe.MainCharacter powinny były przeciążone metody dla każdego z obiektów może kolidujących z:

class MainCharacter { 
    void collideWith(Monster&) { /* ... */ } 
    void collideWith(EvilScaryDog&) { /* ... */ } 
    void collideWith(Boss&) { /* ... */ } 
    /* etc. for each object */ 
}; 

class Object { 
    virtual void collideWithCharacter(MainCharacter&) = 0; 
}; 

class Monster : public Object { 
    virtual void collideWithCharacter(MainCharacter& c) 
    { 
    c.collideWith(*this); // Tell the main character it collided with us 
    /* ... */ 
    } 
}; 

/* So on for each object */ 

ten sposób powiadomić głównego bohatera o kolizji i może podjąć odpowiednie działania. Jeśli potrzebujesz obiektu, który nie powinien powiadamiać głównego bohatera o kolizji, możesz po prostu usunąć wywołanie powiadomienia w tej konkretnej klasie. Ta metoda nazywa się double dispatch.

Chciałbym również rozważyć dokonywania MainCharacter sama jest Object przesuń przeciążeń do Object i używać collideWith zamiast collideWithCharacter.

+0

Zgadzam się, kolizja i lub zależy bardziej od rodzaju obiektu niż od głównego obiektu imho, więc konkretne obiekty powinny implementować zachowanie. Następnie możesz dodać obiekty później bez zmiany głównego znaku. Twój główny znak powinien jawnie odsłonić interfejs głównych elementów (np. Odbicia (kierunek wektora), ..), który pozwala obiektom wchodzić w interakcje z głównym znacznikiem. –

+0

Właśnie zaktualizowałem pierwotny wpis, ponieważ zapomniałem wyjaśnić: zarówno obiekt, jak i postać mogą mieć zdarzenia; nie jest tak, że tylko postać robi coś lub tylko obiekt robi, ale oba robią. – bfops

+1

@Robot: Szukasz podwójnej wysyłki. W MainCharacter, napisz przeciążenie dla swoich zdarzeń na każdym typie obiektu, który posiadasz. W Object, napisz wirtualną funkcję, która wywoła funkcję OnCollision MainCharacter - normalnie, ludzie właśnie używają * tego. W ten sposób zasadniczo zregenerujesz informacje o typie w czasie wykonywania. – Puppy

2

Co powiesz na wyprowadzenie wszystkich możliwych do zbuforowania obiektów z jednej wspólnej klasy abstrakcyjnej (nazwijmy to Collidable). Ta klasa może zawierać wszystkie właściwości, które można zmienić za pomocą funkcji Collision i jedną funkcję HandleCollision. Gdy zderzają się dwa obiekty, po prostu wywołaj HandleCollision na każdym obiekcie, a drugi obiekt jako argument. Każdy obiekt manipuluje drugim, aby poradzić sobie z kolizją. Żaden z obiektów nie musi wiedzieć, do jakiego typu obiektu się on po prostu wkradł i nie ma żadnych dużych instrukcji przełączania.

+0

To jest ogólne ustawienie, które mam teraz, ale nie wszystko koliduje z Potwór zareaguje w ten sam sposób, dlatego mówię, że muszę wiedzieć, co się koliduje, a nie tylko to, czy zdarzają się kolizje, czy nie, – bfops

0

Jeżeli jestem coraz problem prawidłowo, chciałbym sth jak

Class EventManager { 
// some members/methods 
handleCollisionEvent(ObjectType1 o1, ObjectType2 o2); 
// and do overloading for every type of unique behavior with different type of objects. 
// can have default behavior as well for unhandled object types 
} 
1

Dodać wszystkie podmioty colidable implementować interfejs (powiedzmy "Collidable") za pomocą metody collideWith (Collidable). Następnie na ciebie algorytmu detekcji kolizji, jeśli wykryje, że A zderza się z B, to nazwałbym:

A->collideWith((Collidable)B); 
B->collideWith((Collidable)A); 

Załóżmy, że A jest MainCharacter i B potworem i zarówno wdrożenia Collidable interfejs.

A->collideWith(B); 

nazwałbym następujące:

MainCharacter::collideWith(Collidable& obj) 
{ 
    //switch(obj.GetType()){ 
    // case MONSTER: 
    // ... 
    //instead of this switch you were doing, dispatch it to another function 
    obj->collideWith(this); //Note that "this", in this context is evaluated to the 
    //something of type MainCharacter. 
} 

To z kolei nazywają Monster :: collideWith (MainCharacter) metody i można wdrożyć wszystkie zachowania potwór-znakowy tam:

Monster::CollideWith(MainCharacter mc){ 
    //take the life of character and make it bounce back 
    mc->takeDamage(this.attackPower); 
    mc->bounceBack(20/*e.g.*/); 
} 

Więcej informacji: Single Dispatch

Mam nadzieję, że to pomaga.

+0

Interesujący pomysł. Na marginesie chciałbym zauważyć, że są pewne drobne problemy z tym kodem, ale intencja jest jasna i zwięzła. Jest to podobne do mojego obecnego kodu, ale zamiast przeciążać funkcję Monster :: CollideWith dla każdego typu, po prostu mam taki, który jest podobny do tego w MainCharacter. Muszę to wypróbować w ten sposób. – bfops

1

To, co nazywacie "denerwującą instrukcją przełącznika", nazwałbym "świetną grą", więc jesteś na dobrej drodze.

Posiadanie funkcji dla każdej interakcji/reguły gry jest dokładnie tym, co sugerowałbym. To sprawia, że ​​łatwo znaleźć, debugowanie, zmiany i dodać nową funkcjonalność:

void PlayerCollidesWithWall(player, wall) { 
    player.velocity = 0; 
} 

void PlayerCollidesWithHPPotion(player, hpPoition) { 
    player.hp = player.maxHp; 
    Destroy(hpPoition); 
} 

... 

Więc pytanie jest naprawdę, jak wykrywać każdy z tych przypadków. Zakładając, że masz jakieś wykrywanie kolizji, które powoduje kolizje X i Y (tak proste, jak testy N^2 nakładania się (hej, działa to na rośliny vs zombie, i to się dzieje dużo!) Lub tak skomplikowane, jak zamiatanie i przycinanie + GJK)

void DoCollision(x, y) { 
    if (x.IsPlayer() && y.IsWall()) { // need reverse too, y.IsPlayer, x.IsWall 
    PlayerCollidesWithWall(x, y); // unless you have somehow sorted them... 
    return; 
    } 

    if (x.IsPlayer() && y.IsPotion() { ... } 

    ... 

Ten styl, natomiast gadatliwy jest

  • łatwy do debugowania
  • łatwo dodać przypadki
  • pokazuje, kiedy trzeba nieścisłości logiczne/projektowe lub przeoczeń „oh wha t jeśli X jest zarówno graczem jak i ścianą ze względu na zdolność "PosessWall" , co wtedy!?!" (a następnie pozwala po prostu dodać Szafy obsłużyć tych)

stadium komórek zarodników używa dokładnie tego stylu i ma około 100 kontrole wynikające w około 70 różnych efektów (nie licząc odwrócenia param). To tylko dziesięciominutowa gra to 1 nowa interakcja co 6 sekund na całej scenie - teraz ta wartość gry!

+0

i dla tych z was, którzy koncentrowali się na rzeczach dziedziczenia typu OO, pamiętajcie, że ten test może być dowolny: IsPlayerFlying(), IsWallMadeOfFire() & & WallFireTemperature()> 1000 Kelwinów), IsItTuesday() –