¿Cuál es una manera de implementar un sistema flexible de beneficio / desventaja?

66

Visión de conjunto:

Muchos juegos con estadísticas de tipo RPG permiten "beneficios" de los personajes, que van desde el simple "Infligir un 25% de daño extra" hasta cosas más complicadas como "Infligir 15 daños a los atacantes cuando son golpeados".

Los detalles de cada tipo de beneficio no son realmente relevantes. Estoy buscando una forma (presumiblemente orientada a objetos) para manejar beneficios arbitrarios.

Detalles:

En mi caso particular, tengo varios personajes en un entorno de batalla por turnos, por lo que imaginé que los buff estaban vinculados a eventos como "OnTurnStart", "OnReceiveDamage", etc. Quizás cada buff es una subclase de una clase abstracta de Buff principal, donde solo los eventos relevantes están sobrecargados. Entonces cada personaje podría tener un vector de beneficios actualmente aplicado.

¿Tiene sentido esta solución? Ciertamente puedo ver que docenas de tipos de eventos son necesarios, parece que crear una nueva subclase para cada beneficio es excesivo, y no parece permitir ninguna "interacción" de beneficios. Es decir, si quisiera implementar un límite en los aumentos de daño para que, incluso si tuviera 10 beneficios diferentes, todos con un 25% de daño adicional, solo haría un 100% adicional en lugar de un 250% adicional.

Y hay situaciones más complicadas que idealmente podría controlar. Estoy seguro de que todos pueden encontrar ejemplos de cómo los aficionados más sofisticados pueden interactuar entre sí de una manera que, como desarrollador de juegos, puede que no quiera.

Como programador de C ++ relativamente inexperto (generalmente he usado C en sistemas embebidos), siento que mi solución es simplista y probablemente no aprovecha al máximo el lenguaje orientado a objetos.

Pensamientos? ¿Alguien aquí ha diseñado un sistema de mejora bastante robusto antes?

Editar: con respecto a la (s) respuesta (s):

Seleccioné una respuesta basada principalmente en buenos detalles y una respuesta sólida a la pregunta que hice, pero leer las respuestas me dio más información.

Quizás, como era de esperar, los diferentes sistemas o sistemas modificados parecen aplicarse mejor a ciertas situaciones. El sistema que funcione mejor para mi juego dependerá de los tipos, la varianza y la cantidad de beneficios que tengo la intención de aplicar.

Para un juego como Diablo 3 (mencionado a continuación), donde casi cualquier parte del equipo puede cambiar la fuerza de un beneficio, los beneficios son solo un sistema de estadísticas de personajes que parece una buena idea siempre que sea posible.

Para la situación por turnos en la que me encuentro, el enfoque basado en eventos puede ser más adecuado.

En cualquier caso, todavía espero que alguien venga con una elegante bala mágica "OO" que me permitirá aplicar una distancia de movimiento de +2 por beneficio de turno , un 50% del daño recibido al beneficio de atacante , y un telepuerto automáticamente a una baldosa cercana al ser atacados a partir de 3 o más fichas de distancia aficionado en un solo sistema sin necesidad de encender un 5 fuerza aficionado en su propia subclase.

Creo que lo más parecido es la respuesta que marqué, pero el piso aún está abierto. Gracias a todos por el aporte.

gkimsey
fuente
No estoy publicando esto como respuesta, ya que solo estoy haciendo una lluvia de ideas, pero ¿qué tal una lista de aficionados? Cada beneficio tiene una constante y un modificador de factor. Constante sería +10 de daño, el factor sería 1.10 para un aumento de daño de + 10%. En tus cálculos de daño, iteras todos los beneficios para obtener un modificador total y luego impones las limitaciones que desees. Haría esto para cualquier tipo de atributo modificable. Sin embargo, necesitaría un método de caso especial para cosas complicadas.
William Mariager
Por cierto, ya había implementado algo así para mi objeto de estadísticas cuando estaba haciendo un sistema para armas y accesorios equipables. Como dijiste, es una solución lo suficientemente decente para los beneficios que solo modifican los atributos existentes, pero por supuesto, incluso entonces, querré que ciertos beneficios expiren después de X turnos, otros expiren una vez que el efecto ocurre Y veces, etc. No lo hice. Mencione esto en la pregunta principal ya que ya se estaba haciendo muy largo.
gkimsey
1
Si tiene un método "onReceiveDamage" que recibe una llamada de un sistema de mensajería, o manualmente, o de alguna otra manera, debería ser bastante fácil incluir una referencia de quién / de qué está recibiendo el daño. Entonces, podría poner esta información a disposición de su aficionado
Correcto, esperaba que cada plantilla de evento para la clase abstracta de Buff incluyera parámetros relevantes como ese. Ciertamente funcionaría, pero dudo porque parece que no escalará bien. Me cuesta imaginar que un MMORPG con varios cientos de beneficios diferentes tenga una clase separada definida para cada beneficio, seleccionando entre cientos de eventos diferentes. No es que esté haciendo tantos beneficios (probablemente más cerca de 30), pero si hay un sistema más simple, más elegante o más flexible, me gustaría usarlo. Sistema más flexible = beneficios / habilidades más interesantes.
gkimsey 01 de
44
Esta no es una buena respuesta al problema de interacción, pero me parece que el patrón decorador se aplica bien aquí; solo aplica más beneficios (decoradores) uno encima del otro. Tal vez con un sistema para manejar la interacción "fusionando" beneficios juntos (por ejemplo, 10x 25% se fusiona en un beneficio 100%).
ashes999

Respuestas:

32

Este es un tema complicado, porque estás hablando de algunas cosas diferentes que (en estos días) se agrupan como 'beneficios':

  • modificadores a los atributos de un jugador
  • efectos especiales que suceden en ciertos eventos
  • combinaciones de lo anterior.

Siempre implemento el primero con una lista de efectos activos para un determinado personaje. La eliminación de la lista, ya sea en función de la duración o explícitamente, es bastante trivial, por lo que no lo trataré aquí. Cada efecto contiene una lista de modificadores de atributos y puede aplicarlo al valor subyacente mediante una simple multiplicación.

Luego lo envuelvo con funciones para acceder a los atributos modificados. p.ej.:

def get_current_attribute_value(attribute_id, criteria):
    val = character.raw_attribute_value[attribute_id]
    # Accumulate the modifiers
    for effect in character.all_effects:
        val = effect.apply_attribute_modifier(attribute_id, val, criteria)
    # Make sure it doesn't exceed game design boundaries
    val = apply_capping_to_final_value(val)
    return val

class Effect():
    def apply_attribute_modifier(attribute_id, val, criteria):
        if attribute_id in self.modifier_list:
            modifier = self.modifier_list[attribute_id]
            # Does the modifier apply at this time?
            if modifier.criteria == criteria:
                # Apply multiplicative modifier
                return val * modifier.amount
        else:
            return val

class Modifier():
    amount = 1.0 # default that has no effect
    criteria = None # applies all of the time

Eso te permite aplicar efectos multiplicativos con bastante facilidad. Si también necesita efectos aditivos, decida en qué orden los aplicará (probablemente el último aditivo) y revise la lista dos veces. (Probablemente tendría listas de modificadores separadas en Efecto, una para multiplicativo, una para aditivo).

El valor del criterio es permitirle implementar "+ 20% vs No Muerto": establezca el valor NO MUERTO en el Efecto y solo pase el valor NO MUERTO get_current_attribute_value()cuando esté calculando una tirada de daño contra un enemigo no muerto.

Por cierto, no estaría tentado a intentar escribir un sistema que aplique y no aplique los valores directamente al valor del atributo subyacente; el resultado final es que es muy probable que sus atributos se desvíen del valor deseado debido a un error. (por ejemplo, si multiplica algo por 2, pero luego lo limita, cuando lo divida nuevamente por 2, será más bajo de lo que comenzó).

En cuanto a los efectos basados ​​en eventos, como "Infligir 15 daños a los atacantes cuando son golpeados", puede agregar métodos en la clase Efecto para eso. Pero si desea un comportamiento distinto y arbitrario (por ejemplo, algunos efectos para el evento anterior podrían reflejar daños, algunos podrían curarlo, podría teletransportarse al azar, lo que sea) necesitará funciones o clases personalizadas para manejarlo. Puede asignar funciones a los controladores de eventos en el efecto, luego puede llamar a los controladores de eventos en cualquier efecto activo.

# This is a method on a Character, called during combat
def on_receive_damage(damage_info):
    for effect in character.all_effects:
        effect.on_receive_damage(character, damage_info)

class Effect():
    self.on_receive_damage_handler = DoNothing # a default function that does nothing
    def on_receive_damage(character, damage_info):
        self.on_receive_damage_handler(character, damage_info)

def reflect_damage(character, damage_info):
    damage_info.attacker.receive_damage(15)

reflect_damage_effect = new Effect()
reflect_damage_effect.on_receive_damage_handler = reflect_damage
my_character.all_effects.add(reflect_damage_effect)

Obviamente, su clase de efectos tendrá un controlador de eventos para cada tipo de evento, y puede asignar funciones de controlador a todas las que necesite en cada caso. No necesita subclasificar Effect, ya que cada uno está definido por la composición de los modificadores de atributos y controladores de eventos que contiene. (Probablemente también contendrá un nombre, una duración, etc.)

Kylotan
fuente
2
+1 para excelentes detalles. Esta es la respuesta más cercana a responder oficialmente mi pregunta como he visto. La configuración básica aquí parece permitir mucha flexibilidad y una pequeña abstracción de lo que de otro modo podría ser una lógica de juego desordenada. Como dijiste, los efectos más funky aún necesitarían sus propias clases, pero esto maneja la mayor parte de las necesidades típicas de un sistema "buff", creo.
gkimsey
+1 para señalar las diferencias conceptuales ocultas aquí. No todos funcionarán con la misma lógica de actualización basada en eventos. Vea la respuesta de @ Ross para una aplicación totalmente diferente. Ambos tendrán que existir uno al lado del otro.
ctietze
22

En un juego en el que trabajé con un amigo para una clase, creamos un sistema de beneficio / desventaja para cuando el usuario queda atrapado en la hierba alta y acelera los azulejos y lo que no, y algunas cosas menores como sangrados y venenos.

La idea era simple, y aunque la aplicamos en Python, fue bastante efectiva.

Básicamente, así es como fue:

  • El usuario tenía una lista de beneficios y desventajas aplicadas actualmente (tenga en cuenta que un beneficio y una desventaja son relativamente iguales, es solo el efecto el que tiene un resultado diferente)
  • Los aficionados tienen una variedad de atributos como la duración, el nombre y el texto para mostrar información y el tiempo de vida. Los importantes son el tiempo vivo, la duración y una referencia al actor al que se aplica este beneficio.
  • Para el Buff, cuando se adjunta al jugador a través de player.apply (buff / debuff), llamaría un método start (), esto aplicaría los cambios críticos al jugador, como aumentar la velocidad o reducir la velocidad.
  • Luego iteraríamos a través de cada beneficio en un ciclo de actualización y los beneficios se actualizarían, esto aumentaría su tiempo de vida. Las subclases implementarían cosas como envenenar al jugador, darle al jugador HP con el tiempo, etc.
  • Cuando finalizaba la mejora, es decir, timeAlive> = duración, la lógica de actualización eliminaría la mejora y llamaría a un método finish (), que variaría desde eliminar las limitaciones de velocidad de un jugador hasta causar un pequeño radio (piense en un efecto de bomba después de un DoT)

Ahora, cómo aplicar los beneficios del mundo es una historia diferente. Aquí está mi comida para pensar sin embargo.

Ross
fuente
1
Esto suena como una mejor explicación de lo que estaba tratando de describir anteriormente. Es relativamente simple, ciertamente fácil de entender. Básicamente mencionaste tres "eventos" allí (OnApply, OnTimeTick, OnExpired) para asociarlo aún más con mi pensamiento. Tal como está, no admitiría cosas como devolver el daño cuando se golpea y así sucesivamente, pero se escala mejor para muchos beneficios. Prefiero no limitar lo que pueden hacer mis buffs (que = limitar la cantidad de eventos que se me ocurren que deben ser llamados por la lógica principal del juego), pero la escalabilidad de buff puede ser más importante. ¡Gracias por tu contribución!
gkimsey
Sí, no implementamos nada de eso. Suena muy bien y un gran concepto (algo así como un beneficio de espinas).
Ross
@gkimsey Para cosas como Espinas y otros beneficios pasivos, implementaría la lógica en tu clase Mob como una estadística pasiva similar al daño o la salud y aumentaría esta estadística al aplicar el beneficio. Esto simplifica mucho el caso cuando tienes múltiples mejoras de espinas, así como mantener la interfaz limpia (10 mejoras mostraría 1 daño de retorno en lugar de 10) y permite que el sistema de mejora permanezca simple.
3Doubloons
Este es un enfoque casi contraintuitivamente simple, pero comencé a pensar en mí mismo cuando jugaba Diablo 3. Noté que el robo de vida, la vida al golpear, el daño a los atacantes cuerpo a cuerpo, etc. eran todas sus propias estadísticas en la ventana del personaje. Por supuesto, D3 no tiene el sistema de buffing o las interacciones más complicadas del mundo, pero no es trivial. Esto tiene mucho sentido. Aún así, hay potencialmente 15 beneficios diferentes con 12 efectos diferentes que caerían en esto. Parece extraño
rellenar la
11

No estoy seguro si todavía estás leyendo esto, pero he luchado con este tipo de problema durante mucho tiempo.

He diseñado numerosos tipos diferentes de sistemas de afecto. Los revisaré brevemente ahora. Todo esto se basa en mi experiencia. No pretendo saber todas las respuestas.


Modificadores estáticos

Este tipo de sistema se basa principalmente en enteros simples para determinar cualquier modificación. Por ejemplo, +100 a Max HP, +10 a atacar y así sucesivamente. Este sistema también podría manejar porcentajes también. Solo necesita asegurarse de que el apilamiento no se salga de control.

Realmente nunca almacené en caché los valores generados para este tipo de sistema. Por ejemplo, si quisiera mostrar la salud máxima de algo, generaría el valor en el acto. Esto evitó que las cosas fueran propensas a errores y simplemente más fácil de entender para todos los involucrados.

(Trabajo en Java, por lo que lo que sigue está basado en Java, pero debería funcionar con algunas modificaciones para otros idiomas). Este sistema se puede hacer fácilmente usando enumeraciones para los tipos de modificación, y luego enteros. El resultado final se puede colocar en algún tipo de colección que tenga pares ordenados de clave y valor. Esto será una búsqueda rápida y cálculos, por lo que el rendimiento es muy bueno.

En general, funciona muy bien con solo modificadores estáticos. Sin embargo, el código debe existir en los lugares adecuados para que se usen los modificadores: getAttack, getMaxHP, getMeleeDamage, y así sucesivamente.

Donde este método falla (para mí) es una interacción muy compleja entre aficionados. No hay una manera realmente fácil de interactuar, excepto por un poco de gueto. Tiene algunas posibilidades simples de interacción. Para hacerlo, debe realizar una modificación en la forma en que almacena los modificadores estáticos. En lugar de utilizar una enumeración como clave, utiliza una cadena. Esta cadena sería el nombre Enum + variable adicional. 9 de cada 10 veces, la variable adicional no se utiliza, por lo que aún conserva el nombre de enumeración como clave.

Hagamos un ejemplo rápido: si quisiera poder modificar el daño contra criaturas no muertas, podría tener un par ordenado como este: (DAMAGE_Undead, 10) El DAÑO es la enumeración y los no muertos es la variable adicional. Entonces, durante tu combate, puedes hacer algo como:

dam += attacker.getMod(Mod.DAMAGE + npc.getRaceFamily()); //in this case the race family would be undead

De todos modos, funciona bastante bien y es rápido. Pero falla en interacciones complejas y tener código "especial" en todas partes. Por ejemplo, considere la situación de "25% de posibilidades de teletransportarse en caso de muerte". Este es uno "bastante" complejo. El sistema anterior puede manejarlo, pero no fácilmente, ya que necesita lo siguiente:

  1. Determina si el jugador tiene este mod.
  2. En algún lugar, tenga algún código para ejecutar la teletransportación, si tiene éxito. ¡La ubicación de este código es una discusión en sí misma!
  3. Obtenga los datos correctos del mapa Mod. ¿Qué significa el valor? ¿Es la habitación donde se teletransportan también? ¿Qué pasa si un jugador tiene dos modos de teletransporte? ¿¿No se sumarán las cantidades juntas ?????? ¡FRACASO!

Entonces esto me lleva a la siguiente:


El último sistema de mejora compleja

Una vez intenté escribir un MMORPG 2D solo. ¡Fue un error terrible, pero aprendí mucho!

Reescribí el sistema de afecto 3 veces. El primero utilizó una variación menos poderosa de lo anterior. El segundo fue de lo que voy a hablar.

Este sistema tenía una serie de clases para cada modificación, por lo que cosas como: ChangeHP, ChangeMaxHP, ChangeHPByPercent, ChangeMaxByPercent. Tenía un millón de estos tipos, incluso cosas como TeleportOnDeath.

Mis clases tenían cosas que harían lo siguiente:

  • applyAffect
  • removeAffect
  • checkForInteraction <--- importante

Aplicar y eliminar explicarse a sí mismos (aunque para cosas como porcentajes, el efecto mantendría un registro de cuánto aumentó el HP para garantizar que cuando el efecto desapareciera, solo eliminaría la cantidad que agregó. Esto fue buggy, lol y Me llevó mucho tiempo asegurarme de que era correcto. Todavía no tenía un buen presentimiento al respecto).

El método checkForInteraction fue un código horrendísimo complejo. En cada una de las clases de afectos (es decir: ChangeHP), tendría un código para determinar si esto debería ser modificado por el afecto de entrada. Entonces, por ejemplo, si tuvieras algo como ...

  • Buff 1: inflige 10 de daño de Fuego en el ataque
  • Buff 2: aumenta todo el daño de fuego en un 25%.
  • Buff 3: aumenta todo el daño de fuego en 15.

El método checkForInteraction manejaría todos estos efectos. ¡Para hacer esto, cada efecto en TODOS los jugadores cercanos tenía que ser verificado! Esto se debe a la clase de afectos que tuve con varios jugadores en un área. Esto significa que el código NUNCA TENÍA ninguna declaración especial como la anterior: "si acabamos de morir, debemos verificar el teletransporte en caso de muerte". Este sistema lo manejaría automáticamente correctamente en el momento adecuado.

Intentar escribir este sistema me tomó como 2 meses y explotó varias veces por la cabeza. SIN EMBARGO, era REALMENTE poderoso y podía hacer una cantidad increíble de cosas, especialmente cuando se tienen en cuenta los siguientes dos hechos para las habilidades en mi juego: 1. Tenían rangos objetivo (es decir, solo, auto, solo grupo, PB AE auto , PB AE objetivo, AE objetivo, etc.). 2. Las habilidades podrían tener más de 1 efecto sobre ellas.

Como mencioné anteriormente, este fue el segundo sistema de afecto tercero para este juego. ¿Por qué me alejé de esto?

¡Este sistema tuvo el peor rendimiento que he visto!Fue muy lento, ya que tenía que hacer muchas comprobaciones para cada cosa que sucedía. Traté de mejorarlo, pero lo consideré un fracaso.

Entonces llegamos a mi tercera versión (y otro tipo de sistema de mejora):


Clase de afecto complejo con manejadores

Así que esto es más o menos una combinación de los dos primeros: podemos tener variables estáticas en una clase de Afecto que contiene mucha funcionalidad y datos adicionales. Luego solo llame a los controladores (para mí, más o menos algunos métodos de utilidad estáticos en lugar de subclases para acciones específicas. Pero estoy seguro de que podría ir con subclases para acciones si también lo desea) cuando queremos hacer algo.

La clase Affect tendría todas las cosas buenas y jugosas, como los tipos de objetivos, la duración, el número de usos, la posibilidad de ejecutar, etc.

Todavía tendríamos que agregar códigos especiales para manejar las situaciones, por ejemplo, teletransportarse en caso de muerte. Todavía tendríamos que verificar esto manualmente en el código de combate, y luego, si existiera, obtendríamos una lista de afectos. Esta lista de efectos contiene todos los efectos aplicados actualmente en el jugador que se ocupó de teletransportarse al morir. Luego solo miraríamos cada uno y verificaríamos si se ejecutó y tuvo éxito (Nos detendríamos en el primero). Si fue exitoso, simplemente llamaríamos al manejador para encargarse de esto.

La interacción se puede hacer, si quieres también. Solo tendría que escribir el código para buscar beneficios específicos en los jugadores / etc. Debido a que tiene un buen rendimiento (ver más abajo), debería ser bastante eficiente hacerlo. Simplemente necesitaría controladores más complejos, etc.

Por lo tanto, tiene mucho rendimiento del primer sistema y aún mucha complejidad como el segundo (pero no tanto). Al menos en Java, puede hacer algunas cosas difíciles para obtener el rendimiento de casi el primero en la MAYORÍA de los casos (es decir, tener un mapa de enumeración ( http://docs.oracle.com/javase/6/docs/api/java /util/EnumMap.html ) con Enums como las claves y ArrayList de los afectos como los valores. Esto le permite ver si tiene efectos rápidamente [ya que la lista sería 0 o el mapa no tendría la enumeración] y no tener iterar continuamente sobre las listas de afectos del jugador sin ninguna razón. No me importa iterar sobre los afectos si los necesitamos en este momento. Lo optimizaré más adelante si se convierte en un problema).

Actualmente estoy volviendo a abrir (reescribiendo el juego en Java en lugar de la base de código FastROM en la que estaba originalmente) mi MUD que terminó en 2005 y recientemente me he encontrado con ¿cómo quiero implementar mi sistema de mejora? Voy a usar este sistema porque funcionó muy bien en mi juego fallido anterior.

Bueno, espero que alguien, en algún lugar, encuentre algunas de estas ideas útiles.

dayrinni
fuente
6

Una clase diferente (o función direccionable) para cada beneficio no es exagerada si el comportamiento de esos beneficios es diferente el uno del otro. Una cosa sería tener + 10% o + 20% de beneficios (que, por supuesto, estaría mejor representado como dos objetos de la misma clase), otra sería implementar efectos muy diferentes que requerirían un código personalizado de todos modos. Sin embargo, creo que es mejor tener formas estándar de personalizar la lógica del juego lugar de dejar que cada aficionado haga lo que le plazca (y posiblemente interferir entre sí de manera imprevista, lo que perturba el equilibrio del juego).

Sugeriría dividir cada "ciclo de ataque" en pasos, donde cada paso tiene un valor base, una lista ordenada de modificaciones que se pueden aplicar a ese valor (tal vez con un tope) y un tope final. Cada modificación tiene una transformación de identidad por defecto, y puede verse influenciada por cero o más ventajas / desventajas. Los detalles de cada modificación dependerán del paso aplicado. La forma en que se implementa el ciclo depende de usted (incluida la opción de una arquitectura basada en eventos, como ha estado discutiendo).

Un ejemplo de ciclo de ataque podría ser:

  • calcular el ataque del jugador (base + mods);
  • calcular la defensa del oponente (base + mods);
  • hacer la diferencia (y aplicar modificaciones) y determinar el daño base;
  • calcular cualquier efecto de parada / armadura (modificaciones en el daño base) y aplicar daño;
  • calcule cualquier efecto de retroceso (modificaciones en el daño base) y aplique al atacante.

Lo importante a tener en cuenta es que cuanto más temprano en el ciclo se aplique un beneficio, más efecto tendrá en el resultado . Entonces, si quieres un combate más "táctico" (donde la habilidad del jugador es más importante que el nivel de personaje) crea muchos beneficios / desventajas en las estadísticas básicas. Si quieres un combate más "equilibrado" (donde el nivel es más importante, importante en los MMOG para limitar la tasa de progreso), solo usa mejoras / desventajas más adelante en el ciclo.

La distinción entre "Modificaciones" y "Beneficios" que mencioné anteriormente tiene un propósito: las decisiones sobre las reglas y el equilibrio se pueden implementar en el primero, por lo que cualquier cambio en esos no tiene que reflejarse en los cambios en cada clase de este último. OTOH, los números y tipos de beneficios solo están limitados por su imaginación, ya que cada uno de ellos puede expresar su comportamiento deseado sin tener que tener en cuenta cualquier interacción posible entre ellos y los demás (o incluso la existencia de otros).

Entonces, respondiendo la pregunta: no crees una clase para cada Buff, sino una para cada (tipo de) Modificación, y vincula la Modificación al ciclo de ataque, no al personaje. Los beneficios pueden ser simplemente una lista de tuplas (Modificación, clave, valor), y puedes aplicar un beneficio a un personaje simplemente agregándolo / eliminándolo al conjunto de beneficios del personaje. Esto también reduce la ventana de error, ya que las estadísticas del personaje no necesitan cambiarse en absoluto cuando se aplican los beneficios (por lo que hay menos riesgo de restaurar una estadística al valor incorrecto después de que expire un beneficio).

mgibsonbr
fuente
Este es un enfoque interesante porque se encuentra en algún lugar entre las dos implementaciones que había considerado, es decir, ya sea restringiendo los beneficios a modificadores de daño de estadísticas y resultados bastante simples, o creando un sistema muy robusto pero de alta sobrecarga que podría manejar cualquier cosa. Esta es una especie de expansión de la primera para permitir las "espinas" mientras se mantiene una interfaz simple. Si bien no creo que sea la bala mágica para lo que necesito, ciertamente parece que hace que el equilibrio sea mucho más fácil que otros enfoques, por lo que puede ser el camino a seguir. ¡Gracias por tu contribución!
gkimsey
3

No sé si todavía lo estás leyendo, pero así es como lo estoy haciendo ahora (el código se basa en UE4 y C ++). Después de reflexionar sobre el problema durante más de dos semanas (!!), finalmente encontré esto:

http://gamedevelopment.tutsplus.com/tutorials/using-the-composite-design-pattern-for-an-rpg-attributes-system--gamedev-243

Y pensé que, bueno, encapsular un solo atributo dentro de la clase / estructura no es una mala idea después de todo. Sin embargo, tenga en cuenta que estoy aprovechando realmente la gran ventaja del sistema de reflexión de código integrado UE4, por lo que, sin algunos cambios, esto podría no ser adecuado en todas partes.

De todos modos, comencé a envolver el atributo en una sola estructura:

USTRUCT(BlueprintType)
struct GAMEATTRIBUTES_API FGAAttributeBase
{
    GENERATED_USTRUCT_BODY()
public:
    UPROPERTY()
        FName AttributeName;
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float BaseValue;
    /*
        This is maxmum value of this attribute.
    */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float ClampValue;
protected:
    float BonusValue;
    //float OldCurrentValue;
    float CurrentValue;
    float ChangedValue;

    //map of modifiers.
    //It could be TArray, but map seems easier to use in this case
    //we need to keep track of added/removed effects, and see 
    //if this effect affected this attribute.
    TMap<FGAEffectHandle, FGAModifier> Modifiers;

public:

    inline float GetFinalValue(){ return BaseValue + BonusValue; };
    inline float GetCurrentValue(){ return CurrentValue; };
    void UpdateAttribute();

    void Add(float ValueIn);
    void Subtract(float ValueIn);

    //inline float GetCurrentValue()
    //{
    //  return FMath::Clamp<float>(BaseValue + BonusValue + AccumulatedBonus, 0, GetFinalValue());;
    //}

    void AddBonus(const FGAModifier& ModifiersIn, const FGAEffectHandle& Handle);
    void RemoveBonus(const FGAEffectHandle& Handle);

    void InitializeAttribute();

    void CalculateBonus();

    inline bool operator== (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName == AttributeName);
    }

    inline bool operator!= (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName != AttributeName);
    }

    inline bool IsValid() const
    {
        return !AttributeName.IsNone();
    }
    friend uint32 GetTypeHash(const FGAAttributeBase& AttributeIn)
    {
        return AttributeIn.AttributeName.GetComparisonIndex();
    }
};

Todavía no está terminado, pero la idea base es que esta estructura realiza un seguimiento de su estado interno. Los atributos solo pueden ser modificados por Efectos. Intentar modificarlos directamente no es seguro y no está expuesto a los diseñadores. Supongo que todo lo que puede interactuar con los atributos es Efecto. Incluyendo bonificaciones planas de artículos. Cuando se equipa un nuevo elemento, se crea un nuevo efecto (junto con el mango), y se agrega al mapa dedicado, que maneja bonificaciones de duración infinita (aquellas que el jugador debe eliminar manualmente). Cuando se aplica un nuevo efecto, se crea un nuevo identificador para él (el identificador es solo int, envuelto con struct), y luego ese identificador se pasa por todos lados como un medio para interactuar con este efecto, así como para realizar un seguimiento si el efecto es Aún en activo. Cuando se elimina el efecto, su asa se transmite a todos los objetos interesados,

La parte realmente importante de esto es TMap (TMap es un mapa hash). FGAModifier es una estructura muy simple:

struct FGAModifier
{
    EGAAttributeOp AttributeMod;
    float Value;
};

Contiene tipo de modificación:

UENUM()
enum class EGAAttributeOp : uint8
{
    Add,
    Subtract,
    Multiply,
    Divide,
    Set,
    Precentage,

    Invalid
};

Y el valor, que es el valor final calculado, lo vamos a aplicar al atributo.

Agregamos un nuevo efecto usando una función simple, y luego llamamos:

void FGAAttributeBase::CalculateBonus()
{
    float AdditiveBonus = 0;
    auto ModIt = Modifiers.CreateConstIterator();
    for (ModIt; ModIt; ++ModIt)
    {
        switch (ModIt->Value.AttributeMod)
        {
        case EGAAttributeOp::Add:
            AdditiveBonus += ModIt->Value.Value;
                break;
            default:
                break;
        }
    }
    float OldBonus = BonusValue;
    //calculate final bonus from modifiers values.
    //we don't handle stacking here. It's checked and handled before effect is added.
    BonusValue = AdditiveBonus; 
    //this is absolute maximum (not clamped right now).
    float addValue = BonusValue - OldBonus;
    //reset to max = 200
    CurrentValue = CurrentValue + addValue;
}

Se supone que esta función recalculará toda la pila de bonos, cada vez que se agregue o elimine el efecto. La función aún no está terminada (como puede ver), pero puede obtener la idea general.

Mi mayor queja en este momento es manejar el atributo Damaging / Healing (sin involucrar volver a calcular la pila completa), creo que tengo algo resuelto, pero aún requiere más pruebas para ser 100%.

En cualquier caso, los atributos se definen así (+ macros irreales, omitidos aquí):

FGAAttributeBase Health;
FGAAttributeBase Energy;

etc.

Además, no estoy 100% seguro de manejar el CurrentValue del atributo, pero debería funcionar. Como están ahora.

En cualquier caso, espero que salve a algunas personas de la memoria caché, no estoy seguro de si esta es la mejor o incluso la mejor solución, pero me gusta más que rastrear los efectos independientemente de los atributos. Hacer que cada atributo rastree su propio estado es mucho más fácil en este caso, y debería ser menos propenso a errores. Esencialmente, solo hay un punto de falla, que es una clase bastante corta y simple.

Łukasz Baran
fuente
¡Gracias por el enlace y la explicación de tu trabajo! Creo que te estás moviendo esencialmente hacia lo que estaba pidiendo. Algunas cosas que recuerdan son el orden de las operaciones (por ejemplo, 3 efectos "agregar" y 2 efectos "multiplicar" en el mismo atributo, ¿qué debería ocurrir primero?), Y esto es puramente soporte de atributos. También existe la noción de desencadenantes (como los efectos de tipo "perder 1 AP cuando se golpea") para abordar, pero eso probablemente sería una investigación por separado.
gkimsey
El orden de operación, en caso de que solo se calcule la bonificación del atributo, es fácil de hacer. Puedes ver aquí que tengo allí y cambiar. Para iterar sobre todos los bonos actuales (que se pueden sumar, restar, multiplicar, dividir, etc.), y luego simplemente acumularlos. Luego haces algo como BonusValue = (BonusValue * MultiplyBonus + AddBonus-SubtractBonus) / DivideBonus, o como quieras ver esta ecuación. Debido al único punto de entrada, es fácil experimentar con él. En cuanto a los desencadenantes, no he escrito sobre eso, porque ese es el otro problema que considero, y ya intenté 3-4 (límite)
Łukasz Baran
soluciones, ninguna de ellas funcionó como yo quería (mi objetivo principal es que sean amigables con el diseñador). Mi idea general es usar etiquetas y verificar los efectos entrantes contra las etiquetas. Si coincide la etiqueta, el efecto puede desencadenar otro efecto. (la etiqueta es un nombre humano simple, como Damage.Fire, Attack.Physical, etc.). En el núcleo es un concepto muy fácil, el problema es organizar los datos, para que sean fácilmente accesibles (búsqueda rápida) y la facilidad de agregar nuevos efectos. Puede consultar el código aquí github.com/iniside/ActionRPGGame (GameAttributes es el módulo que le interesará)
Łukasz Baran
2

Trabajé en un pequeño MMO y todos los elementos, poderes, beneficios, etc. tuvieron "efectos". Un efecto era una clase que tenía variables para 'AddDefense', 'InstantDamage', 'HealHP', etc. Los poderes, elementos, etc. manejarían la duración de ese efecto.

Cuando lanzas un poder o te pones un objeto, aplicaría el efecto al personaje durante la duración especificada. Luego, el ataque principal, etc., los cálculos tendrían en cuenta los efectos aplicados.

Por ejemplo, tienes un beneficio que agrega defensa. Habría como mínimo un EffectID y una Duración para ese beneficio. Al lanzarlo, aplicaría el EffectID al personaje durante la duración especificada.

Otro ejemplo para un artículo, tendría los mismos campos. Pero la duración sería infinita o hasta que se elimine el efecto quitando el elemento del personaje.

Este método le permite iterar sobre una lista de efectos que se aplican actualmente.

Espero haber explicado este método con suficiente claridad.

La licenciatura
fuente
Según tengo entendido con mi experiencia mínima, esta es la forma tradicional de implementar modificaciones estadísticas en los juegos de rol. Funciona bien y es fácil de entender e implementar. La desventaja es que no parece dejarme espacio para hacer cosas como el beneficio de "espinas", o algo más avanzado o situacional. También ha sido históricamente la causa de algunas hazañas en los juegos de rol, aunque son bastante raras, y dado que estoy haciendo un juego para un solo jugador, si alguien encuentra una hazaña, no estoy realmente preocupado. Gracias por el aporte.
gkimsey
2
  1. Si usted es un usuario de la unidad, aquí hay algo para comenzar: http://www.stevegargolinski.com/armory-a-free-and-unfinished-stat-inventory-and-buffdebuff-framework-for-unity/

Estoy usando ScriptableOjects como beneficios / hechizos / talentos

public class Spell : ScriptableObject 
{
    public SpellType SpellType = SpellType.Ability;
    public SpellTargetType SpellTargetType = SpellTargetType.SingleTarget;
    public SpellCategory SpellCategory = SpellCategory.Ability;
    public MagicSchools MagicSchool = MagicSchools.Physical;
    public CharacterClass CharacterClass = CharacterClass.None;
    public string Description = "no description available";
    public SpellDragType DragType = SpellDragType.Active; 
    public bool Active = false;
    public int TargetCount = 1;
    public float CastTime = 0;
    public uint EffectRange = 3;
    public int RequiredLevel = 1;
    public virtual void OnGUI()
    {
    }
}

usando UnityEngine; usando System.Collections.Generic;

public enum BuffType {Buff, Debuff} [System.Serializable] public class BuffStat {public Stat Stat = Stat.Strength; flotante público ModValueInPercent = 0.1f; }

public class Buff : Spell
{
    public BuffType BuffType = BuffType.Buff;
    public BuffStat[] ModStats;
    public bool PersistsThroughDeath = false;
    public int AmountPerTick = 3;
    public bool UseTickTimer = false;
    public float TickTime = 1.5f;
    [HideInInspector]
    public float Ticktimer = 0;
    public float Duration = 360; // in seconds
    public float ModifierPerStack = 1.1f;
    [HideInInspector]
    public float Timer = 0;
    public int Stack = 1;
    public int MaxStack = 1;
}

BuffModul:

using System;
using RPGCore;
using UnityEngine;

public class Buff_Modul : MonoBehaviour
{
    private Unit _unit;

    // Use this for initialization
    private void Awake()
    {
        _unit = GetComponent<Unit>();
    }

    #region BUFF MODUL

    public virtual void RUN_BUFF_MODUL()
    {
        try
        {
            foreach (var buff in _unit.Attr.Buffs)
            {
                CeckBuff(buff);
            }
        }
        catch(Exception e) {throw new Exception(e.ToString());}
    }

    #endregion BUFF MODUL

    public void ClearBuffs()
    {
        _unit.Attr.Buffs.Clear();
    }

    public void AddBuff(string buffName)
    {
        var buff = Instantiate(Resources.Load("Scriptable/Buff/" + buffName, typeof(Buff))) as Buff;
        if (buff == null) return;
        buff.name = buffName;
        buff.Timer = buff.Duration;
        _unit.Attr.Buffs.Add(buff);
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
    }

    public void RemoveBuff(Buff buff)
    {
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat]  /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
        _unit.Attr.Buffs.Remove(buff);
    }

    void CeckBuff(Buff buff)
    {
        buff.Timer -= Time.deltaTime;
        if (!_unit.IsAlive && !buff.PersistsThroughDeath)
        {
            if (buff.ModStats != null)
                foreach (var stat in buff.ModStats)
                {
                    _unit.Attr.StatsBuff[stat.Stat] = 0;
                }

            RemoveBuff(buff);
        }
        if (_unit.IsAlive && buff.Timer <= 0)
        {
            RemoveBuff(buff);
        }
    }
}
usuario22475
fuente
0

Esta fue una pregunta real para mí. Tengo una idea al respecto.

  1. Como se dijo anteriormente, necesitamos implementar una Bufflista y un actualizador lógico para los aficionados.
  2. Luego necesitamos cambiar todos los ajustes específicos del jugador en cada cuadro en las subclases de la Buffclase.
  3. Luego obtenemos la configuración actual del reproductor desde el campo de configuración modificable.

class Player {
  settings: AllPlayerStats;

  private buffs: Array<Buff> = [];
  private baseSettings: AllPlayerStats;

  constructor(settings: AllPlayerStats) {
    this.baseSettings = settings;
    this.resetSettings();
  }

  addBuff(buff: Buff): void {
    this.buffs.push(buff);
    buff.start(this);
  }

  findBuff(predcate(buff: Buff) => boolean): Buff {...}

  removeBuff(buff: Buff): void {...}

  update(dt: number): void {
    this.resetSettings();
    this.buffs.forEach((item) => item.update(dt));
  }

  private resetSettings(): void {
    //some way to copy base to settings
    this.settings = this.baseSettings.copy();
  }
}

class Buff {
    private owner: Player;        

    start(owner: Player) { this.owner = owner; }

    update(dt: number): void {
      //here we change anything we want in subclasses like
      this.owner.settings.hp += 15;
      //if we need base value, just make owner.baseSettings public but don't change it! only read

      //also here logic for removal buff by time or something
    }
}

De esta manera, puede ser fácil agregar nuevas estadísticas de jugador, sin cambios en la lógica de las Buffsubclases.

DantaliaN
fuente
0

Sé que esto es bastante antiguo, pero estaba vinculado en una publicación más reciente y tengo algunas ideas que me gustaría compartir. Desafortunadamente, no tengo mis notas conmigo en este momento, así que intentaré dar una visión general de lo que estoy hablando y editaré los detalles y algún código de ejemplo cuando lo tenga delante yo.

En primer lugar, creo que desde una perspectiva de diseño, la mayoría de las personas están demasiado atrapadas en los tipos de beneficios que se pueden crear y cómo se aplican y olvidando los principios básicos de la programación orientada a objetos.

¿Que quiero decir? Realmente no importa si algo es un beneficio o una desventaja, ambos son modificadores que solo afectan algo de manera positiva o negativa. Al código no le importa cuál es cuál. En realidad, no importa si algo está agregando estadísticas o multiplicándolas, esos son solo operadores diferentes y nuevamente al código no le importa cuál es cuál.

Entonces, ¿a dónde voy con esto? Que diseñar una buena (léase: simple, elegante) clase buff / debuff no es tan difícil, lo que es difícil es diseñar los sistemas que calculan y mantienen el estado del juego.

Si estuviera diseñando un sistema buff / debuff aquí hay algunas cosas que consideraría:

  • Una clase buff / debuff para representar el efecto en sí.
  • Una clase de tipo buff / debuff para contener la información sobre lo que afecta el buff y cómo.
  • Los personajes, los elementos y posiblemente las ubicaciones tendrían que tener una lista o propiedad de colección para contener beneficios y desventajas.

Algunos detalles sobre qué tipos de beneficios / desventajas deberían contener:

  • A quién / a qué se puede aplicar, IE: jugador, monstruo, ubicación, elemento, etc.
  • Qué tipo de efecto es (positivo, negativo), si es multiplicativo o aditivo, y qué tipo de estadística impacta, IE: ataque, defensa, movimiento, etc.
  • Cuándo debe verificarse (combate, hora del día, etc.).
  • Si se puede eliminar y, de ser así, cómo se puede eliminar.

Eso es solo un comienzo, pero a partir de ahí, solo estás definiendo lo que quieres y actuando según tu estado de juego normal. Por ejemplo, supongamos que desea crear un objeto maldito que reduzca la velocidad de movimiento ...

Siempre que haya establecido los tipos adecuados, es simple crear un registro de beneficio que diga:

  • Tipo: Maldición
  • ObjectType: Item
  • StatCategory: utilidad
  • StatAffected: Movimiento Velocidad
  • Duración: infinita
  • Gatillo: OnEquip

Y así sucesivamente, y cuando creo un beneficio solo le asigno el BuffType of Curse y todo lo demás depende del motor ...

Aithos
fuente