Qual è un modo per implementare un sistema buff / debuff flessibile?


66

Panoramica:

Molti giochi con statistiche simili ai giochi di ruolo consentono "buff" ai personaggi, che vanno dal semplice "Infligge il 25% di danno extra" a cose più complicate come "Infliggi 15 danni agli attaccanti quando viene colpito".

Le specifiche di ogni tipo di buff non sono veramente rilevanti. Sto cercando un modo (presumibilmente orientato agli oggetti) per gestire buff arbitrari.

Dettagli:

Nel mio caso particolare, ho più personaggi in un ambiente di battaglia a turni, quindi ho immaginato che i buff fossero legati ad eventi come "OnTurnStart", "OnReceiveDamage", ecc. Forse ogni buff è una sottoclasse di una classe astratta di Buff principale, dove solo gli eventi rilevanti sono sovraccarichi. Quindi ogni personaggio potrebbe avere un vettore di buff attualmente applicato.

Questa soluzione ha senso? Posso certamente vedere che sono necessari dozzine di tipi di eventi, sembra che creare una nuova sottoclasse per ogni buff sia eccessivo e non sembra consentire alcuna "interazione" di buff. Cioè, se volessi implementare un limite ai potenziamenti dei danni in modo che anche se avessi 10 diversi buff che danno tutti il ​​25% di danno extra, fai solo il 100% in più invece del 250% in più.

E ci sono situazioni più complicate che idealmente potrei controllare. Sono sicuro che tutti possono fornire esempi di come gli appassionati più sofisticati possano potenzialmente interagire tra loro in un modo che come sviluppatore di giochi non potrei desiderare.

Come programmatore C ++ relativamente inesperto (in genere ho usato C in sistemi embedded), mi sento come se la mia soluzione fosse semplicistica e probabilmente non sfruttasse appieno il linguaggio orientato agli oggetti.

Pensieri? Qualcuno qui ha progettato un sistema buff abbastanza robusto prima?

Modifica: per quanto riguarda le risposte:

Ho selezionato una risposta principalmente sulla base di buoni dettagli e di una solida risposta alla domanda che ho posto, ma leggere le risposte mi ha permesso di approfondire ulteriormente.

Forse non sorprende che i diversi sistemi o sistemi ottimizzati sembrano applicarsi meglio a determinate situazioni. Quale sistema funziona meglio per il mio gioco dipenderà dal tipo, dalla varianza e dal numero di buff che intendo essere in grado di applicare.

Per un gioco come Diablo 3 (menzionato di seguito), in cui quasi tutti gli equipaggiamenti possono cambiare la forza di un buff, i buff sono solo un sistema di statistiche dei personaggi che sembra una buona idea quando possibile.

Per la situazione a turni in cui mi trovo, l'approccio basato sugli eventi potrebbe essere più adatto.

In ogni caso, spero ancora che qualcuno arrivi con un fantastico proiettile magico "OO" che mi permetta di applicare una distanza di +2 mosse per buff di turno , un infortunio del 50% del danno riportato al buff dell'attaccante e un teletrasporto automatico su una tessera vicina quando viene attaccato da 3 o più buff di distanza in un singolo sistema senza trasformare un buff di forza +5 nella propria sottoclasse.

Penso che la cosa più vicina sia la risposta che ho segnato, ma il pavimento è ancora aperto. Grazie a tutti per l'input.


Non sto pubblicando questo come risposta, dato che sto solo facendo brainstorming, ma che ne dici di un elenco di appassionati? Ogni buff ha una costante e un modificatore di fattore. Costante sarebbe +10 di danno, fattore 1,10 per un aumento del danno di + 10%. Nei tuoi calcoli del danno, fai l'iterazione di tutti i buff, per ottenere un modificatore totale e quindi imponi le limitazioni che desideri. Lo faresti per qualsiasi tipo di attributo modificabile. Avresti bisogno di un metodo di caso speciale per cose complicate però.
William Mariager,

Per inciso, avevo già implementato qualcosa del genere per il mio oggetto Stats quando stavo realizzando un sistema per armi e accessori equipaggiabili. Come hai detto, è una soluzione abbastanza decente per i buff che modificano solo gli attributi esistenti, ma ovviamente anche allora vorrò che alcuni buff scadano dopo X turni, altri scadano quando si verifica l'effetto Y volte, ecc. menzionarlo nella domanda principale poiché stava già diventando molto lungo.
gkimsey,

1
se hai un metodo "onReceiveDamage" che viene chiamato da un sistema di messaggistica, o manualmente o in qualche altro modo, dovrebbe essere abbastanza facile includere un riferimento a chi / da cosa stai subendo il danno. Quindi potresti mettere queste informazioni a disposizione del tuo appassionato

Bene, mi aspettavo che ogni modello di evento per la classe Buff astratta includesse parametri rilevanti come quello. Funzionerebbe sicuramente, ma sono titubante perché sembra che non si ridimensionerà bene. Ho difficoltà a immaginare che un MMORPG con diverse centinaia di buff diversi abbia una classe separata definita per ogni buff, scegliendo tra centinaia di eventi diversi. Non che sto facendo così tanti buff (probabilmente più vicini ai 30), ma se c'è un sistema più semplice, più elegante o più flessibile, mi piacerebbe usarlo. Sistema più flessibile = buff / abilità più interessanti.
gkimsey,

4
Questa non è una buona risposta al problema dell'interazione, ma mi sembra che il motivo del decoratore si applichi bene qui; basta applicare più buff (decoratori) uno sopra l'altro. Forse con un sistema per gestire l'interazione "unendo" i buff (insieme ad esempio 10x 25% si fonde in un buff al 100%).
ashes999,

Risposte:


32

Questo è un problema complicato, perché stai parlando di alcune cose diverse che (in questi giorni) vengono raggruppate come "appassionati":

  • modificatori degli attributi di un giocatore
  • effetti speciali che si verificano su determinati eventi
  • combinazioni di quanto sopra.

Realizzo sempre il primo con un elenco di effetti attivi per un certo personaggio. La rimozione dall'elenco, sia in base alla durata che esplicitamente, è piuttosto banale, quindi non lo tratterò qui. Ogni effetto contiene un elenco di modificatori di attributo e può applicarlo al valore sottostante tramite una semplice moltiplicazione.

Quindi lo avvolgo con le funzioni per accedere agli attributi modificati. per esempio.:

def get_current_attribute_value(attribute_id, criteria):
    val = character.raw_attribute_value[attribute_id]
    # Accumulate the modifiers
    for effect in character.all_effects:
        val = effect.apply_attribute_modifier(attribute_id, val, criteria)
    # Make sure it doesn't exceed game design boundaries
    val = apply_capping_to_final_value(val)
    return val

class Effect():
    def apply_attribute_modifier(attribute_id, val, criteria):
        if attribute_id in self.modifier_list:
            modifier = self.modifier_list[attribute_id]
            # Does the modifier apply at this time?
            if modifier.criteria == criteria:
                # Apply multiplicative modifier
                return val * modifier.amount
        else:
            return val

class Modifier():
    amount = 1.0 # default that has no effect
    criteria = None # applies all of the time

Ciò consente di applicare effetti moltiplicativi abbastanza facilmente. Se hai bisogno anche di effetti additivi, decidi in quale ordine li applicherai (probabilmente l'ultimo additivo) ed esegui l'elenco due volte. (Probabilmente avrei elenchi di modificatori separati in Effect, uno per moltiplicativo, uno per additivo).

Il valore del criterio è quello di permetterti di implementare "+ 20% vs Undead" - imposta il valore UNDEAD sull'effetto e passa il valore UNDEAD solo get_current_attribute_value()quando stai calcolando un tiro di danno contro un nemico non morto.

Per inciso, non sarei tentato di provare a scrivere un sistema che applica e non applica i valori direttamente al valore dell'attributo sottostante - il risultato finale è che è molto probabile che i tuoi attributi si allontanino dal valore previsto a causa di un errore. (ad es. se moltiplichi qualcosa per 2, ma poi lo limiti, quando lo dividi di nuovo per 2, sarà inferiore rispetto a quando è iniziato.)

Per quanto riguarda gli effetti basati sugli eventi, come "Infligge 15 danni agli attaccanti quando colpiti", puoi aggiungere dei metodi sulla classe Effect. Ma se vuoi un comportamento distinto e arbitrario (ad es. Alcuni effetti per l'evento sopra riportato potrebbero riflettere un danno, alcuni potrebbero guarirti, potrebbe teletrasportarti casualmente, qualunque cosa) avrai bisogno di funzioni o classi personalizzate per gestirlo. Puoi assegnare funzioni ai gestori di eventi sull'effetto, quindi puoi semplicemente chiamare i gestori di eventi su qualsiasi effetto attivo.

# This is a method on a Character, called during combat
def on_receive_damage(damage_info):
    for effect in character.all_effects:
        effect.on_receive_damage(character, damage_info)

class Effect():
    self.on_receive_damage_handler = DoNothing # a default function that does nothing
    def on_receive_damage(character, damage_info):
        self.on_receive_damage_handler(character, damage_info)

def reflect_damage(character, damage_info):
    damage_info.attacker.receive_damage(15)

reflect_damage_effect = new Effect()
reflect_damage_effect.on_receive_damage_handler = reflect_damage
my_character.all_effects.add(reflect_damage_effect)

Ovviamente la tua classe Effect avrà un gestore di eventi per ogni tipo di evento e puoi assegnare le funzioni del gestore a quante ne hai bisogno in ogni caso. Non è necessario sottoclassare Effect, poiché ognuno è definito dalla composizione dei modificatori di attributo e dei gestori di eventi che contiene. (Probabilmente conterrà anche un nome, una durata, ecc.)


2
+1 per dettagli eccellenti. Questa è la risposta più vicina a rispondere ufficialmente alla mia domanda come ho visto. La configurazione di base qui sembra consentire molta flessibilità e una piccola astrazione di quella che potrebbe essere altrimenti una logica di gioco disordinata. Come hai detto, gli effetti più funky avrebbero comunque bisogno delle loro stesse classi, ma questo gestisce la maggior parte delle esigenze di un tipico sistema "buff", credo.
gkimsey,

+1 per indicare le differenze concettuali nascoste qui. Non tutti funzioneranno con la stessa logica di aggiornamento basata su eventi. Vedi la risposta di @ Ross per un'applicazione totalmente diversa. Entrambi dovranno esistere uno accanto all'altro.
ctietze,

22

In un gioco a cui ho lavorato con un amico per una classe abbiamo creato un sistema buff / debuff per quando l'utente viene intrappolato in erba alta e velocizza le tessere e cosa no, e alcune cose minori come sanguinamenti e veleni.

L'idea era semplice e mentre l'abbiamo applicata in Python, è stata piuttosto efficace.

Fondamentalmente, ecco come è andata:

  • L'utente aveva un elenco di buff e debuff attualmente applicati (si noti che un buff e un debuff sono relativamente gli stessi, è solo l'effetto che ha un risultato diverso)
  • Gli appassionati hanno una varietà di attributi come durata, nome e testo per la visualizzazione delle informazioni e il tempo di vita. I più importanti sono il tempo vivo, la durata e un riferimento all'attore a cui è applicato questo buff.
  • Per il Buff, quando è attaccato al giocatore tramite player.apply (buff / debuff), chiamerebbe un metodo start (), questo applicherebbe le modifiche critiche al giocatore come aumentare la velocità o rallentare.
  • Vorremmo quindi scorrere ogni buff in un ciclo di aggiornamento e i buff si aggiornerebbero, questo aumenterebbe il loro tempo di vita. Le sottoclassi implementerebbero cose come avvelenare il giocatore, dare al giocatore HP nel tempo, ecc.
  • Al termine del buff, che significa timeAlive> = duration, la logica di aggiornamento rimuoveva il buff e chiamava un metodo finish (), che variava dalla rimozione delle limitazioni di velocità su un giocatore a causare un piccolo raggio (pensa a un effetto bomba dopo un DoT)

Ora come applicare effettivamente gli appassionati del mondo è una storia diversa. Ecco il mio spunto di riflessione però.


1
Sembra una spiegazione migliore di ciò che stavo cercando di descrivere sopra. È relativamente semplice, sicuramente facile da capire. In sostanza hai citato tre "eventi" lì (OnApply, OnTimeTick, OnExpired) per associarlo ulteriormente al mio pensiero. Così com'è, non supporterebbe cose come la restituzione del danno quando viene colpito e così via, ma si adatta meglio a molti buff. Preferirei non limitare ciò che i miei buff possono fare (che = limitando il numero di eventi che mi viene in mente che devono essere chiamati dalla logica di gioco principale), ma la scalabilità dei buff potrebbe essere più importante. Grazie per il tuo contributo!
gkimsey,

Sì, non abbiamo implementato nulla del genere. Sembra davvero pulito e un ottimo concetto (un po 'come un appassionato di Thorns).
Ross,

@gkimsey Per cose come Thorns e altri buff passivi, implementerei la logica nella tua classe Mob come stat passivo simile a danno o salute e aumenterei questo stat quando applichi il buff. Questo semplifica molto il caso in cui hai più buff di spine e mantiene pulita l'interfaccia (10 buff mostrerebbero 1 danno di ritorno anziché 10) e lasciano il sistema buff semplice.
3Doubloons,

Questo è un approccio quasi controintuitivamente semplice, ma ho iniziato a pensare a me stesso giocando a Diablo 3. Ho notato che la vita rubata, la vita a colpi, i danni agli attaccanti in mischia, ecc. Erano tutte le loro statistiche nella finestra del personaggio. Certo, D3 non ha il sistema di buffing o le interazioni più complicati al mondo, ma è quasi banale. Questo ha molto senso. Tuttavia, ci sono potenzialmente 15 diversi buff con 12 diversi effetti che potrebbero cadere in questo. Sembra strano
riempire il

11

Non sono sicuro che stai leggendo questo, ma ho lottato con questo tipo di problema per molto tempo.

Ho progettato numerosi tipi diversi di sistemi affettivi. Li esaminerò brevemente ora. Tutto si basa sulla mia esperienza. Non pretendo di conoscere tutte le risposte.


Modificatori statici

Questo tipo di sistema si basa principalmente su interi semplici per determinare eventuali modifiche. Ad esempio, da +100 a Max HP, +10 per attaccare e così via. Questo sistema potrebbe anche gestire anche le percentuali. Devi solo assicurarti che l'impilamento non esca dal controllo.

Non ho mai memorizzato nella cache i valori generati per questo tipo di sistema. Ad esempio, se volessi visualizzare la massima salute di qualcosa, genererei il valore sul posto. Ciò ha impedito che le cose fossero soggette a errori e che fossero più facili da capire per tutti i soggetti coinvolti.

(Lavoro in Java, quindi quello che segue è basato su Java ma dovrebbe funzionare con alcune modifiche per altri linguaggi) Questo sistema può essere fatto facilmente usando enum per i tipi di modifica, e quindi numeri interi. Il risultato finale può essere inserito in una sorta di raccolta che ha coppie chiave, valore ordinate. Questa sarà una ricerca rapida e calcoli, quindi le prestazioni sono molto buone.

Nel complesso, funziona molto bene con modificatori statici completamente piatti. Tuttavia, il codice deve esistere nei posti corretti per i modificatori da utilizzare: getAttack, getMaxHP, getMeleeDamage e così via e così via.

Dove questo metodo fallisce (per me) è un'interazione molto complessa tra i buff. Non esiste un modo molto semplice per interagire se non ghettandolo un po '. Ha alcune semplici possibilità di interazione. Per fare ciò, è necessario apportare una modifica al modo in cui si memorizzano i modificatori statici. Invece di usare un enum come chiave, usi una stringa. Questa stringa sarebbe il nome Enum + variabile extra. 9 volte su 10, la variabile extra non viene utilizzata, quindi manterrai comunque il nome enum come chiave.

Facciamo un rapido esempio: se vuoi essere in grado di modificare il danno contro creature non morte, potresti avere una coppia ordinata come questa: (DAMAGE_Undead, 10) Il DAMAGE è l'Enum e il Undead è la variabile extra. Quindi durante il tuo combattimento, puoi fare qualcosa del tipo:

dam += attacker.getMod(Mod.DAMAGE + npc.getRaceFamily()); //in this case the race family would be undead

Ad ogni modo, funziona abbastanza bene ed è veloce. Ma fallisce in interazioni complesse e ha un codice "speciale" ovunque. Ad esempio, si consideri la situazione del "25% di probabilità di teletrasportarsi sulla morte". Questo è "abbastanza" complesso. Il sistema sopra può gestirlo, ma non facilmente, poiché è necessario quanto segue:

  1. Determina se il giocatore ha questa mod.
  2. Da qualche parte, disporre di un codice per eseguire il teletrasporto, in caso di successo. La posizione di questo codice è una discussione in sé!
  3. Ottieni i dati giusti dalla mappa Mod. Cosa significa il valore? È la stanza in cui si teletrasportano anche? Cosa succede se un giocatore ha due mod di teletrasporto su di loro ?? Gli importi non verranno sommati insieme ?????? FALLIMENTO!

Quindi questo mi porta al prossimo:


Il massimo sistema di buff complesso

Una volta ho provato a scrivere un MMORPG 2D da solo. Questo è stato un terribile errore ma ho imparato molto!

Ho riscritto il sistema di affetto 3 volte. Il primo utilizzava una variante meno potente di quanto sopra. Il secondo era quello di cui parlerò.

Questo sistema aveva una serie di classi per ogni modifica, quindi cose come: ChangeHP, ChangeMaxHP, ChangeHPByPercent, ChangeMaxByPercent. Ho avuto un milione di questi ragazzi, anche cose come TeleportOnDeath.

Le mie lezioni avevano cose che avrebbero fatto quanto segue:

  • applyAffect
  • removeAffect
  • checkForInteraction <--- importante

Applicare e rimuovere si spiegano da soli (anche se per cose come le percentuali, l'affetto avrebbe tenuto traccia di quanto aumentava l'HP per assicurarsi che quando l'effetto si fosse esaurito, avrebbe rimosso solo la quantità aggiunta. Era buggy, lol e mi ci è voluto molto tempo per assicurarmi che fosse giusto. Non mi sentivo ancora bene.).

Il metodo checkForInteraction era un pezzo di codice orribilmente complesso. In ciascuna delle classi degli affetti (es.: ChangeHP), avrebbe il codice per determinare se questo dovrebbe essere modificato dall'affetto dell'input. Quindi, ad esempio, se avessi qualcosa come ...

  • Buff 1: Infligge 10 danni da fuoco in attacco
  • Buff 2: Aumenta tutti i danni da fuoco del 25%.
  • Buff 3: Aumenta tutti i danni da fuoco di 15.

Il metodo checkForInteraction gestirà tutti questi effetti. Per fare ciò, è stato necessario controllare ogni effetto su TUTTI i giocatori vicini !! Questo perché il tipo di affetti che avevo affrontato con più giocatori nell'arco di un'area. Ciò significa che il codice non ha MAI AVUTO alcuna dichiarazione speciale come sopra - "se siamo appena morti, dovremmo verificare il teletrasporto sulla morte". Questo sistema lo gestirà automaticamente correttamente al momento giusto.

Cercare di scrivere questo sistema mi ha richiesto circa 2 mesi e fatto esplodere a testa in diverse volte. TUTTAVIA, era DAVVERO potente e poteva fare una quantità folle di roba - specialmente quando prendi in considerazione i seguenti due fatti per le abilità nel mio gioco: 1. Avevano range di destinazione (cioè: singolo, sé, solo gruppo, PB AE stesso , Target PB AE, AE targetizzato e così via). 2. Le abilità possono avere più di 1 effetto su di esse.

Come accennato in precedenza, questo è stato il 2 ° del 3 ° sistema di affetto per questo gioco. Perché mi sono allontanato da questo?

Questo sistema ha avuto le peggiori prestazioni che abbia mai visto! Era terribilmente lento perché doveva fare così tanto controllo per ogni cosa che succedeva. Ho provato a migliorarlo, ma lo ho ritenuto un fallimento.

Quindi arriviamo alla mia terza versione (e un altro tipo di sistema buff):


Classe di affetto complessa con gestori

Quindi questa è praticamente una combinazione delle prime due: possiamo avere variabili statiche in una classe Affect che contiene molte funzionalità e dati extra. Quindi chiama solo i gestori (per me, praticamente alcuni metodi di utilità statici invece di sottoclassi per azioni specifiche. Ma sono sicuro che potresti voler utilizzare sottoclassi per azioni se lo desideri anche tu) quando vogliamo fare qualcosa.

La classe Affect avrebbe tutte le cose succose, come tipi di target, durata, numero di usi, possibilità di esecuzione e così via.

Dovremmo ancora aggiungere codici speciali per gestire le situazioni, ad esempio il teletrasporto sulla morte. Dovremmo ancora verificare manualmente questo nel codice di combattimento, e quindi se esistesse, otterremmo un elenco di affetti. Questo elenco di affetti contiene tutti gli affetti attualmente applicati sul giocatore che ha avuto a che fare con il teletrasporto alla morte. Quindi guarderemmo ciascuno e verificheremmo se fosse eseguito e se avesse avuto successo (ci saremmo fermati al primo con successo). Se avesse avuto successo, avremmo semplicemente chiamato l'handler per occuparci di questo.

L'interazione può essere fatta, se vuoi anche tu. Dovrebbe solo scrivere il codice per cercare buff specifici sui giocatori / ecc. Poiché ha buone prestazioni (vedi sotto), dovrebbe essere abbastanza efficiente farlo. Avrebbe solo bisogno di gestori più complessi e così via.

Quindi ha molte prestazioni del primo sistema e ancora molta complessità come il secondo (ma non tanto). Almeno in Java, puoi fare alcune cose difficili per ottenere le prestazioni di quasi il primo nella maggior parte dei casi (ad esempio: avere una mappa enum ( http://docs.oracle.com/javase/6/docs/api/java /util/EnumMap.html ) con Enums come chiavi e ArrayList di affetti come valori. Ciò ti consente di vedere se hai rapidamente degli affetti [poiché l'elenco sarebbe 0 o la mappa non avrebbe l'enum] e non avere per iterare continuamente sugli elenchi degli affetti del giocatore senza motivo. Non mi dispiace iterare gli affetti se ne abbiamo bisogno in questo momento. Ottimizzerò più tardi se diventa un problema).

Attualmente sto riaprendo (riscrivendo il gioco in Java anziché nella base di codici FastROM in cui era originariamente) il mio MUD che si è concluso nel 2005 e recentemente mi sono imbattuto in come voglio implementare il mio sistema buff? Userò questo sistema perché ha funzionato bene nel mio precedente gioco fallito.

Bene, si spera che qualcuno, da qualche parte, troverà utili alcune di queste intuizioni.


6

Una classe diversa (o funzione indirizzabile) per ciascun buff non è eccessiva se il comportamento di tali buff è diverso l'uno dall'altro. Una cosa sarebbe avere buff + 10% o + 20% (che, ovviamente, sarebbero meglio rappresentati come due oggetti della stessa classe), altri implementerebbero effetti incredibilmente diversi che richiederebbero comunque un codice personalizzato. Tuttavia, credo che sia meglio avere modi standard di personalizzare la logica del gioco invece di lasciare che ogni appassionato faccia quello che vuole (e possibilmente interferire tra loro in modi imprevisti, disturbando l'equilibrio del gioco).

Suggerirei di dividere ogni "ciclo di attacco" in passaggi, in cui ogni passaggio ha un valore di base, un elenco ordinato di modifiche che possono essere applicate a quel valore (forse limitato) e un limite finale. Ogni modifica ha una trasformazione dell'identità come predefinita e può essere influenzata da zero o più buff / debuff. Le specifiche di ciascuna modifica dipendono dal passaggio applicato. Come viene implementato il ciclo dipende da te (inclusa l'opzione di un'architettura guidata dagli eventi, come hai discusso).

Un esempio di ciclo di attacco potrebbe essere:

  • calcolare l'attacco del giocatore (base + mod);
  • calcola la difesa dell'avversario (base + mod);
  • fai la differenza (e applica le mod) e determina il danno base;
  • calcola eventuali effetti parata / armatura (mod sul danno base) e applica il danno;
  • calcola qualsiasi effetto di rinculo (mod sul danno base) e applica all'attaccante.

La cosa importante da notare è che prima nel ciclo viene applicato un buff più effetto avrà nel risultato . Quindi, se vuoi un combattimento più "tattico" (in cui l'abilità del giocatore è più importante del livello del personaggio) crea molti buff / debuff sulle statistiche di base. Se vuoi un combattimento più "equilibrato" (dove il livello conta di più - importante nei MMOG per limitare il tasso di progresso) usa solo buff / debuff più avanti nel ciclo.

La distinzione tra "Modifiche" e "Buffs" che ho citato in precedenza ha uno scopo: le decisioni sulle regole e sull'equilibrio possono essere implementate sulla prima, quindi eventuali cambiamenti su quelle non devono riflettersi nelle modifiche a ogni classe della seconda. OTOH, i numeri e i tipi di appassionati sono limitati solo dalla tua immaginazione, dal momento che ciascuno di essi può esprimere il comportamento desiderato senza dover tenere conto di alcuna possibile interazione tra loro e gli altri (o addirittura l'esistenza degli altri).

Quindi, rispondendo alla domanda: non creare una classe per ogni Buff, ma una per ogni (tipo di) Modifica e legare la Modifica al ciclo di attacco, non al personaggio. I buff possono essere semplicemente un elenco di tuple (Modifica, chiave, valore) e puoi applicare un buff a un personaggio semplicemente aggiungendolo / rimuovendolo al set di buff del personaggio. Ciò riduce anche la finestra per errore, poiché le statistiche del personaggio non devono essere cambiate affatto quando vengono applicati i buff (quindi c'è meno rischio di ripristinare una stat al valore sbagliato dopo che un buff scade).


Questo è un approccio interessante perché rientra tra le due implementazioni che avevo preso in considerazione, vale a dire limitando semplicemente i buff a modificatori stat e di danno del risultato abbastanza semplici, o creando un sistema molto robusto ma molto elevato in grado di gestire qualsiasi cosa. Questa è una sorta di espansione del primo per consentire le "spine" pur mantenendo una semplice interfaccia. Anche se non penso che sia il proiettile magico per ciò di cui ho bisogno, sembra certamente che renda il bilanciamento molto più facile rispetto ad altri approcci, quindi potrebbe essere la strada da percorrere. Grazie per il tuo contributo!
gkimsey,

3

Non so se lo stai ancora leggendo, ma ecco come lo sto facendo ora (il codice si basa su UE4 e C ++). Dopo aver riflettuto sul problema per più di due settimane (!!), ho finalmente trovato questo:

http://gamedevelopment.tutsplus.com/tutorials/using-the-composite-design-pattern-for-an-rpg-attributes-system--gamedev-243

E ho pensato che l'incapsulamento di un singolo attributo all'interno di class / struct non è poi una cattiva idea. Tieni presente, tuttavia, che sto sfruttando davvero un grande vantaggio del sistema di riflessione del codice di build UE4, quindi senza qualche modifica, questo potrebbe non essere adatto ovunque.

In ogni caso, sono partito dall'avvolgere l'attributo in una singola struttura:

USTRUCT(BlueprintType)
struct GAMEATTRIBUTES_API FGAAttributeBase
{
    GENERATED_USTRUCT_BODY()
public:
    UPROPERTY()
        FName AttributeName;
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float BaseValue;
    /*
        This is maxmum value of this attribute.
    */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float ClampValue;
protected:
    float BonusValue;
    //float OldCurrentValue;
    float CurrentValue;
    float ChangedValue;

    //map of modifiers.
    //It could be TArray, but map seems easier to use in this case
    //we need to keep track of added/removed effects, and see 
    //if this effect affected this attribute.
    TMap<FGAEffectHandle, FGAModifier> Modifiers;

public:

    inline float GetFinalValue(){ return BaseValue + BonusValue; };
    inline float GetCurrentValue(){ return CurrentValue; };
    void UpdateAttribute();

    void Add(float ValueIn);
    void Subtract(float ValueIn);

    //inline float GetCurrentValue()
    //{
    //  return FMath::Clamp<float>(BaseValue + BonusValue + AccumulatedBonus, 0, GetFinalValue());;
    //}

    void AddBonus(const FGAModifier& ModifiersIn, const FGAEffectHandle& Handle);
    void RemoveBonus(const FGAEffectHandle& Handle);

    void InitializeAttribute();

    void CalculateBonus();

    inline bool operator== (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName == AttributeName);
    }

    inline bool operator!= (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName != AttributeName);
    }

    inline bool IsValid() const
    {
        return !AttributeName.IsNone();
    }
    friend uint32 GetTypeHash(const FGAAttributeBase& AttributeIn)
    {
        return AttributeIn.AttributeName.GetComparisonIndex();
    }
};

Non è ancora finito, ma l'idea di base è che questa struttura tiene traccia del suo stato interno. Gli attributi possono essere modificati solo dagli Effetti. Cercare di modificarli direttamente non è sicuro e non è esposto ai progettisti. Suppongo che tutto ciò che può interagire con gli attributi sia Effetto. Compresi i bonus piatti dagli oggetti. Quando viene equipaggiato un nuovo oggetto, viene creato un nuovo effetto (insieme alla maniglia), che viene aggiunto alla mappa dedicata, che gestisce bonus di durata infinita (quelli che devono essere rimossi manualmente dal giocatore). Quando viene applicato il nuovo effetto, viene creato un nuovo handle per esso (handle è solo int, racchiuso con struct) e quindi quel handle viene passato tutto intorno come mezzo per interagire con questo effetto, oltre a tenere traccia se l'effetto è ancora attivo. Quando l'effetto viene rimosso, l'handle viene trasmesso a tutti gli oggetti interessati,

La parte veramente importante di ciò è TMap (TMap è la mappa con hash). FGAModifier è una struttura molto semplice:

struct FGAModifier
{
    EGAAttributeOp AttributeMod;
    float Value;
};

Contiene il tipo di modifica:

UENUM()
enum class EGAAttributeOp : uint8
{
    Add,
    Subtract,
    Multiply,
    Divide,
    Set,
    Precentage,

    Invalid
};

E valore che è il valore finale calcolato che applicheremo all'attributo.

Aggiungiamo un nuovo effetto utilizzando la funzione semplice, quindi chiamiamo:

void FGAAttributeBase::CalculateBonus()
{
    float AdditiveBonus = 0;
    auto ModIt = Modifiers.CreateConstIterator();
    for (ModIt; ModIt; ++ModIt)
    {
        switch (ModIt->Value.AttributeMod)
        {
        case EGAAttributeOp::Add:
            AdditiveBonus += ModIt->Value.Value;
                break;
            default:
                break;
        }
    }
    float OldBonus = BonusValue;
    //calculate final bonus from modifiers values.
    //we don't handle stacking here. It's checked and handled before effect is added.
    BonusValue = AdditiveBonus; 
    //this is absolute maximum (not clamped right now).
    float addValue = BonusValue - OldBonus;
    //reset to max = 200
    CurrentValue = CurrentValue + addValue;
}

Questa funzione dovrebbe ricalcolare l'intera pila di bonus, ogni volta che si aggiunge o rimuove l'effetto. La funzione non è ancora terminata (come puoi vedere), ma puoi avere un'idea generale.

La mia più grande lamentela in questo momento è gestire l'attributo Danni / Guarigione (senza comportare il ricalcolo dell'intero stack), penso di averlo risolto in qualche modo, ma richiede ancora più test per essere al 100%.

In ogni caso, gli attributi sono definiti in questo modo (+ macro irreali, omessi qui):

FGAAttributeBase Health;
FGAAttributeBase Energy;

eccetera.

Inoltre, non sono sicuro al 100% sulla gestione di CurrentValue dell'attributo, ma dovrebbe funzionare. Come stanno adesso.

In ogni caso spero che salverà alcune persone con la testa nella cache, non sono sicuro che questa sia la soluzione migliore o addirittura buona, ma mi piace di più, rispetto al tracciamento degli effetti indipendentemente dagli attributi. Rendere ogni tracciamento degli attributi il ​​suo stato è molto più semplice in questo caso e dovrebbe essere meno soggetto ad errori. Esiste essenzialmente un solo punto di errore, che è una classe piuttosto breve e semplice.


Grazie per il link e la spiegazione del tuo lavoro! Penso che ti stai muovendo essenzialmente verso ciò che stavo chiedendo. Alcune cose che mi vengono in mente sono l'ordine delle operazioni (ad esempio, 3 effetti "aggiungi" e 2 effetti "moltiplica" sullo stesso attributo, che dovrebbe accadere per primo?), E questo è puramente supporto agli attributi. C'è anche la nozione di trigger (come "perdere 1 PA quando si colpisce" tipo di effetti) da affrontare, ma probabilmente sarebbe un'indagine separata.
gkimsey,

L'ordine di funzionamento, nel caso in cui sia sufficiente calcolare il bonus dell'attributo, è facile da fare. Puoi vedere qui che ho lì per e passare. Per iterare su tutti i bonus correnti (che possono essere aggiunti, sottratti, moltiplicati, divisi ecc.), E poi semplicemente accumularli. Fai qualcosa come BonusValue = (BonusValue * MultiplyBonus + AddBonus-SubtractBonus) / DivideBonus, O comunque vuoi guardare questa equazione. Grazie al singolo punto di entrata è facile sperimentarlo. Per quanto riguarda i trigger, non ne ho scritto, perché questo è un altro problema su cui rifletto, e ho già provato 3-4 (limite)
Łukasz Baran,

soluzioni, nessuna delle quali ha funzionato come volevo (il mio obiettivo principale è che siano progettisti). La mia idea generale è quella di utilizzare i tag e controllare gli effetti in entrata sui tag. Se il tag corrisponde, l'effetto può attivare altri effetti. (il tag è un semplice nome leggibile dall'uomo, come Damage.Fire, Attack.Physical ecc.). Fondamentalmente è un concetto molto semplice, il problema è l'organizzazione dei dati, per essere facilmente accessibile (veloce per la ricerca) e la facilità di aggiungere nuovi effetti. Puoi controllare il codice qui github.com/iniside/ActionRPGGame (GameAttributes è il modulo che ti interesserà)
Łukasz Baran,

2

Ho lavorato su un piccolo MMO e tutti gli oggetti, i poteri, i buff, ecc. Hanno avuto "effetti". Un effetto era una classe che aveva variabili per 'AddDefense', 'InstantDamage', 'HealHP', ecc. I poteri, gli oggetti, ecc. Avrebbero gestito la durata di quell'effetto.

Quando si lancia un potere o si mette su un oggetto, si applica l'effetto al personaggio per la durata specificata. Quindi i calcoli dell'attacco principale, ecc. Terranno conto degli effetti applicati.

Ad esempio, hai un buff che aggiunge difesa. Ci sarebbero almeno un EffectID e una durata per quel buff. Quando lo lancia, applica l'Effetto al personaggio per la durata specificata.

Un altro esempio per un articolo avrebbe gli stessi campi. Ma la durata sarebbe infinita o fino a quando l'effetto non verrà rimosso togliendo l'oggetto dal personaggio.

Questo metodo consente di scorrere su un elenco di effetti attualmente applicati.

Spero di aver spiegato questo metodo abbastanza chiaramente.


A quanto ho capito con la mia minima esperienza, questo è il modo tradizionale di implementare le mod statistiche nei giochi di ruolo. Funziona bene ed è facile da capire e implementare. Il rovescio della medaglia è che non sembra lasciarmi spazio per fare cose come il buff "spine", o qualcosa di più avanzato o situazionale. Storicamente è stata anche la causa di alcuni exploit nei giochi di ruolo, anche se sono piuttosto rari, e dal momento che sto realizzando un gioco per giocatore singolo, se qualcuno trova un exploit non sono poi così preoccupato. Grazie per l'input.
gkimsey,

2
  1. Se sei un utente di unità, ecco qualcosa per iniziare: http://www.stevegargolinski.com/armory-a-free-and-unfinished-stat-inventory-and-buffdebuff-framework-for-unity/

Sto usando ScriptableOjects come buff / incantesimi / talenti

public class Spell : ScriptableObject 
{
    public SpellType SpellType = SpellType.Ability;
    public SpellTargetType SpellTargetType = SpellTargetType.SingleTarget;
    public SpellCategory SpellCategory = SpellCategory.Ability;
    public MagicSchools MagicSchool = MagicSchools.Physical;
    public CharacterClass CharacterClass = CharacterClass.None;
    public string Description = "no description available";
    public SpellDragType DragType = SpellDragType.Active; 
    public bool Active = false;
    public int TargetCount = 1;
    public float CastTime = 0;
    public uint EffectRange = 3;
    public int RequiredLevel = 1;
    public virtual void OnGUI()
    {
    }
}

utilizzando UnityEngine; utilizzando System.Collections.Generic;

public enum BuffType {Buff, Debuff} [System.Serializable] classe pubblica BuffStat {stat pubblico Stat = Stat.Strength; float pubblico ModValueInPercent = 0.1f; }

public class Buff : Spell
{
    public BuffType BuffType = BuffType.Buff;
    public BuffStat[] ModStats;
    public bool PersistsThroughDeath = false;
    public int AmountPerTick = 3;
    public bool UseTickTimer = false;
    public float TickTime = 1.5f;
    [HideInInspector]
    public float Ticktimer = 0;
    public float Duration = 360; // in seconds
    public float ModifierPerStack = 1.1f;
    [HideInInspector]
    public float Timer = 0;
    public int Stack = 1;
    public int MaxStack = 1;
}

BuffModul:

using System;
using RPGCore;
using UnityEngine;

public class Buff_Modul : MonoBehaviour
{
    private Unit _unit;

    // Use this for initialization
    private void Awake()
    {
        _unit = GetComponent<Unit>();
    }

    #region BUFF MODUL

    public virtual void RUN_BUFF_MODUL()
    {
        try
        {
            foreach (var buff in _unit.Attr.Buffs)
            {
                CeckBuff(buff);
            }
        }
        catch(Exception e) {throw new Exception(e.ToString());}
    }

    #endregion BUFF MODUL

    public void ClearBuffs()
    {
        _unit.Attr.Buffs.Clear();
    }

    public void AddBuff(string buffName)
    {
        var buff = Instantiate(Resources.Load("Scriptable/Buff/" + buffName, typeof(Buff))) as Buff;
        if (buff == null) return;
        buff.name = buffName;
        buff.Timer = buff.Duration;
        _unit.Attr.Buffs.Add(buff);
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
    }

    public void RemoveBuff(Buff buff)
    {
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat]  /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
        _unit.Attr.Buffs.Remove(buff);
    }

    void CeckBuff(Buff buff)
    {
        buff.Timer -= Time.deltaTime;
        if (!_unit.IsAlive && !buff.PersistsThroughDeath)
        {
            if (buff.ModStats != null)
                foreach (var stat in buff.ModStats)
                {
                    _unit.Attr.StatsBuff[stat.Stat] = 0;
                }

            RemoveBuff(buff);
        }
        if (_unit.IsAlive && buff.Timer <= 0)
        {
            RemoveBuff(buff);
        }
    }
}

0

Questa è stata una vera domanda per me. Ne ho un'idea.

  1. Come detto in precedenza, dobbiamo implementare un Buffelenco e un aggiornamento della logica per i buff.
  2. Abbiamo quindi bisogno di cambiare tutte le impostazioni specifiche del giocatore ogni fotogramma nelle sottoclassi della Buffclasse.
  3. Otteniamo quindi le impostazioni correnti del giocatore dal campo delle impostazioni modificabili.

class Player {
  settings: AllPlayerStats;

  private buffs: Array<Buff> = [];
  private baseSettings: AllPlayerStats;

  constructor(settings: AllPlayerStats) {
    this.baseSettings = settings;
    this.resetSettings();
  }

  addBuff(buff: Buff): void {
    this.buffs.push(buff);
    buff.start(this);
  }

  findBuff(predcate(buff: Buff) => boolean): Buff {...}

  removeBuff(buff: Buff): void {...}

  update(dt: number): void {
    this.resetSettings();
    this.buffs.forEach((item) => item.update(dt));
  }

  private resetSettings(): void {
    //some way to copy base to settings
    this.settings = this.baseSettings.copy();
  }
}

class Buff {
    private owner: Player;        

    start(owner: Player) { this.owner = owner; }

    update(dt: number): void {
      //here we change anything we want in subclasses like
      this.owner.settings.hp += 15;
      //if we need base value, just make owner.baseSettings public but don't change it! only read

      //also here logic for removal buff by time or something
    }
}

In questo modo, può essere facile aggiungere nuove statistiche ai giocatori, senza cambiare la logica delle Buffsottoclassi.


0

So che questo è piuttosto vecchio, ma è stato collegato in un post più recente e ho alcune riflessioni che vorrei condividere. Purtroppo al momento non ho i miei appunti con me, quindi cercherò di dare una panoramica generale di ciò di cui sto parlando e modificherò nei dettagli e alcuni esempi di codice quando li avrò davanti me.

In primo luogo, penso che dal punto di vista del design la maggior parte delle persone sia troppo coinvolta in quali tipi di buff possono essere creati e in che modo vengono applicati e dimenticando i principi di base della programmazione orientata agli oggetti.

Cosa voglio dire? Non importa se qualcosa è un buff o un debuff, sono entrambi modificatori che influenzano qualcosa in modo positivo o negativo. Al codice non importa quale sia quale. In ogni caso, alla fine non importa se qualcosa sta aggiungendo statistiche o moltiplicandole, questi sono solo operatori diversi e di nuovo al codice non importa quale sia.

Quindi dove sto andando con questo? Che progettare una buona classe (leggi: semplice, elegante) buff / debuff non è poi così difficile, ciò che è difficile è progettare i sistemi che calcolano e mantengono lo stato del gioco.

Se stavo progettando un sistema buff / debuff qui ci sono alcune cose che prenderei in considerazione:

  • Una classe buff / debuff per rappresentare l'effetto stesso.
  • Una classe di tipo buff / debuff per contenere le informazioni sull'effetto del buff e su come.
  • Personaggi, oggetti e possibilmente posizioni dovrebbero avere un elenco o una proprietà di raccolta per contenere buff e debuff.

Alcune specifiche per quali tipi di buff / debuff dovrebbero contenere:

  • A chi / a cosa può essere applicato, IE: giocatore, mostro, posizione, oggetto, ecc.
  • Che tipo di effetto è (positivo, negativo), che si tratti di moltiplicativo o additivo, e che tipo di stat influisce, IE: attacco, difesa, movimento, ecc.
  • Quando dovrebbe essere controllato (combattimento, ora del giorno, ecc.).
  • Se può essere rimosso e, in tal caso, come può essere rimosso.

Questo è solo un inizio, ma da lì stai solo definendo ciò che vuoi e agendo su di esso usando il tuo normale stato di gioco. Ad esempio, supponi di voler creare un oggetto maledetto che riduca la velocità di movimento ...

Finché ho messo in atto i tipi corretti è semplice creare un record buff che dice:

  • Tipo: Maledizione
  • ObjectType: Item
  • StatCategory: Utilità
  • StatAffected: MovementSpeed
  • Durata: infinita
  • Trigger: OnEquip

E così via, e quando creo un buff, lo assegno semplicemente BuffType of Curse e tutto il resto dipende dal motore ...

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.