2010-09-04 17 views
11

Sono un po 'sottaceto: dire che sto realizzando un gioco semplice, 2D, simile a Zelda. Quando due oggetti si scontrano, ognuno dovrebbe avere un'azione risultante. Tuttavia, quando il personaggio principale si scontra con qualcosa, la sua reazione dipende esclusivamente dal tipo di oggetto con cui è entrato in collisione. Se è un mostro, dovrebbe riprendersi, se è un muro, non dovrebbe accadere nulla, se è una scatola blu magica con nastri, dovrebbe guarire, ecc. (Questi sono solo esempi).Come scrivere un elegante meccanismo di gestione delle collisioni?

Devo anche notare che ENTRAMBI le cose sono parte della collisione, cioè, gli eventi di collisione dovrebbero accadere sia per il personaggio E il mostro, non solo uno o l'altro.

Come scriveresti codice come questo? Posso pensare a un numero di modi incredibilmente ineleganti, ad esempio, con funzioni virtuali nella classe WorldObject globale, per identificare gli attributi, ad esempio una funzione GetObjectType() restituisce ints, char * s, tutto ciò che identifica l'oggetto come Monster , Box o Wall), quindi in classi con più attributi, ad esempio Monster, potrebbero esserci più funzioni virtuali, ad esempio GetSpecies().

Tuttavia, questo diventa fastidioso per mantenere, e conduce ad un grande interruttore a cascata (o se) nella funzione di gestore di collisione

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

} 

C'è anche la possibilità di utilizzare i file, ei file dovrebbe avere cose come :

Object=Monster 
Species=EvilScaryDog 
Subspecies=Boss 

E quindi il codice può recuperare gli attributi senza la necessità di funzioni virtuali che ingombrano tutto. Questo non risolve il problema se a cascata, tuttavia.

E POI c'è la possibilità di avere una funzione per ogni caso, ad esempio CollideWall(), CollideMonster(), CollideHealingThingy(). Questo è personalmente il mio preferito (anche se sono tutt'altro che simpatici), perché sembra il più ingombrante da mantenere.

Qualcuno potrebbe dare qualche informazione su soluzioni più eleganti a questo problema? Grazie per qualsiasi aiuto!

+0

Newton avrebbe qualche parola per te :) –

risposta

11

Lo farei viceversa, perché se il personaggio si scontra con un oggetto, anche un oggetto si scontra con il personaggio. Così si può avere una classe base Object, in questo modo:

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

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

// etc. for each object 

In generale, in progettazione OOP funzioni virtuali sono l'unica soluzione "corretta" per casi come questo:

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

EDIT:
Dopo aver chiarito, dovrai modificare un po 'quanto sopra.Il MainCharacter avrebbe dovuto sovraccarico metodi per ciascuno degli oggetti che possono collidere con:

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 */ 

In questo modo si notificano il personaggio principale circa la collisione e si può prendere azioni appropriate. Inoltre, se hai bisogno di un oggetto che non dovrebbe notificare al personaggio principale la collisione, puoi semplicemente rimuovere la chiamata di notifica in quella particolare classe.

Questo approccio è chiamato double dispatch.

Vorrei anche considerare di rendere il MainCharacter stesso un Object, spostare i sovraccarichi su Object e utilizzare collideWith anziché collideWithCharacter.

+0

Sono d'accordo, il collisionbehav Ior dipende più dal tipo di oggetto che dall'imagine del maincharacter, quindi gli oggetti specifici dovrebbero implementare il comportamento. Quindi è possibile aggiungere oggetti in seguito senza modificare il carattere maincharacter. Il tuo maincharacter dovrebbe ovviamente esporre un'interfaccia di comportamenti maincharacter (ad esempio rimbalzo (direzione vettoriale), ...) che consente agli oggetti di interagire con il maincharacter. –

+0

Ho appena aggiornato il post originale, come ho dimenticato di chiarire: sia l'oggetto che il personaggio potrebbero avere eventi; non è che solo il personaggio fa qualcosa o solo l'oggetto, ma entrambi lo fanno. – bfops

+1

@Robot: Stai cercando una doppia spedizione. In MainCharacter, scrivi un sovraccarico per i suoi eventi su ogni tipo di oggetto che hai. In Object, scrivi una funzione virtuale che chiama la funzione OnCollision di MainCharacter - normalmente, le persone usano solo * questo. In questo modo, si rigenerano sostanzialmente le informazioni sul tipo in fase di esecuzione. – Puppy

2

Come ottenere tutti gli oggetti collidabili da una classe astratta comune (chiamiamola Collidabile). Quella classe potrebbe contenere tutte le proprietà che possono essere modificate da una collisione e una funzione di HandleCollision. Quando due oggetti si scontrano, basta chiamare HandleCollision su ogni oggetto con l'altro oggetto come argomento. Ogni oggetto manipola l'altro per gestire la collisione. Nessuno dei due oggetti ha bisogno di sapere quale altro tipo di oggetto è stato semplicemente rimbalzato e non si hanno grandi istruzioni di commutazione.

+0

Questa è la configurazione generale che ho ora. Tuttavia, non tutto è in collisione con un mostro reagirà allo stesso modo, ecco perché dico che ho bisogno di sapere che cosa è in collisione, non solo se si verificano o meno collisioni. – bfops

0

Se sto ottenendo il problema in modo corretto, vorrei STH come

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

Fai tutte le entità colidable implementano un'interfaccia (diciamo "Collidable") con un metodo collideWith (Collidable). Poi, su di voi algoritmo di rilevamento di collisione, se si rileva che A si scontra con B, si potrebbe chiamare:

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

Si supponga che A è la MainCharacter e B un mostro e sia implementare l'interfaccia Collidable.

A->collideWith(B); 

chiamerei il seguente:

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

Ciò a sua volta chiama il mostro :: collideWith (MainCharacter) il metodo e si può implementare tutti i comportamenti mostri carattere non:

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

Altre informazioni: Single Dispatch

Spero che sia d'aiuto.

+0

Idea interessante. Come nota a margine, dovrei sottolineare che ci sono alcuni problemi minori con quel codice, ma l'intento è chiaro e conciso. Questo è simile al mio codice attuale, ma invece di avere una funzione di Monster :: CollideWith sovraccaricata per ogni tipo, ne ho solo una simile a quella di MainCharacter. Dovrò provare in questo modo. – bfops

1

Ciò che chiami "una fastidiosa istruzione di commutazione" chiamerei "un grande gioco", quindi sei sulla buona strada.

Avere una funzione per ogni regola di interazione/gioco è esattamente ciò che suggerirei. Lo rende facile da trovare, eseguire il debug, modificare e aggiungere nuove funzionalità:

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

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

... 

Quindi la domanda è davvero come rilevare ciascuno di questi casi. Supponendo che tu abbia una sorta di rilevamento di collisione che si traduce in X e Y collide (semplice come N^2 test di sovrapposizione (hey, funziona per piante vs zombi, e c'è molto da fare!) O complicato come sweep e potare + 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() { ... } 

    ... 

Questo stile, mentre verbose è

  • facile per eseguire il debug
  • facile aggiungere casi
  • vi mostra quando si dispone di incoerenze logiche/progettazione o omissioni "oh wha t se una X è sia un giocatore che un muro a causa dell'abilità "PosessWall", che dire allora!?!" (e quindi consente di sufficiente aggiungere casi per gestire quelli)

fase cella di Spore utilizza esattamente questo stile e dispone di circa 100 controlli con conseguente circa 70 diversi esiti (senza contare le inversioni param). E 'solo un gioco di dieci minuti, questa è una nuova interazione ogni 6 secondi per tutto il palco - ora è il valore del gameplay!

+0

e per quelli di voi incentrati sull'ereditarietà del tipo OO, ricordate che questi test possono essere qualsiasi cosa: IsPlayerFlying(), IsWallMadeOfFire() && WallFireTemperature()> 1000 Kelvin), IsItTuesday() –

Problemi correlati