Faire des compétences et des capacités du personnage des commandes, une bonne pratique?

11

Je conçois un jeu composé de personnages qui ont des compétences offensives uniques et d'autres capacités telles que la construction, la réparation, etc. Les joueurs peuvent contrôler plusieurs de ces personnages.

Je pense mettre toutes ces compétences et capacités dans des commandes individuelles. Un contrôleur statique enregistrerait toutes ces commandes dans une liste de commandes statiques. La liste statique comprendrait toutes les compétences et capacités disponibles de tous les personnages du jeu. Ainsi, lorsqu'un joueur sélectionne l'un des personnages et clique sur un bouton de l'interface utilisateur pour lancer un sort ou exécuter une capacité, la vue appelle le contrôleur statique pour récupérer la commande souhaitée dans la liste et l'exécuter.

Ce dont je ne suis pas sûr cependant si c'est une bonne conception étant donné que je construis mon jeu dans Unity. Je pense que j'aurais pu faire toutes les compétences et capacités en tant que composants individuels, qui seraient ensuite attachés aux GameObjects représentant les personnages du jeu. Ensuite, l'interface utilisateur devrait contenir le GameObject du personnage, puis exécuter la commande.

Quelle serait une meilleure conception et pratique pour un jeu que je conçois?

xénon
la source
Ça a l'air bon! Il suffit de jeter ce fait connexe: dans certains langages, vous pouvez même aller jusqu'à faire de chaque commande une fonction à part entière. Cela présente des avantages impressionnants pour les tests, car vous pouvez facilement automatiser la saisie. En outre, le contrôle de liaison peut être effectué facilement en réaffectant une variable de fonction de rappel à une fonction de commande différente.
Anko
@Anko, qu'en est-il de la partie où j'ai toutes les commandes placées dans une liste statique? Je crains que la liste ne devienne énorme et chaque fois qu'une commande est nécessaire, elle doit interroger la grande liste de commandes.
xénon
1
@xenon Il est très peu probable que vous constatiez des problèmes de performances dans cette partie du code. Dans la mesure où quelque chose ne peut se produire qu'une seule fois par interaction avec l'utilisateur, il devrait être très intensif en calcul pour faire une brèche notable dans les performances.
aaaaaaaaaaaa

Réponses:

17

TL; DR

Cette réponse devient un peu folle. Mais c'est parce que je vois que vous parlez d'implémenter vos capacités en tant que «commandes», ce qui implique des modèles de conception C ++ / Java / .NET, ce qui implique une approche lourde de code. Cette approche est valable, mais il existe un meilleur moyen. Vous faites peut-être déjà l'inverse. Si oui, eh bien. J'espère que d'autres trouveront cela utile si c'est le cas.

Regardez l'approche basée sur les données ci-dessous pour aller droit au but. Obtenez la fonctionnalité CustomAssetUility de Jacob Pennock ici et lisez son article à ce sujet .

Travailler avec Unity

Comme d'autres l'ont mentionné, parcourir une liste de 100 à 300 éléments n'est pas aussi important que vous ne le pensez. Donc, si c'est une approche intuitive pour vous, faites-le. Optimiser pour l'efficacité du cerveau. Mais le dictionnaire, comme @Norguard l'a démontré dans sa réponse , est le moyen facile sans cervelle nécessaire pour éliminer ce problème puisque vous obtenez une insertion et une récupération à temps constant. Vous devriez probablement l'utiliser.

Pour ce qui est de bien fonctionner dans Unity, mon instinct me dit qu'un comportement unique par capacité est un chemin dangereux à suivre. Si l'une de vos capacités maintient son état au fil du temps, elle devra être gérée et fournir un moyen de réinitialiser cet état. Les coroutines atténuent ce problème, mais vous gérez toujours une référence IEnumerator sur chaque cadre de mise à jour de ce script, et vous devez absolument vous assurer d'avoir un moyen sûr de réinitialiser les capacités de peur que la boucle soit incomplète et coincée dans un état. les capacités commencent tranquillement à gâcher la stabilité de votre jeu quand elles passent inaperçues. "Bien sûr que je vais faire ça!" vous dites: "Je suis un" bon programmeur "!". Mais vraiment, vous savez, nous sommes tous des programmeurs objectivement terribles et même les plus grands chercheurs en IA et rédacteurs de compilateurs bousillent tout le temps.

De toutes les façons dont vous pouvez implémenter l'instanciation et la récupération de commandes dans Unity, je peux en penser à deux: l'une va bien et ne vous donnera pas d'anévrisme, et l'autre permet une CRÉATIVITÉ MAGIQUE NON LIMITÉE . Sorte de.

Approche centrée sur le code

La première est une approche principalement dans le code. Ce que je recommande, c'est que vous fassiez de chaque commande une classe simple qui hérite d'une classe abtract BaseCommand ou implémente une interface ICommand (je suppose par souci de concision que ces commandes ne seront que des capacités de caractère, il n'est pas difficile à incorporer autres utilisations). Ce système suppose que chaque commande est une ICommand, possède un constructeur public qui ne prend aucun paramètre et nécessite de mettre à jour chaque trame pendant qu'elle est active.

Les choses sont plus simples si vous utilisez une classe de base abstraite, mais ma version utilise des interfaces.

Il est important que vos comportements uniques encapsulent un comportement spécifique ou un système de comportements étroitement liés. Il est normal d'avoir beaucoup de MonoBehaviours qui procurent simplement un proxy vers des classes C # simples, mais si vous vous trouvez en train de faire trop, vous pouvez mettre à jour les appels à toutes sortes d'objets différents au point où cela commence à ressembler à un jeu XNA, alors vous '' re en difficulté et besoin de changer votre architecture.

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

Cela fonctionne parfaitement, mais vous pouvez faire mieux (aussi, a List<T>n'est pas la structure de données optimale pour stocker des capacités chronométrées, vous voudrez peut-être un LinkedList<T>ou un SortedDictionary<float, T>).

Approche axée sur les données

Il est probablement possible que vous réduisiez les effets de votre capacité en comportements logiques qui peuvent être paramétrés. C'est pour cela que Unity a vraiment été conçu. En tant que programmeur, vous concevez un système que vous ou un concepteur pouvez ensuite manipuler dans l'éditeur pour produire une grande variété d'effets. Cela simplifiera grandement le "truquage" du code et se concentrera exclusivement sur l'exécution d'une capacité. Pas besoin de jongler avec les classes de base ou les interfaces et les génériques ici. Tout sera purement piloté par les données (ce qui simplifie également l'initialisation des instances de commande).

La première chose dont vous avez besoin est un ScriptableObject qui peut décrire vos capacités. ScriptableObjects est génial. Ils sont conçus pour fonctionner comme MonoBehaviours dans la mesure où vous pouvez définir leurs champs publics dans l'inspecteur d'Unity, et ces modifications seront sérialisées sur le disque. Cependant, ils ne sont attachés à aucun objet et n'ont pas besoin d'être attachés à un objet de jeu dans une scène ou instanciés. Ce sont les ensembles de données fourre-tout de Unity. Ils peuvent sérialiser les types de base, les énumérations et les classes simples (sans héritage) marquées [Serializable]. Les structures ne peuvent pas être sérialisées dans Unity, et la sérialisation vous permet de modifier les champs d'objet dans l'inspecteur, alors n'oubliez pas cela.

Voici un ScriptableObject qui essaie de faire beaucoup. Vous pouvez répartir cela en classes plus sérialisées et en objets scriptables, mais cela est censé vous donner simplement une idée de la façon de procéder. Normalement, cela semble moche dans un joli langage orienté objet moderne comme C #, car il ressemble vraiment à de la merde C89 avec toutes ces énumérations, mais le vrai pouvoir ici est que maintenant vous pouvez créer toutes sortes de capacités différentes sans jamais écrire de nouveau code à prendre en charge leur. Et si votre premier format ne fait pas ce dont vous avez besoin, continuez à y ajouter jusqu'à ce qu'il le fasse. Tant que vous ne modifiez pas les noms de champs, tous vos anciens fichiers d'actifs sérialisés fonctionneront toujours.

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

Vous pouvez en outre résumer la section Dommages dans une classe sérialisable afin de définir des capacités qui infligent des dégâts ou guérissent, et ont plusieurs types de dégâts dans une seule capacité. La seule règle n'est pas d'héritage sauf si vous utilisez plusieurs objets scriptables et référencez les différents fichiers de configuration de dommages complexes sur le disque.

Vous avez toujours besoin du comportement unique AbilityActivator, mais maintenant il fait un peu plus de travail.

// 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 partie la plus COOL

Ainsi, l'interface et la supercherie générique dans la première approche fonctionneront bien. Mais pour tirer le meilleur parti d'Unity, ScriptableObjects vous amènera où vous voulez être. Unity est génial en ce qu'il fournit un environnement très cohérent et logique pour les programmeurs, mais a également toutes les subtilités de saisie de données pour les concepteurs et les artistes que vous obtenez de GameMaker, UDK, et. Al.

Le mois dernier, notre artiste a pris un powerup ScriptableObject type qui était censé définir le comportement de différents types de missiles à tête chercheuse, l'a combiné avec une AnimationCurve et un comportement qui a fait planer des missiles sur le sol, et a créé cette nouvelle folle rondelle de hockey-hockey-puck- arme mortelle.

J'ai encore besoin de revenir en arrière et d'ajouter un support spécifique pour ce comportement pour m'assurer qu'il fonctionne efficacement. Mais parce que nous avons créé cette interface de description de données générique, il a réussi à tirer cette idée de nulle part et à la mettre dans le jeu sans que nous, les programmeurs, sachions qu'il essayait de le faire jusqu'à ce qu'il vienne et dit: "Hé les gars, regardez à cette chose cool! " Et parce que c'était clairement génial, je suis ravi d'y ajouter un support plus robuste.

michael.bartnett
la source
3

TL: DR - si vous envisagez de placer des centaines ou des milliers de capacités dans une liste / tableau que vous parcourez ensuite, chaque fois qu'une action est appelée, pour voir si l'action existe et s'il y a un personnage qui peut effectuez-le, puis lisez ci-dessous.

Sinon, ne vous en faites pas.
Si vous parlez de 6 caractères / types de caractères et peut-être de 30 capacités, alors ce ne sera vraiment pas important ce que vous faites, car les frais généraux de gestion des complexités pourraient en fait nécessiter plus de code et plus de traitement que de simplement tout vider dans une pile et tri...

C'est exactement pourquoi @eBusiness suggère que vous ne verrez probablement pas de problèmes de performances lors de la répartition des événements, car à moins que vous n'ayez vraiment du mal à le faire, il n'y a pas beaucoup de travail écrasant ici, par rapport à la transformation de la position de 3- millions de sommets à l'écran, etc ...

De plus, ce n'est pas la solution , mais plutôt une solution pour gérer de plus grands ensembles de problèmes similaires ...

Mais...

Tout dépend de la taille de votre jeu, du nombre de personnages partageant les mêmes compétences, du nombre de personnages / compétences différentes, non?

Faire en sorte que les compétences soient des composants du personnage, mais les faire enregistrer / désinscrire à partir d'une interface de commande lorsque les personnages rejoignent ou quittent votre contrôle (ou se font assommer / etc) est toujours logique, d'une manière très StarCraft, avec des raccourcis clavier et la carte de commande.

J'ai très, très peu d'expérience avec les scripts Unity, mais je suis très à l'aise avec JavaScript en tant que langage.
S'ils le permettent, pourquoi ne pas faire de cette liste un simple objet:

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

Et il pourrait être utilisé comme:

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

Où la fonction Dave (). Init pourrait ressembler:

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

S'il y a plus de personnes que Dave .Repair(), mais que vous pouvez garantir qu'il n'y aura qu'un seul Dave, changez-le simplement ensystem.notify("register-ability", "dave:repair", this.Repair);

Et appelez la compétence en utilisant system.notify("use-action", "dave:repair");

Je ne sais pas à quoi ressemblent les listes que vous utilisez. (En termes de système de types UnityScript, ET en termes de ce qui se passe après la compilation).

Je peux probablement dire que si vous avez des centaines de compétences que vous envisagiez de simplement ajouter à la liste (plutôt que de vous inscrire et de vous désinscrire, en fonction des caractères dont vous disposez actuellement), cette itération à travers un tableau JS entier (encore une fois, si c'est ce qu'ils font) pour vérifier une propriété d'une classe / objet, qui correspond au nom de l'action que vous souhaitez effectuer, sera moins performante que cela.

S'il existe des structures plus optimisées, elles seront plus performantes que cela.

Mais dans les deux cas, vous avez maintenant des personnages qui contrôlent leurs propres actions (aller plus loin et en faire des composants / entités, si vous le souhaitez), ET vous avez un système de contrôle qui nécessite un minimum d'itération (comme vous êtes juste faire des recherches de table par nom).

Norguard
la source