Cosa posso fare per evitare flag e controlli una tantum nel mio codice?


17

Prendi in considerazione un gioco di carte, come Hearthstone .

Ci sono centinaia di carte che fanno una grande varietà di cose, alcune delle quali sono uniche anche per una singola carta! Ad esempio, esiste una carta (chiamata Nozdormu) che riduce i turni di gioco a soli 15 secondi!

Quando hai una così ampia varietà di potenziali effetti, come evitare i numeri magici e i controlli una tantum su tutto il codice? Come evitare un metodo "Check_Nozdormu_In_Play" nella classe PlayerTurnTime? E come si può organizzare il codice in modo tale che quando si aggiungono ancora più effetti, non è necessario riformattare i sistemi di base per supportare cose che non hanno mai dovuto supportare prima?


È davvero un problema di prestazioni? Voglio dire, puoi fare un sacco di cose con le moderne CPU in pochissimo tempo ..
Jari Komppa,

11
Chi ha detto qualcosa sui problemi di prestazione? Il problema principale che vedrei è la costante necessità di adattare tutto il codice ogni volta che crei una nuova carta.
jhocking

2
quindi aggiungi un linguaggio di scripting e script ogni scheda.
Jari Komppa,

1
Non c'è tempo per dare una risposta corretta, ma invece di avere ad esempio il controllo Nozdormu e la regolazione di 15 secondi all'interno del codice della classe "PlayerTurnTime" che gestisce i turni del giocatore, puoi codificare la classe "PlayerTurnTime" per chiamare una [classe-, se vuoi ] funzione fornita dall'esterno in punti specifici. Quindi il codice carta Nozdormu (e tutte le altre carte che devono influenzare la stessa condizione) possono implementare una funzione per tale aggiustamento e iniettare quella funzione nella classe PlayerTurnTime quando necessario. Potrebbe essere utile leggere il modello di strategia e l'iniezione di dipendenza dal classico libro Design Patterns
Peteris,

2
Ad un certo punto mi chiedo se l'aggiunta di controlli ad hoc ai relativi bit di codice sia la soluzione più semplice.
user253751

Risposte:


12

Hai esaminato i sistemi dei componenti delle entità e le strategie di messaggistica degli eventi?

Gli effetti di stato dovrebbero essere componenti di qualche tipo che possono applicare i loro effetti persistenti in un metodo OnCreate (), scadere i loro effetti in OnRemoved () e sottoscrivere messaggi di eventi di gioco per applicare effetti che si verificano come reazione a qualcosa che accade.

Se l'effetto è persistentemente condizionato (dura per X turni, ma si applica solo in determinate circostanze) potrebbe essere necessario verificare tali condizioni in varie fasi.

Quindi, ti assicuri solo che il tuo gioco non abbia anche numeri magici predefiniti. Assicurarsi che tutto ciò che può essere modificato sia una variabile basata sui dati piuttosto che valori predefiniti codificati con variabili utilizzate per eventuali eccezioni.

In questo modo, non si assume mai quale sarà la lunghezza del giro. È sempre una variabile costantemente controllata che può essere modificata da qualsiasi effetto e eventualmente annullata in seguito dall'effetto quando scade. Non controlli mai le eccezioni prima di impostare il numero magico per impostazione predefinita.


2
"Assicurati che tutto ciò che può essere modificato sia una variabile basata sui dati piuttosto che valori predefiniti codificati con variabili utilizzate per eventuali eccezioni." - Ooh, piuttosto mi piace. Questo aiuta molto, penso!
Sable Dreamer,

Potresti approfondire "applicare i loro effetti persistenti"? La sottoscrizione a turnStarted e quindi la modifica del valore di Lunghezza renderebbe il codice indebitabile e, o peggio ancora, produrrebbe risultati incoerenti (quando si interagisce tra effetti simili)?
Wondra,

Solo per gli abbonati che avrebbero assunto un determinato lasso di tempo. Devi modellare attentamente. Potrebbe essere utile che il tempo di turno corrente sia diverso dal tempo di svolta del giocatore. PTT verrebbe controllato per creare una nuova svolta. CTT potrebbe essere controllato da carte. Se un effetto deve aumentare l'ora corrente, l'interfaccia utente del timer dovrebbe naturalmente seguire l'esempio se è apolide.
RobStone,

Per rispondere meglio alla domanda. Nient'altro memorizza il tempo di svolta o nulla in base a ciò. Controllalo sempre.
RobStone,

11

RobStone è sulla buona strada, ma volevo approfondire dato che è esattamente quello che ho fatto quando ho scritto Dungeon Ho !, un Roguelike che aveva un sistema di effetti molto complesso per armi e incantesimi.

Ogni carta dovrebbe avere una serie di effetti associati ad essa, definita in modo tale da poter indicare qual è l'effetto, quali bersagli, come e per quanto tempo. Ad esempio, un effetto "danni all'avversario" potrebbe assomigliare a questo;

Effect type: deal damage (enumeration, string, what-have-you)
Effect amount: 20
Source: my weapon
Target: opponent
Effect Cost: 20
Cost Type: Mana

Quindi, quando l'effetto viene attivato, è necessario che una routine generica gestisca l'elaborazione dell'effetto. Come un idiota, ho usato un'enorme dichiarazione case / switch:

switch (effect_type)
{
     case DAMAGE:

     break;
}

Ma un modo molto migliore e più modulare per farlo è tramite il polimorfismo. Crea una classe Effect che racchiuda tutti questi dati, crea una sottoclasse per ogni tipo di effetto, quindi fai in modo che quella classe abbia la precedenza su un metodo onExecute () specifico della classe.

class Effect
{
    Object source;
    int amount;

    public void onExecute(Object target)
    {
          // Do nothing
    }
}

class DamageEffect extends Effect
{
    public void onExecute(Object target)
    {
          target.health -= amount;
    }
}

Quindi avremmo una classe Effect di base, quindi una classe DamageEffect con un metodo onExecute (), quindi nel nostro codice di elaborazione andremmo semplicemente;

Effect effect = card.getActiveEffect();

effect.onExecute();

Il modo per affrontare la conoscenza di ciò che è in gioco è creare un vettore / matrice / elenco collegato / ecc. di effetti attivi (di tipo Effect, la classe base) associati a qualsiasi oggetto (incluso il campo di gioco / "gioco"), quindi piuttosto che dover controllare se un particolare effetto è in gioco, basta scorrere tutti gli effetti associati a gli oggetti e lasciarli eseguire. Se un effetto non è attaccato a un oggetto, non è in gioco.

Effect effect;

for (int o = 0; o < objects.length; o++)
{
    for (int e = 0; e < objects[o].effects.length; e++)
    {
         effect = objects[o].effects[e];

         effect.onExecute();
    }
}

Questo è esattamente come l'ho fatto. Il bello qui è che hai essenzialmente un sistema basato sui dati e puoi regolare la logica piuttosto facilmente in base agli effetti. Di solito dovrete fare un controllo delle condizioni nella logica di esecuzione dell'effetto, ma è ancora molto più coerente poiché questi controlli sono solo per l'effetto in questione.
Manabreak,

1

Offrirò una manciata di suggerimenti. Alcuni si contraddicono a vicenda. Ma forse alcuni sono utili.

Considera gli elenchi rispetto ai flag

Puoi iterare in tutto il mondo e controllare una bandiera su ogni oggetto per decidere se fare la cosa bandiera. Oppure puoi tenere un elenco solo di quegli elementi che dovrebbero fare la cosa bandiera.

Considera elenchi ed enumerazioni

Puoi continuare ad aggiungere campi booleani alla tua classe di articoli, isAThis e isAThat. Oppure puoi avere un elenco di stringhe o elementi enum, come {"isAThis", "isAThat"} o {IS_A_THIS, IS_A_THAT}. In questo modo è possibile aggiungerne di nuovi all'enumerazione (o contro stringhe) senza aggiungere campi. Non che ci sia qualcosa di veramente sbagliato nell'aggiungere campi ...

Considera i puntatori a funzioni

Invece di un elenco di flag o enumerazioni, potrebbe avere un elenco di azioni da eseguire per quell'elemento in contesti diversi. (Entity-ish ...)

Considera gli oggetti

Alcune persone preferiscono approcci basati sui dati, basati su script o su entità componenti. Ma vale la pena considerare anche le gerarchie di oggetti vecchio stile. La classe base deve accettare le azioni, come "gioca questa carta per la fase di turno B" o qualsiasi altra cosa. Quindi ogni tipo di carta può sovrascrivere e rispondere come appropriato. Probabilmente c'è anche un oggetto giocatore e un oggetto di gioco, quindi il gioco può fare cose come, se (player-> isAllowedToPlay ()) {fare il gioco ...}.

Prendi in considerazione l'abilità di debug

Una cosa bella di una pila di campi bandiera è che puoi esaminare e stampare lo stato di ogni oggetto allo stesso modo. Se lo stato è rappresentato da tipi diversi, o gruppi di componenti, o puntatori a funzioni, o essendo in elenchi diversi, potrebbe non essere sufficiente guardare solo i campi dell'elemento. Sono tutti compromessi.

Alla fine, refactoring: considerare i test unitari

Non importa quanto generalizzi la tua architettura, sarai in grado di immaginare cose che non riguardano. Quindi dovrai refactoring. Forse un po ', forse molto.

Un modo per rendere questo più sicuro è con un insieme di test unitari. In questo modo puoi essere sicuro che anche se hai riorganizzato le cose sottostanti (forse di molto!) La funzionalità esistente funziona ancora. Ogni test unitario si presenta, in genere, come questo:

void test1()
{
   Game game;
   game.addThis();
   game.setupThat(); // use primary or backdoor API to get game to known state

   game.playCard(something something).

   int x = game.getSomeInternalState;
   assertEquals(“did it do what we wanted?”, x, 23); // fail if x isn’t 23
}

Come puoi vedere, mantenere stabili quelle chiamate API di alto livello sul gioco (o giocatore, carta, ecc.) È la chiave della strategia di test delle unità.


0

Invece di pensare a ciascuna carta singolarmente, inizia a pensare in termini di categorie di effetti e le carte contengono una o più di queste categorie. Ad esempio, per calcolare la quantità di tempo in un turno, puoi scorrere tutte le carte in gioco e controllare la categoria "manipola la durata del turno" di ogni carta che contiene quella categoria. Ogni carta quindi aumenta o sovrascrive la durata del turno in base alle regole che hai deciso.

Questo è essenzialmente un sistema mini-componente, in cui ogni oggetto "carta" è semplicemente un contenitore per un gruppo di componenti effetto.


Dato che le carte - e anche quelle future - possono fare praticamente qualsiasi cosa, mi aspetto che ogni carta abbia una sceneggiatura. Comunque sono abbastanza sicuro che questo non sia un vero problema di prestazioni ..
Jari Komppa il

4
come da commenti principali: nessuno (diverso da te) ha detto qualcosa sui problemi di prestazione. Per quanto riguarda lo scripting completo in alternativa, approfondiscilo in una risposta.
jhocking
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.