Die Fähigkeiten und Fertigkeiten des Charakters als Befehle einsetzen, gute Praxis?


11

Ich entwerfe für ein Spiel, das aus Charakteren besteht, die über einzigartige Offensivfähigkeiten und andere Fähigkeiten wie Bauen, Reparieren usw. verfügen. Spieler können mehrere solcher Charaktere steuern.

Ich denke darüber nach, all diese Fähigkeiten und Fertigkeiten in einzelne Befehle zu integrieren. Ein statischer Controller würde alle diese Befehle in einer statischen Befehlsliste registrieren. Die statische Liste würde aus allen verfügbaren Fähigkeiten und Fertigkeiten aller Charaktere im Spiel bestehen. Wenn ein Spieler einen der Charaktere auswählt und auf eine Schaltfläche auf der Benutzeroberfläche klickt, um einen Zauber zu wirken oder eine Fähigkeit auszuführen, ruft die Ansicht den statischen Controller auf, um den gewünschten Befehl aus der Liste abzurufen und auszuführen.

Was ich mir jedoch nicht sicher bin, ob dies ein gutes Design ist, da ich mein Spiel in Unity baue. Ich denke, ich hätte alle Fähigkeiten und Fertigkeiten als einzelne Komponenten erstellen können, die dann an die GameObjects angehängt würden, die die Charaktere im Spiel darstellen. Dann müßte die UI halten , die Gameobject des Charakters und dann den Befehl auszuführen.

Was wäre ein besseres Design und eine bessere Übung für ein Spiel, das ich entwerfe?


Hört sich gut an! Wirf einfach diese verwandte Tatsache raus: In einigen Sprachen kannst du sogar so weit gehen, jeden Befehl zu einer Funktion für sich zu machen. Dies hat einige großartige Vorteile beim Testen, da Sie die Eingabe einfach automatisieren können. Das erneute Binden der Steuerung kann auch einfach durchgeführt werden, indem eine Rückruffunktionsvariable einer anderen Befehlsfunktion zugewiesen wird.
Anko

@Anko, was ist mit dem Teil, in dem ich alle Befehle in eine statische Liste eingefügt habe? Ich mache mir Sorgen, dass die Liste sehr umfangreich wird und jedes Mal, wenn ein Befehl benötigt wird, die große Liste von Befehlen abgefragt werden muss.
Xenon

1
@xenon Es ist sehr unwahrscheinlich, dass in diesem Teil des Codes Leistungsprobleme auftreten. Soweit etwas nur einmal pro Benutzerinteraktion passieren kann, müsste es sehr rechenintensiv sein, um die Leistung spürbar zu beeinträchtigen.
aaaaaaaaaaaa

Antworten:


17

TL; DR

Diese Antwort wird ein bisschen verrückt. Aber es liegt daran, dass Sie über die Implementierung Ihrer Fähigkeiten als "Befehle" sprechen, was C ++ / Java / .NET-Entwurfsmuster impliziert, was einen codeintensiven Ansatz impliziert. Dieser Ansatz ist gültig, aber es gibt einen besseren Weg. Vielleicht machst du schon den anderen Weg. Wenn ja, na ja. Hoffentlich finden es andere nützlich, wenn dies der Fall ist.

Schauen Sie sich den datengesteuerten Ansatz unten an, um auf den Punkt zu kommen. Holen Sie sich Jacob Pennocks CustomAssetUility hier und lesen Sie seinen Beitrag darüber .

Mit der Einheit arbeiten

Wie andere bereits erwähnt haben, ist das Durchsuchen einer Liste mit 100 bis 300 Elementen keine so große Sache, wie Sie vielleicht denken. Wenn dies für Sie ein intuitiver Ansatz ist, tun Sie dies einfach. Optimieren Sie die Effizienz Ihres Gehirns. Aber das Wörterbuch ist, wie @Norguard in seiner Antwort gezeigt hat , die einfache Möglichkeit, dieses Problem ohne Gehirnleistung zu beseitigen, da Sie zeitlich konstant einfügen und abrufen können. Sie sollten es wahrscheinlich verwenden.

Um diese Funktion in Unity gut zu machen, sagt mir mein Bauch, dass ein MonoBehaviour pro Fähigkeit ein gefährlicher Weg ist. Wenn eine Ihrer Fähigkeiten den Status im Laufe der Zeit beibehält und ausgeführt wird, müssen Sie dies verwalten und eine Möglichkeit zum Zurücksetzen dieses Status bereitstellen. Coroutinen lindern dieses Problem, aber Sie verwalten immer noch eine IEnumerator-Referenz in jedem Update-Frame dieses Skripts und müssen unbedingt sicherstellen, dass Sie die Fähigkeiten auf sichere Weise zurücksetzen können, damit sie nicht unvollständig sind und in einer Statusschleife stecken bleiben Fähigkeiten beeinträchtigen leise die Stabilität Ihres Spiels, wenn sie unbemerkt bleiben. "Natürlich mache ich das!" Sie sagen: "Ich bin ein guter Programmierer!" Aber wirklich, wissen Sie, wir sind alle objektiv schreckliche Programmierer und selbst die größten KI-Forscher und Compiler-Autoren vermasseln ständig Dinge.

Von allen Möglichkeiten, wie Sie die Instanziierung und den Abruf von Befehlen in Unity implementieren können, kann ich mir zwei vorstellen : Eine ist in Ordnung und gibt Ihnen kein Aneurysma, und die andere ermöglicht UNBOUNDED MAGICAL CREATIVITY . Art von.

Code-zentrierter Ansatz

Erstens ist ein größtenteils im Code enthaltener Ansatz. Ich empfehle, dass Sie jeden Befehl zu einer einfachen Klasse machen, die entweder von einer BaseCommand abtract-Klasse erbt oder eine ICommand-Schnittstelle implementiert (der Kürze halber gehe ich davon aus, dass diese Befehle immer nur Zeichenfähigkeiten sind, es ist nicht schwer zu integrieren andere Verwendungen). Dieses System geht davon aus, dass jeder Befehl ein ICommand ist, einen öffentlichen Konstruktor hat, der keine Parameter akzeptiert, und dass jeder Frame aktualisiert werden muss, während er aktiv ist.

Die Dinge sind einfacher, wenn Sie eine abstrakte Basisklasse verwenden, aber meine Version verwendet Schnittstellen.

Es ist wichtig, dass Ihre MonoBehaviours ein bestimmtes Verhalten oder ein System eng verwandter Verhaltensweisen enthalten. Es ist in Ordnung, viele MonoBehaviours zu haben, die sich effektiv nur auf einfache C # -Klassen übertragen lassen. Wenn Sie dies jedoch auch tun, können Sie Aufrufe an alle möglichen Objekte so weit aktualisieren, dass sie wie ein XNA-Spiel aussehen. Sie sind in ernsthaften Schwierigkeiten und müssen Ihre Architektur ändern.

// 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);
    }
}

Dies funktioniert völlig in Ordnung, aber Sie können es besser machen (außerdem ist a List<T>nicht die optimale Datenstruktur zum Speichern zeitgesteuerter Fähigkeiten, möglicherweise möchten Sie a LinkedList<T>oder a SortedDictionary<float, T>).

Datengesteuerter Ansatz

Es ist wahrscheinlich möglich, dass Sie die Auswirkungen Ihrer Fähigkeiten auf logische Verhaltensweisen reduzieren können, die parametrisiert werden können. Dafür wurde Unity wirklich gebaut. Als Programmierer entwerfen Sie ein System, das Sie oder ein Designer im Editor bearbeiten können, um eine Vielzahl von Effekten zu erzielen. Dies wird das "Rigging" des Codes stark vereinfachen und sich ausschließlich auf die Ausführung einer Fähigkeit konzentrieren. Hier müssen keine Basisklassen oder Schnittstellen und Generika jongliert werden. Es wird alles rein datengesteuert sein (was auch das Initialisieren von Befehlsinstanzen vereinfacht).

Das erste, was Sie brauchen, ist ein ScriptableObject, das Ihre Fähigkeiten beschreiben kann. ScriptableObjects sind fantastisch. Sie funktionieren wie MonoBehaviours, da Sie ihre öffentlichen Felder im Unity-Inspektor festlegen können und diese Änderungen auf die Festplatte serialisiert werden. Sie sind jedoch keinem Objekt zugeordnet und müssen nicht an ein Spielobjekt in einer Szene angehängt oder instanziiert werden. Sie sind die Sammeldatenbereiche von Unity. Sie können markierte Basistypen, Aufzählungen und einfache Klassen (keine Vererbung) serialisieren [Serializable]. Strukturen können in Unity nicht serialisiert werden. Durch Serialisierung können Sie die Objektfelder im Inspektor bearbeiten. Denken Sie also daran.

Hier ist ein ScriptableObject, das viel zu tun versucht. Sie können dies in mehr serialisierte Klassen und ScriptableObjects aufteilen, aber dies soll Ihnen nur eine Vorstellung davon geben, wie Sie vorgehen müssen. Normalerweise sieht dies in einer schönen modernen objektorientierten Sprache wie C # hässlich aus, da es sich wirklich wie C89-Scheiße mit all diesen Aufzählungen anfühlt, aber die wahre Stärke hier ist, dass Sie jetzt alle möglichen Fähigkeiten erstellen können, ohne jemals neuen Code zur Unterstützung zu schreiben Sie. Und wenn Ihr erstes Format nicht das tut, was Sie brauchen, fügen Sie es einfach hinzu, bis es es tut. Solange Sie die Feldnamen nicht ändern, funktionieren alle Ihre alten serialisierten Asset-Dateien weiterhin.

// 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(); 
}

Sie können den Abschnitt "Schaden" weiter in eine serialisierbare Klasse abstrahieren, um Fähigkeiten zu definieren, die Schaden verursachen oder heilen, und mehrere Schadensarten in einer Fähigkeit zu haben. Die einzige Regel ist keine Vererbung, es sei denn, Sie verwenden mehrere skriptfähige Objekte und verweisen auf die verschiedenen Konfigurationsdateien für komplexe Schäden auf der Festplatte.

Sie benötigen noch den AbilityActivator MonoBehaviour, aber jetzt erledigt er etwas mehr Arbeit.

// 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)
    }
}

Der coolste Teil

Die Benutzeroberfläche und die generischen Tricks im ersten Ansatz funktionieren also einwandfrei. Um Unity wirklich optimal nutzen zu können, bringt Sie ScriptableObjects dahin, wo Sie sein möchten. Unity ist insofern großartig, als es Programmierern eine sehr konsistente und logische Umgebung bietet, aber auch alle Feinheiten der Dateneingabe für Designer und Künstler bietet, die Sie von GameMaker, UDK, et. al.

Letzten Monat nahm unser Künstler einen Powerup-ScriptableObject-Typ, der das Verhalten für verschiedene Arten von Zielsuchraketen definieren sollte, kombinierte ihn mit einer AnimationCurve und einem Verhalten, das Raketen über den Boden schweben ließ, und machte diesen verrückten neuen Spinning-Hockey-Puck- Todeswaffe.

Ich muss noch zurückgehen und spezifische Unterstützung für dieses Verhalten hinzufügen, um sicherzustellen, dass es effizient ausgeführt wird. Aber weil wir diese generische Datenbeschreibungsoberfläche erstellt haben, konnte er diese Idee aus dem Nichts herausholen und ins Spiel bringen, ohne dass wir Programmierer wussten, dass er es versuchte, bis er vorbeikam und sagte: "Hey Leute, schaut bei dieser coolen Sache! " Und weil es eindeutig großartig war, freue ich mich darauf, eine robustere Unterstützung dafür hinzuzufügen.


3

TL: DR - Wenn Sie darüber nachdenken, Hunderte oder Tausende von Fähigkeiten in eine Liste / ein Array zu packen, die Sie dann jedes Mal durchlaufen würden, wenn eine Aktion aufgerufen wird, um zu sehen, ob die Aktion existiert und ob es einen Charakter gibt, der dies kann Führen Sie es aus und lesen Sie weiter unten.

Wenn nicht, dann mach dir keine Sorgen.
Wenn Sie über 6 Zeichen / Zeichentypen und vielleicht 30 Fähigkeiten sprechen, spielt es keine Rolle, was Sie tun, da der Aufwand für die Verwaltung von Komplexitäten möglicherweise mehr Code und mehr Verarbeitung erfordert, als nur alles auf einen Stapel zu werfen und Sortierung...

Genau aus diesem Grund weist @eBusiness darauf hin, dass während des Event-Versands wahrscheinlich keine Leistungsprobleme auftreten. Wenn Sie sich nicht wirklich darum bemühen, gibt es hier nicht viel überwältigende Arbeit, verglichen mit der Veränderung der Position von 3- Millionen Eckpunkte auf dem Bildschirm usw.

Dies ist auch nicht die Lösung , sondern eine Lösung für die Verwaltung größerer Mengen ähnlicher Probleme ...

Aber...

Es kommt darauf an, wie groß du das Spiel machst, wie viele Charaktere die gleichen Fähigkeiten haben, wie viele verschiedene Charaktere / verschiedene Fähigkeiten es gibt, oder?

Es ist immer noch sehr sinnvoll, wenn die Fähigkeiten Komponenten des Charakters sind, aber wenn sie sich über eine Befehlsoberfläche registrieren / die Registrierung aufheben, wenn Charaktere sich Ihrer Kontrolle anschließen oder diese verlassen (oder ausgeschaltet werden / usw.), ist dies in einer Art StarCraft-Art mit Hotkeys und immer noch sinnvoll die Befehlskarte.

Ich habe sehr, sehr wenig Erfahrung mit Unitys Skripten, aber ich bin sehr vertraut mit JavaScript als Sprache.
Wenn sie es zulassen, warum sollte diese Liste kein einfaches Objekt sein:

// 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;

Und es könnte verwendet werden wie:

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();

Wo die Dave (). Init-Funktion aussehen könnte:

// 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 */ };

Wenn mehr Leute als nur Dave haben .Repair(), aber Sie können garantieren, dass es nur einen Dave geben wird, ändern Sie ihn einfach insystem.notify("register-ability", "dave:repair", this.Repair);

Und nennen Sie die Fähigkeit mit system.notify("use-action", "dave:repair");

Ich bin mir nicht sicher, wie die von Ihnen verwendeten Listen aussehen. (In Bezug auf das UnityScript-Typsystem UND in Bezug auf die Nachkompilierung).

Ich kann wahrscheinlich sagen, dass, wenn Sie Hunderte von Fähigkeiten haben, die Sie nur in die Liste einfügen wollten (anstatt sich zu registrieren und die Registrierung aufzuheben, basierend auf den derzeit verfügbaren Zeichen), diese durch ein ganzes JS-Array iterieren (wieder) Wenn sie genau das tun, um eine Eigenschaft einer Klasse / eines Objekts zu überprüfen, die mit dem Namen der Aktion übereinstimmt, die Sie ausführen möchten, ist dies weniger performant.

Wenn es optimierte Strukturen gibt, sind sie leistungsfähiger.

In beiden Fällen haben Sie jetzt Charaktere, die ihre eigenen Aktionen steuern (gehen Sie noch einen Schritt weiter und machen Sie sie zu Komponenten / Entitäten, wenn Sie dies wünschen), UND Sie haben ein Kontrollsystem, das ein Minimum an Iteration erfordert (wie Sie es gerade sind) Tabellensuche nach Namen).

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.