Rendere le abilità e le abilità del personaggio come comandi, buone pratiche?


11

Sto progettando un gioco composto da personaggi che hanno abilità offensive uniche e altre abilità come costruzione, riparazione, ecc. I giocatori possono controllare più di questi personaggi.

Sto pensando di mettere tutte queste abilità e abilità nei singoli comandi. Un controller statico registra tutti questi comandi in un elenco di comandi statici. L'elenco statico consisterebbe in tutte le abilità e abilità disponibili di tutti i personaggi del gioco. Pertanto, quando un giocatore seleziona uno dei personaggi e fa clic su un pulsante nell'interfaccia utente per lanciare una magia o eseguire un'abilità, la vista chiamerebbe il controller statico per recuperare il comando desiderato dall'elenco ed eseguirlo.

Ciò di cui non sono sicuro, tuttavia, se questo è un buon design dato che sto costruendo il mio gioco in Unity. Sto pensando che avrei potuto creare tutte le abilità e le abilità come singoli componenti, che sarebbero stati collegati ai GameObject che rappresentavano i personaggi del gioco. Quindi l'interfaccia utente dovrebbe contenere l'oggetto GameOb del personaggio e quindi eseguire il comando.

Quale sarebbe una migliore progettazione e pratica per un gioco che sto progettando?


Sembra buono! Basta gettare questo fatto correlato là fuori: in alcune lingue, puoi persino arrivare a rendere ogni comando una funzione a sé. Questo ha alcuni incredibili vantaggi per i test, poiché puoi automatizzare facilmente l'input. Inoltre, è possibile eseguire facilmente il rebinding del controllo riassegnando una variabile della funzione di callback a una diversa funzione di comando.
Anko,

@Anko, per quanto riguarda la parte in cui ho tutti i comandi inseriti in un elenco statico? Sono preoccupato che l'elenco possa diventare enorme e ogni volta che è necessario un comando, deve interrogare l'enorme elenco di comandi.
xeno

1
@xenon È molto improbabile vedere problemi di prestazioni in questa parte del codice. Nella misura in cui qualcosa può accadere solo una volta per interazione da parte dell'utente, dovrebbe essere molto intensivo dal punto di vista computazionale per incidere notevolmente sulle prestazioni.
aaaaaaaaaaaa,

Risposte:


17

TL; DR

Questa risposta diventa un po 'folle. Ma è perché vedo che stai parlando dell'implementazione delle tue abilità come "Comandi", il che implica modelli di progettazione C ++ / Java / .NET, che implica un approccio pieno di codice. Quell'approvazione è valida, ma c'è un modo migliore. Forse stai già facendo diversamente. Se è così, vabbè. Speriamo che altri lo trovino utile se è così.

Guarda l'approccio basato sui dati di seguito per andare al sodo. Ottieni qui CustomAssetUility di Jacob Pennock e leggi il suo post al riguardo .

Lavorare con l'unità

Come altri hanno già detto, attraversare un elenco di 100-300 articoli non è un grosso problema come potresti pensare. Quindi se questo è un approccio intuitivo per te, allora fallo. Ottimizza per l'efficienza del cervello. Ma il Dizionario, come ha dimostrato @Norguard nella sua risposta , è il modo semplice e non necessario per il cervello di eliminare quel problema poiché si ottiene l'inserimento e il recupero a tempo costante. Probabilmente dovresti usarlo.

Per quanto riguarda il corretto funzionamento di Unity, il mio istinto mi dice che un comportamento MonoBeur per abilità è un percorso pericoloso da percorrere. Se una qualsiasi delle tue abilità mantiene lo stato nel tempo mentre viene eseguita, dovrai gestirla e fornire un modo per ripristinare quello stato. Le coroutine alleviano questo problema, ma stai ancora gestendo un riferimento IEnumerator su ogni frame di aggiornamento di quello script e devi assolutamente assicurarti di avere un modo sicuro per ripristinare le abilità per non essere incompleto e bloccato in un ciclo di stato le abilità iniziano silenziosamente a rovinare la stabilità del gioco quando passano inosservate. "Certo che lo farò!" dici "Sono un buon programmatore"! Ma davvero, sai, siamo tutti programmatori oggettivamente terribili e persino i più grandi ricercatori e scrittori di compilatori di intelligenza artificiale rovinano tutto il tempo.

Di tutti i modi in cui è possibile implementare l'istanza e il recupero dei comandi in Unity, posso pensare a due: uno va bene e non ti darà un aneurisma, e l'altro consente la CREATIVITÀ MAGICA NON RICONDIZIONATA . Una specie di.

Approccio incentrato sul codice

Il primo è un approccio prevalentemente nel codice. Quello che consiglio è di rendere ogni comando una semplice classe che eredita da una classe abtract di BaseCommand o implementa un'interfaccia ICommand (suppongo per brevità che questi comandi siano sempre e solo abilità di carattere, non è difficile da incorporare altri usi). Questo sistema presuppone che ogni comando sia un comando IC, abbia un costruttore pubblico che non accetta parametri e richiede l'aggiornamento di ciascun frame mentre è attivo.

Le cose sono più semplici se usi una classe base astratta, ma la mia versione usa interfacce.

È importante che i tuoi MonoBehaviours incapsulino un comportamento specifico o un sistema di comportamenti strettamente correlati. Va bene avere un sacco di MonoBehaviour che eseguono effettivamente il proxy su semplici classi C #, ma se ti accorgi di fare anche tu potresti aggiornare le chiamate a tutti i tipi di oggetti diversi al punto in cui inizia a sembrare un gioco XNA, allora tu ' sei in gravi difficoltà e devi cambiare la tua architettura.

// ICommand.cs
public interface ICommand
{
    public void Execute(AbilityActivator originator, TargetingInfo targets);
    public void Update();
    public bool IsActive { get; }
}


// CommandList.cs
// Attach this to a game object in your loading screen
public static class CommandList
{
    public static ICommand GetInstance(string key)
    {
        return commandDict[key].GetRef();
    }


    static CommandListInitializerScript()
    {
        commandDict = new Dictionary<string, ICommand>() {

            { "SwordSpin", new CommandRef<SwordSpin>() },

            { "BellyRub", new CommandRef<BellyRub>() },

            { "StickyShield", new CommandRef<StickyShield>() },

            // Add more commands here
        };
    }


    private class CommandRef<T> where T : ICommand, new()
    {
        public ICommand GetNew()
        {
            return new T();
        }
    }

    private static Dictionary<string, ICommand> commandDict;
}


// AbilityActivator.cs
// Attach this to your character objects
public class AbilityActivator : MonoBehaviour
{
    List<ICommand> activeAbilities = new List<ICommand>();

    void Update()
    {
        string activatedAbility = GetActivatedAbilityThisFrame();
        if (!string.IsNullOrEmpty(acitvatedAbility))
            ICommand command = CommandList.Get(activatedAbility).GetRef();
            command.Execute(this, this.GetTargets());
            activeAbilities.Add(command);
        }

        foreach (var ability in activeAbilities) {
            ability.Update();
        }

        activeAbilities.RemoveAll(a => !a.IsActive);
    }
}

Funziona perfettamente, ma puoi fare di meglio (inoltre, a List<T>non è la struttura di dati ottimale per la memorizzazione di abilità a tempo, potresti volere a LinkedList<T>o a SortedDictionary<float, T>).

Approccio basato sui dati

È probabile che tu possa ridurre gli effetti della tua abilità in comportamenti logici che possono essere parametrizzati. Questo è ciò per cui Unity è stata davvero costruita. Tu, come programmatore, progetti un sistema che poi tu o un designer potete andare e manipolare nell'editor per produrre un'ampia varietà di effetti. Ciò semplificherà notevolmente il "rigging" del codice e si concentrerà esclusivamente sull'esecuzione di un'abilità. Non c'è bisogno di destreggiarsi tra classi di base o interfacce e generici qui. Sarà tutto basato esclusivamente sui dati (il che semplifica anche l'inizializzazione delle istanze di comando).

La prima cosa di cui hai bisogno è uno ScriptableObject in grado di descrivere le tue abilità. ScriptableObjects sono fantastici. Sono progettati per funzionare come MonoBehaviours in quanto è possibile impostare i loro campi pubblici nella finestra di ispezione di Unity e tali modifiche verranno serializzate su disco. Tuttavia, non sono collegati a nessun oggetto e non devono essere collegati a un oggetto di gioco in una scena o istanziati. Sono i secchi di dati generali di Unity. Possono serializzare tipi di base, enumerazioni e classi semplici (nessuna eredità) contrassegnate [Serializable]. Le strutture non possono essere serializzate in Unity e la serializzazione è ciò che ti consente di modificare i campi degli oggetti nella finestra di ispezione, quindi ricorda che.

Ecco uno ScriptableObject che cerca di fare molto. Puoi dividerlo in classi più serializzate e ScriptableObjects, ma ciò dovrebbe darti solo un'idea di come procedere. Normalmente sembra brutto in un bel linguaggio moderno orientato agli oggetti come C #, dal momento che sembra davvero una merda C89 con tutti quegli enum, ma il vero potere qui è che ora puoi creare tutti i tipi di abilità diverse senza mai scrivere un nuovo codice per supportare loro. E se il tuo primo formato non fa quello che ti serve, continua ad aggiungerlo fino a quando non lo fa. Finché non cambi i nomi dei campi, tutti i tuoi vecchi file di risorse serializzati continueranno a funzionare.

// CommandAbilityDescription.cs
public class CommandAbilityDecription : ScriptableObject
{

    // Identification and information
    public string displayName; // Name used for display purposes for the GUI
    // We don't need an identifier field, because this will actually be stored
    // as a file on disk and thus implicitly have its own identifier string.

    // Description of damage to targets

    // I put this enum inside the class for answer readability, but it really belongs outside, inside a namespace rather than nested inside a class
    public enum DamageType
    {
        None,
        SingleTarget,
        SingleTargetOverTime,
        Area,
        AreaOverTime,
    }

    public DamageType damageType;
    public float damage; // Can represent either insta-hit damage, or damage rate over time (depend)
    public float duration; // Used for over-time type damages, or as a delay for insta-hit damage

    // Visual FX
    public enum EffectPlacement
    {
        CenteredOnTargets,
        CenteredOnFirstTarget,
        CenteredOnCharacter,
    }

    [Serializable]
    public class AbilityVisualEffect
    {
        public EffectPlacement placement;
        public VisualEffectBehavior visualEffect;
    }

    public AbilityVisualEffect[] visualEffects;
}

// VisualEffectBehavior.cs
public abtract class VisualEffectBehavior : MonoBehaviour
{
    // When an artist makes a visual effect, they generally make a GameObject Prefab.
    // You can extend this base class to support different kinds of visual effects
    // such as particle systems, post-processing screen effects, etc.
    public virtual void PlayEffect(); 
}

Potresti ulteriormente astrarre la sezione Danno in una classe serializzabile in modo da poter definire abilità che infliggono danno o cure e che abbiano più tipi di danno in un'abilità. L'unica regola è l'ereditarietà a meno che non si utilizzino più oggetti scriptabili e si faccia riferimento ai diversi file di configurazione del danno complesso sul disco.

Hai ancora bisogno dell'AbilityActivator MonoBehaviour, ma ora fa un po 'più di lavoro.

// AbilityActivator.cs
public class AbilityActivator : MonoBehaviour
{
    public void ActivateAbility(string abilityName)
    {
        var command = (CommandAbilityDescription) Resources.Load(string.Format("Abilities/{0}", abilityName));
        ProcessCommand(command);
    }

    private void ProcessCommand(CommandAbilityDescription command)
    {

        foreach (var fx in command.visualEffects) {
            fx.PlayEffect();
        }

        switch(command.damageType) {
            // yatta yatta yatta
        }

        // and so forth, whatever your needs require

        // You could even make a copy of the CommandAbilityDescription
        var myCopy = Object.Instantiate(command);

        // So you can keep track of state changes (ie: damage duration)
    }
}

La parte più fredda

Quindi l'interfaccia e l'inganno generico nel primo approccio funzioneranno bene. Ma per ottenere davvero il massimo da Unity, ScriptableObjects ti porterà dove vuoi essere. Unity è eccezionale in quanto fornisce un ambiente molto coerente e logico per i programmatori, ma ha anche tutte le caratteristiche di immissione dei dati per designer e artisti che si ottengono da GameMaker, UDK, ecc. al.

Il mese scorso, il nostro artista ha preso un tipo di oggetto ScriptableObject che avrebbe dovuto definire il comportamento per diversi tipi di missili homing, combinato con un AnimationCurve e un comportamento che ha fatto librare i missili lungo il terreno e ha creato questo pazzo nuovo spinning-hockey-puck- arma della morte.

Devo ancora tornare indietro e aggiungere un supporto specifico per questo comportamento per assicurarmi che funzioni in modo efficiente. Ma poiché abbiamo creato questa interfaccia di descrizione dei dati generici, è stato in grado di estrarre questa idea dal nulla e metterla in gioco senza che noi programmatori sapessimo anche che stava cercando di farlo fino a quando non si è avvicinato e ha detto: "Ehi ragazzi, guardate a questa bella cosa! " E poiché è stato chiaramente fantastico, sono entusiasta di aggiungere un supporto più solido per questo.


3

TL: DR - se stai pensando di inserire centinaia o migliaia di abilità in un elenco / array che avresti ripetuto, ogni volta che viene chiamata un'azione, per vedere se l'azione esiste e se c'è un personaggio che può eseguilo, quindi leggi di seguito.

Altrimenti, non preoccuparti.
Se stai parlando di 6 personaggi / tipi di carattere e forse 30 abilità, non importa davvero cosa fai, perché il sovraccarico della gestione delle complessità potrebbe effettivamente richiedere più codice e più elaborazione rispetto al semplice scaricamento di tutto in una pila e ordinamento...

Questo è esattamente il motivo per cui @eBusiness suggerisce che è improbabile che si verifichino problemi di prestazioni durante l'invio degli eventi, perché a meno che non ci si stia impegnando molto per farlo, non c'è molto lavoro travolgente qui, rispetto alla trasformazione della posizione di 3- milioni di vertici sullo schermo, ecc ...

Inoltre, questa non è la soluzione , ma piuttosto una soluzione per la gestione di insiemi più grandi di problemi simili ...

Ma...

Tutto dipende da quanto stai realizzando il gioco, quanti personaggi condividono le stesse abilità, quanti personaggi / abilità differenti ci sono, giusto?

Avere le abilità come componenti del personaggio, ma registrarle / annullare la registrazione da un'interfaccia di comando mentre i personaggi si uniscono o lasciano il controllo (o vengono eliminati / ecc.) Ha ancora senso, in un modo molto StarCraft, con tasti di scelta rapida e la carta di comando.

Ho avuto pochissima esperienza con gli script di Unity, ma mi sento molto a mio agio con JavaScript come lingua.
Se lo consentono, perché non avere quell'elenco come un semplice oggetto:

// Command interface wraps this
var registered_abilities = {},

    register = function (name, callback) {
        registered_abilities[name] = callback;
    },
    unregister = function (name) {
        registered_abilities[name] = null;
    },

    call = function (name,/*arr/undef*/params) {
        var callback = registered_abilities[name];
        if (callback) { callback(params); }
    },

    public_interface = {
        register : register,
        unregister : unregister,
        call : call
    };

return public_interface;

E potrebbe essere usato come:

var command_card = new CommandInterface();

// one-time setup
system.listen("register-ability",   command_card.register  );
system.listen("unregister-ability", command_card.unregister);
system.listen("use-action",         command_card.call      );

// init characters
var dave = new PlayerCharacter("Dave"); // Character Factory pulls out Dave + dependencies
dave.init();

Dove la funzione Dave (). Init potrebbe apparire come:

// Inside of Dave class
init = function () {
    // other instance-level stuff ...

    system.notify("register-ability", "repair",  this.Repair );
    system.notify("register-ability", "science", this.Science);
},

die = function () {
    // other clean-up stuff ...

    system.notify("unregister-ability", "repair" );
    system.notify("unregister-ability", "science");
},

resurrect = function () { /* same idea as init */ };

Se ci sono più persone che solo Dave .Repair(), ma puoi garantire che ci sarà solo un Dave, allora cambialo insystem.notify("register-ability", "dave:repair", this.Repair);

E chiama l'abilità usando system.notify("use-action", "dave:repair");

Non sono sicuro di come siano gli elenchi che stai utilizzando. (In termini di sistema di tipo UnityScript, E in termini di ciò che sta succedendo dopo la compilazione).

Probabilmente posso dire che se hai centinaia di abilità che stavi pianificando di inserire nella lista (piuttosto che registrarti e annullare la registrazione, in base ai personaggi che hai attualmente disponibili), che ripeterai attraverso un intero array JS (di nuovo, se è quello che stanno facendo) per verificare una proprietà di una classe / oggetto, che corrisponde al nome dell'azione che si desidera eseguire, sarà meno performante di questo.

Se ci sono strutture più ottimizzate, saranno più performanti di così.

Ma in entrambi i casi, ora hai Personaggi che controllano le proprie azioni (fai un passo avanti e trasformali in componenti / entità, se lo desideri), E hai un sistema di controllo che richiede un minimo di iterazione (dato che sei solo facendo ricerche di tabella per nome).

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.