Come posso progettare molti diversi tipi di attacco che possono essere combinati?


17

Sto realizzando un gioco 2D dall'alto verso il basso e voglio avere molti tipi di attacco diversi. Vorrei rendere gli attacchi molto flessibili e combinabili come funziona The Binding of Isaac. Ecco un elenco di tutti gli oggetti da collezione nel gioco . Per trovare un buon esempio, diamo un'occhiata all'elemento Spoon Bender .

Spoon Bender dà a Isaac la possibilità di sparare lacrime di ritorno.

Se guardi la sezione "sinergie", vedrai che può essere combinato con altri oggetti da collezione per effetti interessanti ma intuitivi. Ad esempio, se si combina con The Inner Eye , "Permetterà a Isaac di sparare più colpi di homing contemporaneamente". Questo ha senso, perché The Inner Eye

Dà a Isaac un triplo tiro

Qual è una buona architettura per progettare cose come questa? Ecco una soluzione di forza bruta:

if not spoon bender and not the inner eye then ...
if spoon bender and not the inner eye then ...
if not spoon bender and the inner eye then ...
if spoon bender and the inner eye then ...

Ma questo sfuggirà di mano molto velocemente. Qual è il modo migliore per progettare un sistema come questo?


Personalmente terrei semplicemente tutti gli oggetti equipaggiati in un elenco e li farei implementare una sorta di interfaccia comune che prende un oggetto mutato da oggetto a oggetto. "per ogni oggetto modifica attacco pianificato" in modo che un oggetto possa duplicare la quantità di proiettili, si potrebbe aggiungere la sua tinta e cambiare il danno (quindi se un oggetto ha fatto bulloni rossi e un altro ha ingiallito, si avrebbe un attacco arancione dopo che entrambi hanno modificato l'attacco) . Potresti anche avere solo una classe di oggetti generica con parametri per decidere come modificare l'attacco pianificato.
Benjamin Danger Johnson,

2
Vorrei sottolineare che Kirby 64 ha adottato l'approccio della forza bruta e ha programmato duramente effetti diversi per tutte le possibili combinazioni di abilità, quindi è fattibile.
Kevin,

Risposte:


16

Non hai assolutamente bisogno di codificare manualmente le combinazioni. Puoi invece concentrarti sulle proprietà che ti dà ogni elemento. Ad esempio, l' articolo A imposta Projectile=Fireball,Targetting=Homing. Voce B set FireMode=ArcShot,Count=3. La ArcShotlogica è responsabile dell'invioCount numero di Projectileelementi in un arco.

Questi due elementi possono essere combinati con qualsiasi altro oggetto che modifica liberamente queste (o altre) proprietà. Se aggiungi un nuovo tipo di proiettile, funzionerà solo automaticamente ArcShote, se aggiungi una nuova modalità di fuoco, funzionerà automaticamente con i Fireballproiettili. Allo stesso modo,Targetting è una proprietà che imposta il controller per i proiettili mentre FireModecrea i proiettili, in modo che possano essere facilmente e banalmente combinati in qualsiasi combinazione, secondo il senso.

È inoltre possibile impostare dipendenze di proprietà e simili. Ad esempio, ArcShotrichiede di avere un provider di Projectile(che potrebbe essere solo il valore predefinito). È possibile impostare le priorità in modo che se si hanno due elementi attivi che entrambi fornisconoProjectile il codice sappiano quale utilizzare. Oppure puoi fornire l'interfaccia utente per consentire all'utente di selezionare il tipo di proiettile da utilizzare, o semplicemente richiedere al giocatore di annullare la divisione di oggetti ad alta priorità che non desidera, oppure utilizzare l'elemento più recente, ecc. Puoi inoltre consentire un sistema di incompatibilità , ad esempio in modo tale che due elementi che entrambi modificano Projectilenon possano essere equipaggiati contemporaneamente.

In generale, quando possibile, preferisci qualsiasi tipo di approccio basato sui dati (o dichiarativo ) rispetto agli approcci procedurali (i grossi pastelli if-else) quando si tratta di oggetti e simili nel tuo gioco. La logica generica di livello superiore configurabile da dati semplici è molto preferibile rispetto a elenchi codificati di regole con casing speciale.


Piccolo nit, ma non sembra che tu abbia le proprietà corrette per gli esempi che stai usando. "Spoon Bender" aggiunge homing e "Inner Eye" aggiunge tre colpi. Nessuno dei due aggiunge arco ed entrambi sono lacrime. Se stai usando proprietà arbitrarie nel tuo esempio per astrarre il design, sarebbe più facile da leggere se non fossero state nominate in modo fuorviante. Preferirei "Articolo A" e "Articolo B" a questo.
Daniel Kaplan,

1
@tieTYT: ho generalizzato i nomi e ampliato l'esempio per includere modalità di puntamento oltre a tipi di proiettili e modalità di fuoco. Non ho mai giocato alla BoI per più di qualche minuto, quindi non ho i nomi interiorizzati come potrebbero fare gli altri. :)
Sean Middleditch l'

Mi sembra che la parte difficile sia identificare le categorie di proprietà. ad esempio: il targeting è uno, FireMode è un altro, Count può essere un terzo ... o no. Forse 3 proiettili potrebbero essere una palla di fuoco e 5 potrebbero essere granate anche se è un'arma.
Daniel Kaplan,

@tieTYT: assolutamente. Puoi estendere ulteriormente il sistema per consentire combinazioni o logiche speciali, certamente, ma questa dovrebbe essere l'eccezione piuttosto che la regola. Ottimizza per il caso comune, non per il caso angolare.
Sean Middleditch,

Ecco un ottimo video sul perché ti sbagli riguardo alla programmazione procedurale youtu.be/QM1iUe6IofM , anche il modo basato sui dati che descrivi le mappe di blocchi if / else in singoli set di dati, essenzialmente non facendo nulla per ridurre il numero di scenari di casi speciali che devi programma, come devi programmarli tutti ...
RenaissanceProgrammer

8

Se stai usando un linguaggio OOP, questo suona come un buon posto per utilizzare il modello Decoratore . Quando si desidera modificare il modo in cui si verifica un attacco, è sufficiente decorarlo con l'appropriato potenziamento.

Esempio di c ++ grezzo:

class AttackBehaviour
{
    /* other code */
    virtual void Attack(double angle);
};

class TearAttack: public AttackBehaviour
{
    /* other code */
    void Attack(double angle);
};

class TripleAttack: public AttackBehaviour
{
    /* other code */
    AttackBehaviour* baseAttackBehaviour;
    void Attack(double angle);
};

void TripleAttack::Attack(angle)
{
    baseAttackBehaviour->Attack(angle-30);
    baseAttackBehaviour->Attack(angle);
    baseAttackBehaviour->Attack(angle+30);
}

Questo metodo sarebbe il migliore se hai un numero molto elevato di attacchi e devi farli comportare tutti più o meno allo stesso modo. Se vuoi cambiare sostanzialmente il modo in cui l'attacco si verifica con il modificatore (ad es. Nuova animazione con modificatore), questo metodo non fa per te.


Se voglio cambiare l'animazione, perché questo non fa per me? Inoltre, cosa succede se sto lavorando in un linguaggio più funzionale? Conosci un modello di design adatto a loro?
Daniel Kaplan,

È più rigido perché il modificatore finirà sempre per chiamare il Attackmetodo dell'oggetto che aggrega. La TripleAttackclasse non dovrebbe conoscere la TearAttackclasse. Se questo fosse vero, porterebbe a tanti mal di testa quanto il else-ifblocco. Ciò significa che qualsiasi animazione lacrimale deve risiedere all'interno TearAttackBehaviourdell'oggetto. Questo oggetto non (e non dovrebbe) sapere che è stato decorato da un TripleAttackoggetto. Il risultato è che le 3 animazioni lacrimali procedono in modo indipendente, perché sono indipendenti.
NauticalMile,

Sto facendo fatica a spiegarlo a parole, se qualcun altro vuole prenderlo a pugni, sii mio ospite.
NauticalMile,

Per quanto riguarda l'implementazione di questo in un linguaggio più funzionale, ci penserò per un po 'e modificherò la mia risposta quando sarò pronto.
NauticalMile,

1

Come fan di Binding of Isaac, anch'io mi sono chiesto come affrontare qualcosa del genere. Il sistema nel gioco è abbastanza robusto in cui i comportamenti emergenti derivano dalla combinazione di effetti (quello che mi viene in mente è ottenere specchio, piegatore a cucchiaio e alcuni amplificatori di portata si traducono in una parete lacrimante vorticosa e homing attorno a Isaac, in stile Magneto ). Il solo numero di questi renderebbe impraticabile un blocco "if".

La mia conclusione è che Isaac e le sue lacrime sono due entità al centro di un imponente Componente-Entità Quadro . Le entità hanno alcune statistiche di base (velocità di spostamento, vita, distanza, danno, sprite, ecc.) E ogni componente porterebbe un modificatore di stat e un verbo.

Nel codice, Isaac e le sue lacrime avrebbero ciascuna un elenco che conterrebbe elementi di un'interfaccia. Isaac avrebbe un elenco di cose che si abbonano all'interfaccia di IsaacMutator e le sue lacrimeMutator. IsaacMutator avrebbe funzioni per modificare la salute, la velocità, la portata, gli sguardi di Isaac e alcuni verbi speciali. TearMutator sarebbe simile. Una volta per ciclo di gioco, Isaac passava attraverso tutti gli IsaacMutator che ha, e anche tutte le lacrime viventi. Per andare dal tuo esempio inglese, si legge come:

Isaac has IsaacMutators:
--spoonbender which gives no stat change and: Tears are made homing
--MeatEater which give +1 health, +1 damage and: nothing
--MagicMirror which gives no stat change and: Tears are made reflecting

Tears have tearMutators:
--(depends on MeatEater) +1 damage and: nothing
--(depends on MagicMirror) no stat change and: +1 vector towards isaac
--(depnds on spoonbender) no stat change and: +1 vector towards enemytype

e così via. Poiché i tipi sono additivi, puoi impilare, aggiungere e rimuovere il contenuto del tuo cuore.


-5

Penso che la tua strada funzioni meglio. Ciascuno di questi elementi dà una condizione, se usati insieme producono una condizione diversa, allora avresti effettivamente bisogno di definire tutte e 3 le possibili condizioni.

Potresti anche aggirare il problema creando un nuovo tipo di definizione quando sono presenti entrambi gli elementi, ma questo in realtà si aggiunge alla convoluzione:

if spoon bender and the inner eye then new spoon bender inner eye

if spoon bender inner eye then ...
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.