2010-05-04 6 views
22

Ho due classi; chiamiamoli Ogre e Wizard. (Tutti i campi sono pubblici per rendere l'esempio più facile da digitare.)Utilizzo dell'ereditarietà e del polimorfismo per risolvere un problema di gioco comune

public class Ogre 
{ 
    int weight; 
    int height; 
    int axeLength; 
} 

public class Wizard 
{ 
    int age; 
    int IQ; 
    int height; 
} 

In ogni classe posso creare un metodo chiamato, per esempio, la battaglia() che determinerà chi vincerà se un Ogre incontra e Ogre o un mago incontra un mago. Ecco un esempio. Se un orco incontra un orco, il più pesante vince. Ma se il peso è lo stesso, vince quello con l'ascia più lunga.

public Ogre battle(Ogre o) 
{ 
    if (this.height > o.height) return this; 
    else if (this.height < o.height) return o; 
    else if (this.axeLength > o.axeLength) return this; 
    else if (this.axeLength < o.axeLength) return o; 
    else return this; // default case 
} 

Possiamo creare un metodo simile per Wizards.

E se un mago incontra un orco? Ovviamente potremmo fare un metodo per questo, confrontando, diciamo, solo le altezze.

public Wizard battle(Ogre o) 
{ 
    if (this.height > o.height) return this; 
    else if (this.height < o.height) return o; 
    else return this; 
} 

E ne faremmo uno simile per gli Ogri che incontrano il Wizard. Ma le cose sfuggono di mano se dobbiamo aggiungere più tipi di caratteri al programma.

Questo è il punto in cui mi blocco. Una soluzione ovvia è creare una classe di caratteri con i tratti comuni. Ogre e Wizard ereditano dal personaggio e lo estendono per includere gli altri tratti che definiscono ciascuno.

public class Character 
{ 
    int height; 

    public Character battle(Character c) 
    { 
    if (this.height > c.height) return this; 
    else if (this.height < c.height) return c; 
    else return this; 
    } 
} 

C'è un modo migliore per organizzare le lezioni? Ho esaminato il modello di strategia e il modello di mediazione, ma non sono sicuro di come uno di essi (se presente) potrebbe aiutare qui. Il mio obiettivo è raggiungere una sorta di metodo di battaglia comune, in modo che se un Ogre incontra un Ogre usa la battaglia Ogre-vs-Ogre, ma se un Ogre incontra un Wizard, ne usa uno più generico. Inoltre, cosa succede se i personaggi che si incontrano non condividono tratti comuni? Come possiamo decidere chi vince una battaglia?

Modifica: un sacco di grandi risposte! Devo digerirli e capire quale funziona meglio per la mia situazione.

+0

Quindi vuoi che tutti i 'caratteri' abbiano un gruppo di campi' int', di cui tutti i tratti comuni devono essere confrontati in un certo ordine? – Eric

+2

È possibile inserire una magia/più booleana magica nella classe della procedura guidata –

+0

Si è obbligati ad avere un tipo comune per Ogre e Wizard. Quando li hai confrontati direttamente in pubblico Wizard battle (Ogre o), ad un certo punto si restituisce "o" di tipo Ogre e il metodo si aspetta un Wizard .. A volte naturalmente si viene spinti ad avere una buona gerarchia di oggetti. Ma non è così facile vedere quel fenomeno nei nostri progetti quotidiani. E bella domanda tbw :) –

risposta

4

Che dire separare la logica di scontro nella propria classe, con metodi come

Battle(Ogre ogre, Wizard wizard) 

Che sarebbe restituire un oggetto che contiene il vincitore (o la vincitrice in sé, a prescindere). Ciò separare la logica di battaglia dai combattenti, e consentono anche di generercize, vale a dire:

Battle(Creature creat1, Creature creat2) 

sarebbe un metodo di ripiego per qualsiasi abbinamento creatura (supponendo Wizard/Ogre/etc tutti hanno 'Creature' come base classe) che non ha una logica specifica. Questo ti permetterebbe di aggiungere/modificare/rimuovere la logica di battaglia senza modificare alcuna delle creature stesse.

+5

Il problema con questo approccio è che Java guarderà al tipo in fase di compilazione per determinare quale versione del metodo 'Battle' chiamare. Ad esempio: 'Creatura c1 = nuovo Ogre(); Creatura c2 = nuova procedura guidata(); Battle (c1, c2); 'chiamerebbe' Battle (Creature, Creature) 'e ** not **' Battle (Ogre, Wizard) '. – Skrud

+0

-1 Crea un accoppiamento elevato. – OscarRyz

2

Un modo per farlo sarebbe quello di fare una nuova interfaccia per tutti i tipi di personaggio come

public interface Fightable 
{ 
    public Fightable doBattle(Fightable b); 
} 

E poi da lì si sarebbe implementare doBattle in ogni classe. Ad esempio, nella classe Ogre, puoi verificare se b fosse un'istanza di Ogre (nel qual caso fai una cosa), una procedura guidata (nel qual caso un'altra), ecc ...

Il problema è che ogni Dopo aver aggiunto un nuovo tipo, è necessario aggiungere codice a ogni singola classe di caratteri a ogni altro carattere, che non è particolarmente gestibile. Inoltre dovresti insistere per assicurarti che le operazioni siano state mantenute correttamente, ad es.se hai cambiato il metodo doBattle nella classe Ogre rispetto a Wizard ma non nella classe Wizard rispetto ad Ogres, potresti avere una situazione in cui il risultato è diverso se chiami anOgre.doBattle (aWizard) o unWizard.doBattle (anOgre) .

Quello che potrebbe essere meglio sarebbe creare una classe di battaglia che accetta due personaggi e contiene la logica per combattere vedendo quali sono stati passati due tipi di classe: in questo modo, è sufficiente cambiare la classe di battaglia ogni volta è stato aggiunto un nuovo tipo di personaggio! Vuoi incapsulare il comportamento che è più probabile che cambi più spesso.

+0

codice riformattato; per favore, ripristina se non è corretto. – trashgod

+0

Questo approccio è utile se alcuni personaggi non implementano 'Fightable' ma implementano' Communicative', ecc. Puoi anche, astrarre i parametri di input della tua funzione di risultato e organizzare per ogni personaggio implementatore di restituire un valore basato sul suo proprio caratteristiche. – trashgod

+0

Sembra meglio, grazie :) –

5

Suoni come desideri double dispatch.

Fondamentalmente il tuo Ogre e Wizard avrebbe (probabilmente) una base comune. Quando si chiama il metodo battle dalla base Ogre, esso prende la base di Wizard e richiama un'altra funzione su quella base che utilizza lo Ogre come argomento. Il comportamento polimorfico su entrambe le chiamate di funzione fornisce efficacemente il polimorfismo sui due tipi simultaneamente.

+1

-1 Creerai una dipendenza da ciascuna classe l'una dall'altra. L'accoppiamento alto non è desiderabile. – OscarRyz

+0

@Oscar: non crea dipendenze tra le classi. Ogni classe semplicemente sovraccarica un metodo per ogni tipo per cui desidera specificare il comportamento. La logica effettiva potrebbe essere rimandata a una funzione da qualche altra parte che accetta entrambi i tipi. A quanto ho capito, l'OP stava cercando un modo per prendere due oggetti 'Character' generali e semplicemente chiamare un metodo come' battle' su uno di essi e fare Do The Right Thing che fa questo. – Troubadour

19

Il visitor pattern "è un modo di separare un algoritmo da una struttura di oggetto su cui opera".

Per esempio, si può avere

class Character { 
    boolean battle(BattleVisitor visitor) { 
     return visitor.visit(this); 
    } 
} 

class Ogre extends Character {..} 
class Wizard extends Character {..} 
class Dwarf extends Character {..} 

interface BattleVisitor { 
    boolean visit(Ogre character); 
    boolean visit(Wizard character); 
    boolean visit(Dwarf character); 
} 

class OgreBattleVisitor implements BattleVisitor { 
    private Ogre ogre; 
    OgreBattleVisitor(Ogre ogre) { this.ogre = ogre; } 
    boolean visit(Ogre ogre) { 
     // define the battle 
    } 

    boolean visit(Wizard wizard) { 
     // define the battle 
    } 
    ... 
} 

E ogni volta che si verifica una lotta:

targetChar.battle(new OgreBattleVisitor(ogre)); 

Definire un'implementazione visitatore per una procedura guidata e un nano e ciò che appare. Si noti inoltre che definisco il risultato del metodo visit come boolean (vinto o perso) anziché restituire il vincitore.

Così, quando si aggiungono nuovi tipi, si dovrà aggiungere:

  • un metodo per il visitatore per gestire combattere il nuovo tipo.
  • un'implementazione per gestire le lotte per il nuovo tipo

Ora, qui si scopre che si avrà qualche duplicazione del codice nel caso in cui "Ogre vs Wizard" == "Wizard vs Ogre". Non so se questo è il caso - per esempio potrebbe esserci una differenza a seconda di chi colpisce per primo. Inoltre, potresti voler fornire un algoritmo completamente diverso per, diciamo "Combattere la palude con l'orco" rispetto a "Battaglia di villaggio con l'orco". In questo modo è possibile creare un nuovo visitatore (o una gerarchia di visitatori) e applicare quello appropriato quando necessario.

+0

Il visitatore può essere semplicemente passato con il costruttore per evitare di doverlo passare come parametro per ogni battaglia? –

+1

@mgroves: sì, ma questo consente diversi scenari di battaglia: in diverse situazioni, potrebbero essere richiesti risultati diversi. – Eric

+0

Quindi ... ogni volta che vuoi aggiungere un nuovo personaggio devi aggiungere un metodo in più per ogni personaggio esistente in ogni visitatore? Come è meglio di cambiare tutti gli altri personaggi? – wilhelmtell

0

Hmm, prima di tutto, il tuo primo progetto non è buono, perché lasci che i combattenti decidano chi vince. Se utilizzi il Mediatore, ad es. qualcosa di simile alla classe di battaglia proposta, sarai in grado di centralizzare la logica di combattimento e cambiare facilmente qualsiasi regola di combattimento in un unico posto. Immagina di avere molte creature ... una volta che vorrai cambiare il modo in cui due combattono insieme, dove andrai a cercare il metodo di battaglia? In prima o in seconda classe? Qualche superclasse? Quindi Mediatore è una buona idea. Ancora un altro problema è decidere quale regola usare. Potresti facilmente arrivare al problema della spedizione multipla.

+0

Che cos'è "il problema della spedizione multipla"? La spedizione multipla non è una soluzione? :-) – Ken

+0

@Ken si, è una soluzione, che è in molte lingue problematica da implementare :) –

0

Qualcosa di simile?

class Trait 
{ 
    enum Type 
    { 
     HEIGHT, 
     WEIGHT, 
     IQ 
    } 
    protected Type type; 
    protected int value; 

    public Trait(int value, Type type) 
    { 
     this.type = type; 
     this.value = value; 
    } 

    public boolean compareTo(Trait trait) 
    { 
     if(trait.type != this.type) 
      throw new IllegalArgumentException(trait.type+" and "+this.type+" are not comparable traits"); 
     else 
      return this.value - trait.value; 
    } 
} 

class Character 
{ 
    protected Trait[] traits; 

    protected Character(Trait[] traits) 
    { 
     this.traits = traits; 
    } 

    public Trait getTrait(Trait.Type type) 
    { 
     for(Trait t : traits) 
      if(t.type == type) return t; 
     return null; 
    } 

    public Character doBattleWith(Character that) 
    { 
     for(Trait thisTrait : traits) 
     { 
      otherTrait = that.getTrait(thisTrait.type); 
      if(otherTrait != null) 
      { 
       int comp = thisTrait.compareTo(otherTrait); 

       if(comp > 0) 
        return this; 
       else if (comp < 0) 
        return that; 
      } 
     } 
     return null; 
    } 
} 

class Ogre extends Character 
{ 
    public Ogre(int height, int weight) 
    { 
     super(new Trait[]{ 
      new Trait(Type.HEIGHT,height), 
      new Trait(Type.WEIGHT,height)}); 
    } 
} 
7

Cosa dovrebbe accadere nel caso in cui due Orchi hanno la stessa altezza/peso e la stessa ascia di lunghezza?Secondo il tuo esempio, quello che è stato abbastanza fortunato da essere chiamato primo vincerebbe.

Non so se questa sia un'alternativa adatta, ma cosa succederebbe se si optasse per uno schema completamente diverso e si attribuisse un "punteggio di battaglia" a ciascun personaggio invece di basarsi sul confronto dei tratti individuali. Puoi usare gli attributi di un personaggio in una formula per dare un numero intero che può essere paragonato a quello di un altro. Quindi è possibile utilizzare un metodo generico battle per confrontare i due punteggi e restituire il carattere con quello più alto.

Ad esempio, cosa accadrebbe se il "punteggio di battaglia" di un orco fosse calcolato dalla sua altezza più il suo peso moltiplicato per la lunghezza dell'ascia e il punteggio di un mago fosse calcolato per la sua età moltiplicata per il suo QI?

abstract class Character { 
    public abstract int battleScore(); 

    public Character battle(Character c1, Character c2) { 
     (c1.battleScore() > c2.battleScore()) ? return c1 : c2; 
    } 
} 

class Ogre extends Character { 
    public int battleScore() { 
     return (height + weight) * axeLength; 
    } 
} 

class Wizard extends Character { 
    public int battleScore() { 
     return height + (age * IQ); 
    } 
} 
+2

È una procedura guidata di età 100 e IQ 200 potente come una procedura guidata di età 200 e IQ 100? Vedo che questi sono solo esempi, ma c'è un problema con le idee flatning (età, QI, ...) in numeri interi. Tuttavia, personalmente mi piace questa direzione molto più del modello di visitatore. – wilhelmtell

+0

Questo era solo un semplice esempio, quindi si potrebbe dire che l'età vale 'x' per cento e iq vale' y'. – DMan

1

si sta andando ad avere per definire la logica unica (assumendo che la logica è unico) per ogni singola combinazione di battaglia in ogni modo - non importa quale schema di progettazione si sceglie di utilizzare. L'unico requisito è separare questa logica dalla classe Ogre e Wizard e creare i metodi di battaglia in una classe diversa. Penso che quello che stai facendo attualmente sia completamente corretto (una volta spostato la logica di battaglia da qualche altra parte) senza richiedere un pattern di visitatore, che è quello che userei se si trattasse di un gioco aziendale :)

Non ascoltare tutto di questo fluff sui modelli di design ...

+0

-1, penso che il modello di visitatore sia una soluzione molto elegante –

3

Penso che dovresti ripensare a tutta questa faccenda.

Prendiamo semplicemente World of Warcraft come esempio di come si può combattere, semplicemente perché è un gioco ben noto.

Hai un numero di classi diverse, che sono in grado di fare cose diverse, e hanno i loro punti di forza e di debolezza. Tuttavia, tutti condividono alcuni tipi comuni di statistiche. Ad esempio, un Mago ha più Intelletto di un Guerriero, ma il Guerriero avrà molta più forza del Mago.

Quindi come combatte? Bene, indipendentemente dalla classe, ogni personaggio ha un numero di abilità a sua disposizione. Ogni abilità fa una certa quantità di danno, e una volta che i PV di uno dei personaggi scendono a 0, quel personaggio muore - perdono il combattimento.

Si dovrebbe usare un approccio simile: definire una classe base comune con attributi comuni che si applicano a tutti - roba come forza, forza di fuoco, difesa e resistenza. Quindi, quando ogni tipo di personaggio combatte, possono usare una qualsiasi serie di attacchi o incantesimi: il danno causato da ogni attacco o incantesimo dipenderà dalle statistiche sia dell'attaccante che del difensore, usando una formula adatta (con un po 'di casualità per renderlo interessante, probabilmente non sarà divertente se per un Mago è impossibile battere un Ogre o viceversa).

Ma ecco qualcos'altro da considerare: forse non dovresti usare una classe per tipo. Sarebbe più preferibile se potessi usare la stessa formula per tutti - anche se non hanno lo stesso set di abilità. Invece di dover codificare ogni abilità lì dentro, avresti solo un elenco di abilità e i loro parametri in un file, e la classe Personaggio lo userebbe per fare tutti questi calcoli. In questo modo è più facile regolare la formula (solo quella posizione da guardare), e più facile da regolare le abilità (basta cambiare il file). È un po 'più difficile scrivere quella formula, perché potresti voler dare all'Ogre un bonus per avere un'alta forza, mentre il Mago otterrebbe un bonus per un'intelligenza elevata, ma è meglio che avere X formule quasi identiche, una per ciascuna stat che può influenzare l'output.

+0

Capisco cosa stai dicendo qui, ma penso anche che stai fondamentalmente creando un gioco diverso da quello che sta cercando di creare. –

+0

@mgroves: non sto dicendo di coprire ogni singolo aspetto che WoW copre è la strada giusta da percorrere, ma se non hai attributi comuni, devi specificare un modo diverso di combattere per ogni combinazione di tipi - Tutti i quali devono essere specificati singolarmente. È molto probabile che diventi non-mantenibile puramente a causa di tutte queste combinazioni, ed è per questo che probabilmente staresti meglio cercando di centralizzare alcune di queste cose. –

+0

Se i singoli metodi di battaglia finiscono per essere molto simili/ripetitivi, penso che un approccio come il tuo sia il modo migliore per andare. Tuttavia, sembra dalla domanda che ogni abbinamento di battaglia potrebbe essere molto, molto unico, e penso che il modello di visitatore sia il migliore per quello. –

3

Questo è esattamente il tipo di problemi Strategy è lo scopo di risolvere.

Rivediamo le parti del Strategy

  • Contesto: La lotta
  • Strategia: Come definirebbe che vincerà
  • strategia concreta: dove la lotta avviene e la decisione è presa .

Così, invece di lasciare che la responsabilità per il carattere loro stessi (perché saranno sempre dire, "Ho vinto !!, non ho vinto, no ..") è possibile creare una RefereeStrategy.

L'implementazione concreta deciderà chi vince.

strategy in action http://bit.ly/cvvglb

diagramma generato con http://yuml.me

si può sia definire i metodi più comuni che tutti i personaggi accettano di rispondere (che non sembra quello che si vuole, questo è utile quando tutti i personaggi avere gli stessi "attributi" o metodi come defense():int, attack():int, heal():int) o fare una strategia "cieca".

che sto facendo il secondo (strategia "alla cieca")

// All the contenders will implement this. 
interface Character { 
    public String getName();  
} 
// The context 
class FightArena { 
    Character home; 
    Character visitor; 

    // The strategy 
    Referee referee; 

    Character fight(){ 
     this.referee = RefereeFactory.getReferee(home.getName(), visitor.getName()); 
     Character winner = referee.decideFightBetween(home, visitor); 
     out.println(" And the winner iiiiss...... " + winner.getName()); 
    } 
} 

interface Referee { 
    Character decideFightBetween(Character one, Character two); 
} 

class RefereeFactory { 

     static Referee getReferee(Character one, Character two) { 
      .... return the appropiate Refereee... 
     }  
} 

// Concrete Referee implementation 
// Wizard biased referee, dont' trust him 
class OgreWizardReferee implements Referee { 
    Character decideFightBetween(Character one, Character two) { 
     if(one instanceof Wizard){ 
      return one; 
     }else{ 
      return two; 
     } 
    } 
} 
class OgreReferee implements Referee { 
    Character decideFightBetween(Character one, Character two) { 
     Ogre a = (Ogre) one; 
     Ogre b = (Ogre) two; 

     if(a.height > b.height || a.axeLength > a.axeLength) { 
      return a; 
     } 
     return b; 
    } 

} 

Questo permetterà di collegare nuovi algoritmi (strategie - Arbitri - ciò che il modello è buono per) quando ne hai bisogno.

mantiene il contesto (la vostra lotta Arena) gratuita di "if/elseif/else/if/else" costrutti inoltrando all'arbitro la decisione del vincitore, e isolare tuoi personaggi differenti le une dalle altre.

+1

Non direi che mi piace questa opzione, perché si basa su costanti di stringa come chiavi della mappa. Quindi qualcosa di simile a if/elseif/else/if è effettivamente nascosto all'interno della mappa, ma esiste ancora. – Bozho

+0

Posso capire perché questa sarebbe una buona soluzione per un linguaggio che non supporta il polimorfismo. – Troubadour

+0

@Troubadour. Il polimorfismo è necessario affinché la strategia concreta funzioni. – OscarRyz

1

Invece di provare a risolvere ogni battaglia per tipo di mostro o altro tipo di mostro, perché non creare un valore per un mostro in base ai suoi attributi.

Se ha un valore superiore a quello che combatte, vince.

per compensare alcuni nemici che sono meglio contro altri nemici, attuare una sorta di difesa per ogni tipo di attacco

per esempio attacchi Archer della gamma, orco è in mischia, e wizard è magia. Ogre ha difesa in mischia, difesa a distanza e difesa magica.

Il valore di un mostro può quindi essere calcolato sulla base è attacco & ed i nemici rispettive armature, così come HP ecc ecc ecc

Quindi non si preoccupano con un caso per caso.

0

So che questo è un po 'in ritardo, ma Steve Yegge ha scritto an article alcuni anni fa su quasi questo esatto problema (ha anche usato un esempio di gioco!).

Problemi correlati