Creazione di un sistema di oggetti robusto


12

Il mio obiettivo è creare un sistema di articoli modulare / il più generico possibile in grado di gestire cose come:

  • Oggetti aggiornabili (+6 Katana)
  • Modificatori stat (+15 destrezza)
  • Modificatori oggetto (% X possibilità di infliggere Y danni, possibilità di congelare)
  • Articoli ricaricabili (personale magico con 30 utilizzi)
  • Imposta oggetti (equipaggia 4 pezzi di set X per attivare la funzione Y)
  • Rarità (comune, unico, leggendario)
  • Disenchantable (si rompe in alcuni materiali di fabbricazione)
  • Craftable (può essere realizzato con determinati materiali)
  • Consumabili (5min% X potenza d'attacco, cure +15 cv)

* Sono stato in grado di risolvere funzionalità che sono audaci nella seguente configurazione.

Ora ho provato ad aggiungere molte opzioni per riflettere ciò che ho in mente. Non ho intenzione di aggiungere tutte queste funzionalità necessarie, ma vorrei essere in grado di implementarle a mio avviso. Anche questi dovrebbero essere compatibili con il sistema di inventario e la serializzazione dei dati.

Sto pianificando di non usare l'ereditarietà ma piuttosto un approccio basato su entità / dati. Inizialmente ho pensato a un sistema che ha:

  • BaseStat: una classe generica che contiene statistiche in movimento (può essere utilizzata anche per statistiche di oggetti e personaggi)
  • Item: una classe che contiene dati come elenco di, nome, tipo di elemento e cose relative a interfaccia utente, actionName, descrizione ecc.
  • IWeapon: interfaccia per l'arma. Ogni arma avrà la sua classe con IWeapon implementato. Questo avrà Attacco e un riferimento alle statistiche del personaggio. Quando l'arma è equipaggiata, i suoi dati (stat della classe dell'oggetto) verranno iniettati nella stat del personaggio (qualunque sia BaseStat, verranno aggiunti alla classe del personaggio come bonus Stat) Quindi, per esempio, vogliamo produrre una spada (pensando di produce classi di oggetti con json), quindi la spada aggiungerà 5 attacchi alle statistiche del personaggio. Quindi abbiamo un BaseStat come ("Attack", 5) (possiamo usare anche enum). Questa statistica verrà aggiunta alla statistica "Attacco" del personaggio come BonusStat (che sarebbe una classe diversa) dopo averla equipaggiata. Quindi una classe chiamata Sword implementa IWeapon verrà creata quando 'La classe dell'oggetto è stata creata. Quindi possiamo iniettare le statistiche dei personaggi in questa spada e quando attacca, può recuperare le statistiche di attacco totali dalle statistiche dei personaggi e infliggere danni nel metodo di attacco.
  • BonusStat: è un modo per aggiungere statistiche come bonus senza toccare BaseStat.
  • IConsumable: stessa logica di IWeapon. L'aggiunta di statistiche dirette è abbastanza semplice (+15 CV) ma non sono sicuro di aggiungere armi temporanee con questa configurazione (% x per attaccare per 5 minuti).
  • IUpgradeable: questo può essere implementato con questa configurazione. Sto pensando a UpgradeLevel come una statistica di base, che aumenta con l'aggiornamento dell'arma. Una volta aggiornato, possiamo ricalcolare la BaseStat dell'arma in modo che corrisponda al suo livello di Aggiornamento.

Fino a questo punto, posso vedere che il sistema è abbastanza buono. Ma per altre funzionalità, penso che abbiamo bisogno di qualcos'altro, perché ad esempio non riesco a implementare la funzionalità Craftable in questo in quanto il mio BaseStat non sarebbe in grado di gestire questa funzione ed è qui che mi sono bloccato. Posso aggiungere tutti gli ingredienti come Stat ma non avrebbe senso.

Per semplificare il tuo contributo, ecco alcune domande che potresti aiutare con:

  • Devo continuare con questa configurazione per implementare altre funzionalità? Sarebbe possibile senza eredità?
  • C'è un modo in cui ti viene in mente di implementare tutte queste funzionalità senza ereditarietà?
  • A proposito dei modificatori di oggetti, come si può ottenere questo? Perché è molto generico nella sua natura.
  • Cosa si può fare per facilitare il processo di costruzione di questo tipo di architettura, eventuali raccomandazioni?
  • Ci sono fonti che posso scavare che sono legate a questo problema?
  • Cerco davvero di evitare l'ereditarietà, ma pensi che questi sarebbero risolti / raggiunti con eredità con facilità, mantenendola abbastanza mantenibile?

Sentiti libero di rispondere a una sola domanda mentre ho tenuto le domande molto ampie in modo da poter ottenere conoscenze da diversi aspetti / persone.


MODIFICARE


Seguendo la risposta di @ jjimenezg93, ho creato un sistema molto semplice in C # per i test, funziona! Vedi se puoi aggiungere qualcosa:

public interface IItem
{
    List<IAttribute> Components { get; set; }

    void ReceiveMessage<T>(T message);
}

public interface IAttribute
{
    IItem source { get; set; }
    void ReceiveMessage<T>(T message);
}

Finora, IItem e IAttribute sono interfacce di base. Non c'era bisogno (che io possa pensare) di avere un'interfaccia / attributo di base per il messaggio, quindi creeremo direttamente una classe di messaggi di prova. Ora per le classi di test:


public class TestItem : IItem
{
    private List<IAttribute> _components = new List<IAttribute>();
    public List<IAttribute> Components
    {
        get
        {
            return _components;
        }

        set
        {
            _components = value;
        }
    }

    public void ReceiveMessage<T>(T message)
    {
        foreach (IAttribute attribute in Components)
        {
            attribute.ReceiveMessage(message);
        }
    }
}

public class TestAttribute : IAttribute
{
    string _infoRequiredFromMessage;

    public TestAttribute(IItem source)
    {
        _source = source;
    }

    private IItem _source;
    public IItem source
    {
        get
        {
            return _source;
        }

        set
        {
            _source = value;
        }
    }

    public void ReceiveMessage<T>(T message)
    {
        TestMessage convertedMessage = message as TestMessage;
        if (convertedMessage != null)
        {
            convertedMessage.Execute();
            _infoRequiredFromMessage = convertedMessage._particularInformationThatNeedsToBePassed;
            Debug.Log("Message passed : " + _infoRequiredFromMessage);

        }
    }
} 

public class TestMessage
{
    private string _messageString;
    private int _messageInt;
    public string _particularInformationThatNeedsToBePassed;
    public TestMessage(string messageString, int messageInt, string particularInformationThatNeedsToBePassed)
    {
        _messageString = messageString;
        _messageInt = messageInt;
        _particularInformationThatNeedsToBePassed = particularInformationThatNeedsToBePassed;
    }
    //messages should not have methods, so this is here for fun and testing.
    public void Execute()
    {
        Debug.Log("Desired Execution Method: \nThis is test message : " + _messageString + "\nThis is test int : " + _messageInt);
    }
} 

Queste sono le impostazioni necessarie. Ora possiamo usare il sistema (segue è per Unity).

public class TestManager : MonoBehaviour
{

    // Use this for initialization
    void Start()
    {
        TestItem testItem = new TestItem();
        TestAttribute testAttribute = new TestAttribute(testItem);
        testItem.Components.Add(testAttribute);
        TestMessage testMessage = new TestMessage("my test message", 1, "VERYIMPORTANTINFO");
        testItem.ReceiveMessage(testMessage);
    }

}

Allega questo script TestManager a un componente nella scena e puoi vedere nel debug che il messaggio è stato passato con successo.


Per spiegare le cose: ogni oggetto del gioco implementerà l'interfaccia IItem e ogni attributo (il nome non deve confonderti, significa che l'elemento / il sistema dell'elemento. Come aggiornabile o disincantabile) implementerà IAttribute. Quindi abbiamo un metodo per elaborare il messaggio (il motivo per cui abbiamo bisogno del messaggio verrà spiegato in un ulteriore esempio). Quindi, nel contesto, puoi associare gli attributi a un elemento e lasciare che il resto faccia per te. Che è molto flessibile, perché puoi aggiungere / rimuovere gli attributi a tuo agio. Quindi uno pseudo-esempio sarebbe Disenchantable. Avremo una classe chiamata Disenchantable (IAttribute) e nel metodo Disenchant, chiederà:

  • Elenca gli ingredienti (quando l'oggetto è disincantato, quale oggetto dovrebbe essere dato al giocatore) nota: IItem dovrebbe essere esteso per avere ItemType, ItemID ecc.
  • int resultModifier (se si implementa una sorta di potenziamento della funzione di disincanto, è possibile passare un int qui per aumentare gli ingredienti ricevuti quando disincantati)
  • int failureChance (se il processo di disincanto ha una probabilità di errore)

eccetera.

Queste informazioni saranno fornite da una classe chiamata DisenchantManager, riceverà l'oggetto e formerà questo messaggio in base all'oggetto (ingredienti dell'articolo quando disincantato) e alla progressione del giocatore (resultModifier e failureChance). Per passare questo messaggio, creeremo una classe DisenchantMessage, che fungerà da corpo per questo messaggio. Quindi DisenchantManager popolerà un DisenchantMessage e lo invierà all'elemento. L'articolo riceverà il messaggio e lo passerà a tutti i suoi attributi allegati. Poiché il metodo ReceiveMessage della classe Disenchantable cercherà un DisenchantMessage, solo l'attributo Disenchantable riceverà questo messaggio e agirà su di esso. Spero che questo cancella le cose tanto quanto ha fatto per me :).


1
Potresti trovare qualche utile ispirazione nei sistemi di modifica usati in For Honor
DMGregory

@DMGregory Hey! Grazie per il link Anche se sembra molto intraprendente, sfortunatamente ho bisogno del discorso reale per afferrare il concetto. E provo a scoprire il discorso vero e proprio, che sfortunatamente è il contenuto riservato ai membri di GDCVault (495 $ per un anno sono pazzi!) (Puoi trovare il discorso qui se hai l'iscrizione a GDCVault -, -
Vandarthul,

In che modo esattamente il tuo concetto di "BaseStat" escluderebbe le armi costruibili?
Attackfarm,

Non preclude davvero, ma non si adatta davvero al contesto nella mia mente. È possibile aggiungere "Legno", 2 e "Ferro", 5 come BaseStat a una ricetta di fabbricazione, che darebbe la spada. Penso che cambiare il nome di BaseStat in BaseAttribute servirebbe meglio in questo contesto. Tuttavia, il sistema non sarà in grado di servire al suo scopo. Pensa all'oggetto di consumo con 5min -% 50 di potenza d'attacco. Come lo passerei come BaseStat? "Perc_AttackPower", 50 questo deve essere risolto come "se è Perc, considera il numero intero come una percentuale" e manca le informazioni dei minuti. O spero che tu capisca cosa intendo.
Vandarthul,

@Attackfarm a ripensarci, questo concetto di "BaseStat" può essere ampliato con un elenco di ints invece di un solo int. quindi, per il buff di consumo, posso fornire "Attack", 50, 5, 1 e IConsumable cercherebbe 3 numeri interi, 1. - valore, 2. - minuti, 3. - se è una percentuale o meno. Ma sembra impellente mentre altri sistemi entrano e costretti a spiegarsi solo in int.
Vandarthul,

Risposte:


6

Penso che tu possa ottenere ciò che desideri in termini di scalabilità e manutenibilità utilizzando un sistema Entity-Component con eredità di base e un sistema di messaggistica. Naturalmente, tieni presente che questo sistema è il più modulare / personalizzabile / scalabile a cui riesco a pensare, ma probabilmente funzionerà peggio della tua attuale soluzione.

Spiegherò ulteriormente:

Prima di tutto, crei un'interfaccia IIteme un'interfaccia IComponent. Qualsiasi articolo che desideri archiviare deve ereditare IIteme qualsiasi componente che desideri influire sui tuoi articoli deve ereditare IComponent.

IItemavrà una matrice di componenti e un metodo per la gestione IMessage. Questo metodo di gestione invia semplicemente qualsiasi messaggio ricevuto a tutti i componenti memorizzati. Quindi, i componenti interessati a quel determinato messaggio agiranno di conseguenza e gli altri lo ignoreranno.

Un messaggio, ad esempio, è di tipo danno, e informa sia l'attaccante che l'attaccante, quindi sai quanto colpisci e forse carichi la tua barra della furia in base a quel danno. Oppure l'IA del nemico può decidere di scappare se ti colpisce e fa meno di 2HP di danno. Questi sono esempi stupidi, ma usando un sistema simile a quello che sto citando, non dovrai fare altro che creare un messaggio e i gestori appropriati per aggiungere la maggior parte di questo tipo di meccanica.

Ho un'implementazione per un ECS con messaggistica qui , ma questo è usato per entità invece di oggetti e usa C ++. In ogni caso, penso che possa aiutare se si dà un'occhiata a component.h, entity.he messages.h. Ci sono molte cose da migliorare, ma ha funzionato per me in quel semplice lavoro universitario.

Spero che sia d'aiuto.


Ehi @ jjimenezg93, grazie per la tua risposta. Quindi, al fine di elaborare con un semplice esempio di ciò che hai spiegato: Vogliamo una spada che sia: - Disenchantable [Component] - Stat Modifier [Component] - Upgradeable [Component] Ho un elenco di azioni che contiene tutte le cose come - DISENCHANT - MODIFY_STAT - UPGRADE Ogni volta che l'oggetto riceve questo messaggio, passa attraverso tutti i suoi componenti e invia questo messaggio, ogni componente saprà cosa fare con un determinato messaggio. In teoria, questo sembra fantastico! Non ho controllato il tuo esempio ma lo farò, grazie mille!
Vandarthul,

@Vandarthul Sì, questa è fondamentalmente l'idea. In questo modo, l'oggetto non saprà nulla dei suoi componenti, quindi non c'è alcun accoppiamento e, allo stesso tempo, avrà tutte le funzionalità che desideri, che possono anche essere condivise tra diversi tipi di elementi. Spero si adatti alle tue esigenze!
jjimenezg93,
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.