Hacer las habilidades y habilidades del personaje como comandos, ¿buenas prácticas?

11

Estoy diseñando para un juego que consiste en personajes que tienen habilidades ofensivas únicas y otras habilidades como construir, reparar, etc. Los jugadores pueden controlar múltiples de esos personajes.

Estoy pensando en poner todas esas habilidades y habilidades en comandos individuales. Un controlador estático registraría todos estos comandos en una lista de comandos estáticos. La lista estática consistiría en todas las habilidades y habilidades disponibles de todos los personajes del juego. Entonces, cuando un jugador selecciona uno de los personajes y hace clic en un botón en la interfaz de usuario para lanzar un hechizo o realizar una habilidad, la Vista llamará al controlador estático para obtener el comando deseado de la lista y ejecutarlo.

Sin embargo, no estoy seguro de si este es un buen diseño dado que estoy construyendo mi juego en Unity. Creo que podría haber hecho todas las habilidades y destrezas como componentes individuales, que luego se unirían a los GameObjects que representan a los personajes del juego. Luego, la interfaz de usuario necesitaría mantener el GameObject del personaje y luego ejecutar el comando.

¿Cuál sería un mejor diseño y práctica para un juego que estoy diseñando?

xenón
fuente
¡Luce bien! Simplemente lanzando este hecho relacionado: en algunos idiomas, incluso puede llegar a hacer que cada comando sea una función para sí mismo. Esto tiene algunas ventajas increíbles para las pruebas, ya que puede automatizar fácilmente la entrada. Además, el reenlace de control se puede hacer fácilmente reasignando una variable de función de devolución de llamada a una función de comando diferente.
Anko
@ Anko, ¿qué pasa con la parte donde tengo todos los comandos en una lista estática? Me preocupa que la lista se vuelva enorme y cada vez que se necesita un comando, debe consultar la enorme lista de comandos.
xenón
1
@xenon Es muy poco probable que vea problemas de rendimiento en esta parte del código. En la medida en que algo solo puede suceder una vez por interacción del usuario, tendría que ser muy intensivo en cómputo para hacer una marca notable en el rendimiento.
aaaaaaaaaaaa

Respuestas:

17

TL; DR

Esta respuesta se vuelve un poco loca. Pero es porque veo que estás hablando de implementar tus habilidades como "Comandos", lo que implica patrones de diseño C ++ / Java / .NET, lo que implica un enfoque de código pesado. Esa aproximación es válida, pero hay una mejor manera. Tal vez ya estás haciendo lo contrario. Si es así, bueno. Esperemos que otros lo encuentren útil si ese es el caso.

Mire el enfoque basado en datos a continuación para ir al grano. Obtenga el CustomAssetUility de Jacob Pennock aquí y lea su publicación al respecto .

Trabajando con la unidad

Como otros han mencionado, atravesar una lista de 100-300 artículos no es tan importante como podría pensar. Entonces, si ese es un enfoque intuitivo para usted, simplemente haga eso. Optimizar para la eficiencia del cerebro. Pero el Diccionario, como lo demostró @Norguard en su respuesta , es la forma fácil de eliminar el problema sin necesidad de capacidad intelectual, ya que se obtiene una inserción y recuperación en tiempo constante. Probablemente deberías usarlo.

En términos de hacer que esto funcione bien en Unity, mi instinto me dice que un MonoBehaviour por habilidad es un camino peligroso para seguir. Si alguna de sus habilidades mantiene el estado a lo largo del tiempo y se ejecutan, deberá administrar eso y proporcionar una forma de restablecer ese estado. Las rutinas alivian este problema, pero aún está administrando una referencia de IEnumerator en cada marco de actualización de ese script, y tiene que asegurarse absolutamente de que tiene una forma segura de restablecer las habilidades para que no sea incompleto y se atasque en un bucle de estado Las habilidades comienzan a arruinar la estabilidad de tu juego cuando pasan desapercibidas. "¡Por supuesto que haré eso!" usted dice: "Soy un 'buen programador'". Pero realmente, ya sabes, todos somos programadores objetivamente terribles e incluso los mejores investigadores de IA y escritores de compiladores arruinan todo el tiempo.

De todas las formas en que podría implementar la instanciación y recuperación de comandos en Unity, puedo pensar en dos: una está bien y no le dará un aneurisma, y ​​la otra permite una CREATIVIDAD MÁGICA SIN LÍMITES . Algo así como.

Enfoque centrado en el código

Primero es un enfoque mayormente en código. Lo que recomiendo es que convierta cada comando en una clase simple que herede de una clase abstracta de BaseCommand o implemente una interfaz ICommand (supongo, por razones de brevedad, que estos comandos solo serán habilidades de personaje, no es difícil de incorporar otros usos). Este sistema asume que cada comando es un ICommand, tiene un constructor público que no toma parámetros, y requiere actualizar cada trama mientras está activo.

Las cosas son más simples si usa una clase base abstracta, pero mi versión usa interfaces.

Es importante que sus MonoBehaviours encapsulen un comportamiento específico o un sistema de comportamientos estrechamente relacionados. Está bien tener muchos MonoBehaviours que efectivamente se deleguen a las clases simples de C #, pero si te encuentras haciendo esto también puedes actualizar las llamadas a todo tipo de objetos diferentes hasta el punto en que empiece a parecerse a un juego XNA, entonces tú ' Estás en serios problemas y necesitas cambiar tu arquitectura.

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

Esto funciona totalmente bien, pero puede hacerlo mejor (además, a List<T>no es la estructura de datos óptima para almacenar habilidades cronometradas, es posible que desee a LinkedList<T>o a SortedDictionary<float, T>).

Enfoque basado en datos

Probablemente sea posible que pueda reducir los efectos de su habilidad a comportamientos lógicos que puedan parametrizarse. Para esto se construyó realmente Unity. Usted, como programador, diseña un sistema que luego usted o un diseñador pueden manipular en el editor para producir una amplia variedad de efectos. Esto simplificará enormemente la "manipulación" del código y se centrará exclusivamente en la ejecución de una habilidad. No es necesario hacer malabarismos con las clases base o interfaces y genéricos aquí. Todo estará basado únicamente en datos (lo que también simplifica la inicialización de instancias de comandos).

Lo primero que necesita es un ScriptableObject que pueda describir sus habilidades. ScriptableObjects son increíbles. Están diseñados para funcionar como MonoBehaviours, ya que puede configurar sus campos públicos en el inspector de Unity, y esos cambios se serializarán en el disco. Sin embargo, no están unidos a ningún objeto y no tienen que estar unidos a un objeto del juego en una escena o instanciados. Son los cubos de datos generales de Unity. Pueden serializar tipos básicos, enumeraciones y clases simples (sin herencia) marcadas [Serializable]. Las estructuras no se pueden serializar en Unity, y la serialización es lo que le permite editar los campos de objetos en el inspector, así que recuerde eso.

Aquí hay un ScriptableObject que intenta hacer mucho. Puede dividir esto en clases más serializadas y ScriptableObjects, pero se supone que esto solo le dará una idea de cómo hacerlo. Normalmente esto se ve feo en un agradable lenguaje moderno orientado a objetos como C #, ya que realmente se siente como una mierda de C89 con todas esas enumeraciones, pero el verdadero poder aquí es que ahora puedes crear todo tipo de habilidades diferentes sin tener que escribir código nuevo para soportar ellos. Y si su primer formato no hace lo que necesita hacer, continúe agregando hasta que lo haga. Mientras no cambie los nombres de los campos, todos sus archivos de activos serializados anteriores seguirán funcionando.

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

Podrías abstraer aún más la sección Daño en una clase Serializable para poder definir habilidades que infligen daño, o sanan, y tienen múltiples tipos de daño en una habilidad. La única regla es no heredar a menos que use varios objetos programables y haga referencia a los diferentes archivos de configuración de daños complejos en el disco.

Todavía necesitas el AbilityActivator MonoBehaviour, pero ahora hace un poco más de trabajo.

// 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 más fresca

Entonces, la interfaz y el truco genérico en el primer enfoque funcionarán bien. Pero para realmente aprovechar al máximo Unity, ScriptableObjects lo llevará a donde quiere estar. Unity es excelente, ya que proporciona un entorno muy coherente y lógico para los programadores, pero también tiene todas las características de entrada de datos para diseñadores y artistas que obtienes de GameMaker, UDK, et. Alabama.

El mes pasado, nuestro artista tomó un tipo de Objeto Scriptable que se suponía que definía el comportamiento de diferentes tipos de misiles guiados, lo combinó con un AnimationCurve y un comportamiento que hizo que los misiles se movieran por el suelo, e hizo este nuevo y loco disco de hockey giratorio. arma de la muerte.

Todavía necesito regresar y agregar soporte específico para este comportamiento para asegurarme de que se ejecuta de manera eficiente. Pero debido a que creamos esta interfaz genérica de descripción de datos, fue capaz de sacar esta idea de la nada y ponerla en el juego sin que los programadores supiéramos que estaba tratando de hacerlo hasta que vino y dijo: "Hola chicos, miren a esta cosa genial! Y debido a que fue claramente increíble, estoy emocionado de poder agregarle un soporte más robusto.

michael.bartnett
fuente
3

TL: DR: si está pensando en incorporar cientos o miles de habilidades en una lista / matriz que luego iteraría, cada vez que se llame una acción, para ver si la acción existe y si hay un personaje que pueda realizarlo, luego lea a continuación.

Si no, no te preocupes por eso.
Si estás hablando de 6 personajes / tipos de personajes y tal vez 30 habilidades, entonces realmente no importará lo que hagas, porque la sobrecarga de gestionar las complejidades en realidad podría requerir más código y más procesamiento que simplemente tirar todo en una pila y clasificación...

Es por eso que @eBusiness sugiere que es poco probable que vea problemas de rendimiento durante el envío de eventos, porque a menos que esté tratando de hacerlo, no hay mucho trabajo abrumador aquí, en comparación con la transformación de la posición de 3- millones de vértices en pantalla, etc.

Además, esta no es la solución , sino una solución para gestionar conjuntos más grandes de problemas similares ...

Pero...

Todo se reduce a qué tan grande estás haciendo el juego, cuántos personajes comparten las mismas habilidades, cuántos personajes diferentes / habilidades diferentes hay, ¿verdad?

Tener las habilidades como componentes del personaje, pero hacer que se registren / anulen el registro desde una interfaz de comando a medida que los personajes se unen o dejan su control (o quedan eliminados / etc.) todavía tiene sentido, de una manera muy StarCraft, con teclas de acceso rápido y La tarjeta de comando.

He tenido muy, muy poca experiencia con los scripts de Unity, pero estoy muy cómodo con JavaScript como lenguaje.
Si lo permiten, ¿por qué no hacer que esa lista sea un simple objeto?

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

Y podría usarse 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();

Donde la función Dave (). Init podría verse así:

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

Si tiene más personas que solo Dave .Repair(), pero puede garantizar que solo habrá un Dave, entonces cámbielo asystem.notify("register-ability", "dave:repair", this.Repair);

Y llama a la habilidad usando system.notify("use-action", "dave:repair");

No estoy seguro de cómo son las listas que está utilizando. (En términos del sistema de tipos UnityScript, Y en términos de lo que sucede después de la compilación).

Probablemente puedo decir que si tiene cientos de habilidades que planeaba simplemente incluir en la lista (en lugar de registrarse y anular el registro, en función de los caracteres que tiene actualmente disponibles), eso iterando a través de toda una matriz JS (nuevamente, si eso es lo que están haciendo) para verificar una propiedad de una clase / objeto, que coincida con el nombre de la acción que desea realizar, será menos eficaz que esto.

Si hay estructuras más optimizadas, entonces serán más eficaces que esto.

Pero en cualquier caso, ahora tiene Personajes que controlan sus propias acciones (vaya un paso más allá y conviértalos en componentes / entidades, si lo desea), Y tiene un sistema de control que requiere un mínimo de iteración (ya que solo haciendo búsquedas de tablas por nombre).

Norguard
fuente