Tornar as habilidades e habilidades do personagem como comandos, boas práticas?

11

Estou desenvolvendo um jogo que consiste em personagens que possuem habilidades ofensivas únicas e outras habilidades, como construção, reparo etc. Os jogadores podem controlar vários desses personagens.

Estou pensando em colocar todas essas habilidades e habilidades em comandos individuais. Um controlador estático registraria todos esses comandos em uma lista de comandos estáticos. A lista estática consistiria em todas as habilidades disponíveis de todos os personagens do jogo. Portanto, quando um jogador seleciona um dos personagens e clica em um botão na interface do usuário para lançar um feitiço ou executar uma habilidade, o View chama o controlador estático para buscar o comando desejado da lista e executá-lo.

No entanto, não tenho certeza se esse é um bom design, pois estou construindo meu jogo no Unity. Eu acho que poderia ter feito todas as habilidades e componentes como componentes individuais, que seriam anexados aos GameObjects que representam os personagens do jogo. Em seguida, a interface do usuário precisaria conter o GameObject do personagem e, em seguida, executar o comando.

Qual seria um melhor design e prática para um jogo que estou criando?

xenon
fonte
Parece bom! Apenas divulgando esse fato relacionado: em algumas linguagens, você pode ir tão longe quanto tornar cada comando uma função para si mesmo. Isso tem algumas vantagens impressionantes para teste, pois você pode automatizar facilmente a entrada. Além disso, a religação de controle pode ser feita facilmente reatribuindo uma variável de função de retorno de chamada para uma função de comando diferente.
Anko
@ Anko, e a parte em que tenho todos os comandos colocados em uma lista estática? Estou preocupado que a lista possa ficar enorme e toda vez que um comando for necessário, ele precisará consultar a enorme lista de comandos.
Xenon
1
@ xenon É muito improvável que você veja problemas de desempenho nesta parte do código. Na medida em que algo só pode acontecer uma vez por interação do usuário, seria muito intensivo em computação para causar um impacto perceptível no desempenho.
Aaaaaaaaaaaa

Respostas:

17

TL; DR

Esta resposta fica um pouco louca. Mas é porque vejo que você está falando sobre a implementação de suas habilidades como "Comandos", o que implica padrões de design em C ++ / Java / .NET, o que implica uma abordagem pesada de código. Essa abordagem é válida, mas há uma maneira melhor. Talvez você já esteja fazendo o contrário. Se sim, tudo bem. Espero que outros achem útil se for esse o caso.

Veja a Abordagem Orientada a Dados abaixo para ir direto ao ponto. Obtenha o CustomAssetUility de Jacob Pennock aqui e leia seu post sobre isso .

Trabalhando com Unidade

Como outros já mencionaram, percorrer uma lista de 100 a 300 itens não é tão importante quanto você imagina. Portanto, se essa é uma abordagem intuitiva para você, faça isso. Otimize para a eficiência do cérebro. Mas o Dicionário, como o @Norguard demonstrou em sua resposta , é a maneira fácil e sem necessidade de inteligência para eliminar esse problema, já que você obtém inserção e recuperação em tempo constante. Você provavelmente deveria usá-lo.

Em termos de tornar esse trabalho bem dentro do Unity, meu instinto me diz que um MonoBehaviour por habilidade é um caminho perigoso a seguir. Se alguma de suas habilidades mantiver o estado ao longo do tempo e executar, você precisará gerenciar isso e fornecer uma maneira de redefinir esse estado. As corotinas atenuam esse problema, mas você ainda está gerenciando uma referência IEnumerator em todos os quadros de atualização desse script e precisa ter certeza absoluta de que tem uma maneira certa de redefinir as habilidades, a fim de que as lacunas sejam incompletas e presas em um estado As habilidades silenciosamente começam a estragar a estabilidade do seu jogo quando passam despercebidas. "Claro que vou fazer isso!" você diz: "Sou um 'bom programador'!". Mas, realmente, você sabe, somos todos programadores objetivamente terríveis e até os maiores pesquisadores e escritores de compiladores de IA estragam tudo o tempo todo.

De todas as maneiras pelas quais você pode implementar instanciação e recuperação de comando no Unity, posso pensar em duas: uma é boa e não lhe dará um aneurisma, e a outra permite a CRIATIVIDADE MÁGICA NÃO LIMITADA . Tipo de.

Abordagem centrada em código

Primeiro, é uma abordagem principalmente em código. O que eu recomendo é que você torne cada comando uma classe simples que herda de uma classe BaseCommand abtract ou implemente uma interface ICommand (estou assumindo, por uma questão de brevidade, que esses comandos serão apenas habilidades de personagem, não é difícil incorporar outros usos). Este sistema assume que cada comando é um ICommand, possui um construtor público que não aceita parâmetros e requer atualização de cada quadro enquanto está ativo.

As coisas são mais simples se você usar uma classe base abstrata, mas minha versão usa interfaces.

É importante que seus MonoBehaviours encapsulem um comportamento específico, ou um sistema de comportamentos intimamente relacionados. Não há problema em ter muitos MonoBehaviours que efetivamente apenas proxy para classes C # simples, mas se você também estiver fazendo isso, pode atualizar chamadas para todos os tipos de objetos diferentes, a ponto de começar a parecer um jogo XNA, então você ' está com sérios problemas e precisa mudar sua arquitetura.

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

Isso funciona totalmente bem, mas você pode fazer melhor (também, a List<T>não é a estrutura de dados ideal para armazenar habilidades cronometradas, você pode querer um LinkedList<T>ou um SortedDictionary<float, T>).

Abordagem orientada a dados

Provavelmente, é possível reduzir os efeitos de sua habilidade em comportamentos lógicos que podem ser parametrizados. É para isso que o Unity foi realmente construído. Você, como programador, cria um sistema que você ou um designer pode manipular no editor para produzir uma ampla variedade de efeitos. Isso simplificará muito o "rigging" do código e se concentrará exclusivamente na execução de uma habilidade. Não há necessidade de manipular classes básicas ou interfaces e genéricos aqui. Tudo será puramente orientado a dados (o que também simplifica a inicialização de instâncias de comando).

A primeira coisa que você precisa é de um ScriptableObject que possa descrever suas habilidades. ScriptableObjects são impressionantes. Eles foram projetados para funcionar como MonoBehaviours, pois você pode definir seus campos públicos no inspetor do Unity e essas alterações serão serializadas em disco. No entanto, eles não estão anexados a nenhum objeto e não precisam ser anexados a um objeto do jogo em uma cena ou instanciados. Eles são os blocos de dados abrangentes do Unity. Eles podem serializar tipos básicos, enumerações e classes simples (sem herança) marcadas [Serializable]. As estruturas não podem ser serializadas no Unity e a serialização é o que permite editar os campos de objeto no inspetor, lembre-se disso.

Aqui está um ScriptableObject que tenta fazer muito. Você pode dividir isso em classes mais serializadas e ScriptableObjects, mas isso deve fornecer apenas uma idéia de como fazê-lo. Normalmente, isso parece feio em uma linguagem moderna e orientada a objetos, como C #, já que realmente parece uma merda de C89 com todas essas enumerações, mas o verdadeiro poder aqui é que agora você pode criar todo tipo de habilidades diferentes sem precisar escrever um novo código para dar suporte eles. E se o seu primeiro formato não fizer o que você precisa, continue adicionando até ele fazer. Desde que você não altere os nomes dos campos, todos os seus arquivos de ativos serializados antigos ainda funcionarão.

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

Você pode abstrair ainda mais a seção Dano em uma classe Serializable para poder definir habilidades que causam dano ou curam e têm vários tipos de dano em uma habilidade. A única regra é não herança, a menos que você use vários objetos que possam ser gravados e faça referência aos diferentes arquivos de configuração de danos complexos no disco.

Você ainda precisa do AbilityActivator MonoBehaviour, mas agora ele faz um pouco mais de trabalho.

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

A parte MAIS FRESCA

Portanto, a interface e os truques genéricos na primeira abordagem funcionarão bem. Mas, para tirar o máximo proveito do Unity, o ScriptableObjects leva você aonde você quer estar. A Unity é excelente, pois fornece um ambiente muito consistente e lógico para os programadores, mas também possui todas as vantagens de entrada de dados para designers e artistas que você obtém da GameMaker, UDK, et. al.

No mês passado, nosso artista utilizou um tipo de script ScriptableObject que deveria definir o comportamento de diferentes tipos de mísseis, combinando-o com um AnimationCurve e um comportamento que fazia com que os mísseis pairassem no chão e fizesse esse novo e louco hóquei em rotação. arma da morte.

Ainda preciso voltar e adicionar suporte específico para esse comportamento para garantir que ele esteja funcionando de maneira eficiente. Mas, como criamos essa interface genérica de descrição de dados, ele conseguiu tirar essa ideia do nada e colocá-la no jogo sem que nós programadores soubéssemos que ele estava tentando fazê-lo até que ele se aproximou e disse: "Ei, pessoal! nessa coisa legal! " E como foi claramente incrível, estou animado para adicionar um suporte mais robusto a ele.

michael.bartnett
fonte
3

TL: DR - se você estiver pensando em incluir centenas ou milhares de habilidades em uma lista / matriz que você percorreria, toda vez que houver uma ação chamada, para ver se a ação existe e se há um personagem que possa execute-o e leia abaixo.

Caso contrário, não se preocupe.
Se você está falando de 6 caracteres / tipos de caracteres e talvez 30 habilidades, então não importa o que você faz, porque a sobrecarga de gerenciar complexidades pode realmente exigir mais código e mais processamento do que simplesmente jogar tudo em uma pilha e Ordenação...

É exatamente por isso que o @eBusiness sugere que é improvável que você veja problemas de desempenho durante o envio do evento, porque, a menos que esteja se esforçando muito para fazê-lo, não há muito trabalho avassalador aqui, comparado à transformação da posição de 3 milhões de vértices na tela, etc ...

Além disso, essa não é a solução , mas uma solução para gerenciar conjuntos maiores de problemas semelhantes ...

Mas...

Tudo se resume ao tamanho do jogo, quantos personagens compartilham as mesmas habilidades, quantos personagens / habilidades diferentes existem, certo?

Ter as habilidades como componentes do personagem, mas tê-las registrando / cancelando o registro em uma interface de comando à medida que os personagens ingressam ou deixam seu controle (ou são nocauteados / etc) ainda faz sentido, de uma maneira muito StarCraft, com teclas de atalho e o cartão de comando.

Eu tive muito pouca experiência com os scripts do Unity, mas estou muito confortável com o JavaScript como idioma.
Se eles permitirem, por que não fazer com que essa lista seja um objeto simples:

// 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 pode ser usado como:

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

Onde a função Dave (). Init pode parecer:

// 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 houver mais pessoas do que apenas Dave .Repair(), mas você pode garantir que haverá apenas um Dave, altere-o parasystem.notify("register-ability", "dave:repair", this.Repair);

E chame a habilidade usando system.notify("use-action", "dave:repair");

Não sei como são as listas que você está usando. (Em termos do sistema de tipos UnityScript, E em termos do que está acontecendo após a compilação).

Provavelmente, posso dizer que, se você tem centenas de habilidades que planeja incluir na lista (em vez de registrar e cancelar o registro, com base nos caracteres disponíveis atualmente), iteram por toda uma matriz JS (novamente, se é isso que eles estão fazendo) para verificar uma propriedade de uma classe / objeto, que corresponda ao nome da ação que você deseja executar, terá menos desempenho do que isso.

Se houver estruturas mais otimizadas, elas terão mais desempenho do que isso.

Mas em ambos os casos, agora você tem Personagens que controlam suas próprias ações (dê um passo adiante e os torne componentes / entidades, se desejar), E você tem um sistema de controle que requer um mínimo de iteração (como você está fazendo pesquisas de tabela por nome).

Norguard
fonte