Uso del patrón de diseño compuesto para un sistema de atributos RPG

Inteligencia, Fuerza de voluntad, Carisma, Sabiduría: además de ser cualidades importantes que debes tener como desarrollador de juegos, estos también son atributos comunes que se usan en los juegos de rol. Calcular los valores de dichos atributos (aplicar bonificaciones cronometradas y tener en cuenta el efecto de los artículos equipados) puede ser complicado. En este tutorial, te mostraré cómo usar un patrón compuesto ligeramente modificado para manejar esto, sobre la marcha.

Nota: Aunque este tutorial está escrito con Flash y AS3, debería poder utilizar las mismas técnicas y conceptos en casi cualquier entorno de desarrollo de juegos..


Introducción

Los sistemas de atributos son muy utilizados en los juegos de rol para cuantificar las fortalezas, debilidades y habilidades de los personajes. Si no está familiarizado con ellos, hojee la página de Wikipedia para obtener una visión general decente.

Para hacerlos más dinámicos e interesantes, los desarrolladores a menudo mejoran estos sistemas al agregar habilidades, elementos y otras cosas que afectan los atributos. Si desea hacer esto, necesitará un buen sistema que pueda calcular los atributos finales (teniendo en cuenta todos los demás efectos) y manejar la adición o eliminación de diferentes tipos de bonificaciones.

En este tutorial, exploraremos una solución para este problema utilizando una versión ligeramente modificada del patrón de diseño compuesto. Nuestra solución podrá manejar bonos y funcionará en cualquier conjunto de atributos que defina.


¿Qué es el patrón compuesto??

Esta sección es una descripción general del patrón de diseño compuesto. Si ya está familiarizado con él, es posible que desee saltar a Modelando nuestro problema.

El patrón compuesto es un patrón de diseño (una plantilla de diseño general conocida y reutilizable) para subdividir algo grande en objetos más pequeños, a fin de crear un grupo más grande al manejar solo los objetos pequeños. Hace que sea fácil dividir grandes porciones de información en trozos más pequeños y más fáciles de tratar. Esencialmente, es una plantilla para usar un grupo de un objeto en particular como si fuera un objeto en sí mismo..

Vamos a utilizar un ejemplo ampliamente utilizado para ilustrar esto: piense en una aplicación de dibujo simple. Desea que le permita dibujar triángulos, cuadrados y círculos, y tratarlos de manera diferente. Pero también quieres que sea capaz de manejar grupos de dibujos. ¿Cómo podemos hacer eso fácilmente??

El patrón compuesto es el candidato perfecto para este trabajo. Al tratar a un "grupo de dibujos" como un dibujo en sí mismo, uno podría agregar fácilmente cualquier dibujo dentro de este grupo, y el grupo en su conjunto aún sería visto como un solo dibujo..

En términos de programación, tendríamos una clase base., Dibujo, que tiene los comportamientos predeterminados de un dibujo (puede moverlo, cambiar de capa, rotarlo, etc.) y cuatro subclases, Triángulo, Cuadrado, Circulo y Grupo.

En este caso, las tres primeras clases tendrán un comportamiento simple, que requerirá solo la entrada del usuario de los atributos básicos de cada forma. los Grupo Sin embargo, la clase tendrá métodos para agregar y eliminar formas, además de realizar una operación en todas ellas (por ejemplo, cambiar el color de todas las formas en un grupo a la vez). Las cuatro subclases todavía serían tratadas como un Dibujo, para que no tenga que preocuparse por agregar un código específico para cuando desea operar en un grupo.

Para llevar esto a una mejor representación, podemos ver cada dibujo como un nodo en un árbol. Cada nodo es una hoja, a excepción de Grupo nodos, que pueden tener hijos, que a su vez son dibujos dentro de ese grupo.


Una representación visual del patrón

Siguiendo con el ejemplo de la aplicación de dibujo, esta es una representación visual de la "aplicación de dibujo" en la que pensamos. Tenga en cuenta que hay tres dibujos en la imagen: un triángulo, un cuadrado y un grupo que consiste en un círculo y un cuadrado:

Y esta es la representación en árbol de la escena actual (la raíz es el escenario de la aplicación de dibujo):

¿Qué pasa si quisiéramos agregar otro dibujo, que es un grupo de un triángulo y un círculo, dentro del grupo que tenemos actualmente? Simplemente lo agregaríamos como agregaríamos cualquier dibujo dentro de un grupo. Así se vería la representación visual:

Y esto es en lo que se convertiría el árbol:

Ahora, imagine que vamos a construir una solución al problema de atributos que tenemos. Obviamente, no vamos a tener una representación visual directa (solo podemos ver el resultado final, que es el atributo calculado dados los valores brutos y las bonificaciones), por lo que comenzaremos a pensar en el Patrón compuesto con la representación del árbol.


Modelando nuestro problema

Para poder modelar nuestros atributos en un árbol, necesitamos dividir cada atributo en las partes más pequeñas que podamos.

Sabemos que tenemos bonos, que pueden agregar un valor en bruto al atributo o aumentarlo en un porcentaje. Hay bonos que se agregan al atributo, y otros que se calculan después de que se aplican todos los primeros bonos (bonos por habilidades, por ejemplo).

Entonces, podemos tener:

  • Bonificaciones en bruto (agregadas al valor en bruto del atributo)
  • Bonificaciones finales (agregadas al atributo después de que se haya calculado todo lo demás)

Es posible que haya notado que no estamos separando los bonos que agregan un valor al atributo de los bonos que aumentan el atributo en un porcentaje. Eso es porque estamos modelando cada bono para poder cambiar cualquiera de los dos al mismo tiempo. Esto significa que podríamos tener un bono que agregue 5 al valor y Aumenta el atributo en un 10%. Todo esto será manejado en el código..

Estos dos tipos de bonos son solo las hojas de nuestro árbol. Son bastante parecidos a los Triángulo, Cuadrado y Circulo clases en nuestro ejemplo de antes.

Todavía no hemos creado una entidad que servirá como grupo. ¡Estas entidades serán los propios atributos! los Grupo La clase en nuestro ejemplo será simplemente el atributo en sí. Así tendremos un Atributo clase que se comportará como alguna atributo.

Así es como podría verse un árbol de atributos:

Ahora que todo está decidido, comencemos nuestro código.?


Creando las clases base

Usaremos ActionScript 3.0 como el lenguaje para el código en este tutorial, ¡pero no se preocupe! El código se comentará completamente después, y se explicará todo lo que es exclusivo del idioma (y la plataforma Flash) y se proporcionarán alternativas, por lo que si está familiarizado con cualquier lenguaje OOP, podrá seguir esto. tutorial sin problemas.

La primera clase que necesitamos crear es la clase base para cualquier atributo y bonificaciones. El archivo será llamado BaseAttribute.as, Y crearlo es muy simple. Aquí está el código, con comentarios después:

 package public class BaseAttribute private var _baseValue: int; private var _baseMultiplier: Number; función pública BaseAttribute (valor: int, multiplicador: Número = 0) _baseValue = valor; _baseMultiplier = multiplicador;  public function get baseValue (): int return _baseValue;  public function get baseMultiplier (): Number return _baseMultiplier; 

Como puede ver, las cosas son muy simples en esta clase base. Simplemente creamos el _valor y _multiplicador campos, asignarlos en el constructor, y hacer dos métodos getter, uno para cada campo.

Ahora necesitamos crear el RawBonus y FinalBonus clases Estas son simplemente subclases de BaseAttribute, sin nada añadido. Puedes expandirlo tanto como quieras, pero por ahora solo haremos estas dos subclases en blanco de BaseAttribute:

RawBonus.as:

 paquete clase pública RawBonus extiende BaseAttribute función pública RawBonus (valor: int = 0, multiplicador: Número = 0) super (valor, multiplicador); 

FinalBonus.as:

 paquete clase pública FinalBonus extiende BaseAttribute función pública FinalBonus (valor: int = 0, multiplicador: Número = 0) super (valor, multiplicador); 

Como puedes ver, estas clases no tienen nada más que un constructor..


La clase de atributo

los Atributo clase será el equivalente de un grupo en el patrón compuesto. Puede contener cualquier bonificación en bruto o final, y tendrá un método para calcular el valor final del atributo. Ya que es una subclase de BaseAttribute, la _valor base El campo de la clase será el valor inicial del atributo..

Al crear la clase, tendremos un problema al calcular el valor final del atributo: ya que no estamos separando los bonos en bruto de los bonos finales, no hay manera de que podamos calcular el valor final, porque no sabemos cuándo. aplicar cada bono.

Esto se puede resolver haciendo una ligera modificación en el patrón compuesto básico. En lugar de agregar cualquier niño al mismo "contenedor" dentro del grupo, crearemos dos "contenedores", uno para los bonos en bruto y otro para los bonos finales. Cada bono seguirá siendo un hijo de Atributo, pero estará en diferentes lugares para permitir el cálculo del valor final del atributo.

Con eso explicado, vamos al código!

 package public class Attribute extiende BaseAttribute private var _rawBonuses: Array; private var _finalBonuses: Array; private var _finalValue: int; Atributo de función pública (startingValue: int) super (startingValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  función pública addRawBonus (bonus: RawBonus): void _rawBonuses.push (bonus);  función pública addFinalBonus (bonus: FinalBonus): void _finalBonuses.push (bonus);  función pública removeRawBonus (bonus: RawBonus): void if (_rawBonuses.indexOf (bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (bonus), 1);  función pública removeFinalBonus (bonus: FinalBonus): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  función pública calculaValue (): int _finalValue = baseValue; // Agregando valor de raw var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; para cada (var bonus: RawBonus en _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier); // Agregando valor de final var finalBonusValue: int = 0; var finalBonusMultiplier: Number = 0; para cada (var bonus: FinalBonus en _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier); return _finalValue;  public function get finalValue (): int return calculaValue (); 

Los métodos addRawBonus (), addFinalBonus (), removeRawBonus () y removeFinalBonus () son muy claros Todo lo que hacen es agregar o eliminar su tipo de bonificación específico desde o hacia la matriz que contiene todas las bonificaciones de ese tipo.

La parte difícil es la calcular Valor () método. Primero, resume todos los valores que los bonos en bruto agregan al atributo, y también resume todos los multiplicadores. Después de eso, agrega la suma de todos los valores de bonificación en bruto al atributo inicial y luego aplica el multiplicador. Más tarde, realiza el mismo paso para los bonos finales, pero esta vez aplicando los valores y los multiplicadores al valor del atributo final medio calculado.

¡Y hemos terminado con la estructura! Revisa los siguientes pasos para ver cómo lo usarías y extenderlo.


Comportamiento extra: bonos temporizados

En nuestra estructura actual, solo tenemos primas simples y finales, que actualmente no tienen ninguna diferencia. En este paso, añadiremos un comportamiento extra a la FinalBonus clase, con el fin de que se parezca más a los bonos que se aplicarían a través de activo habilidades en un juego.

Como, como su nombre lo indica, dichas habilidades solo están activas durante un cierto período de tiempo, agregaremos un comportamiento de tiempo en las bonificaciones finales. Los bonos en bruto podrían utilizarse, por ejemplo, para bonos agregados a través del equipo..

Para ello, utilizaremos el Minutero clase. Esta clase es nativa de ActionScript 3.0, y todo lo que hace es comportarse como un temporizador, comenzando en 0 segundos y luego llamando a una función especificada después de un período de tiempo específico, restableciendo de nuevo a 0 e iniciando el conteo nuevamente, hasta que alcance el valor especificado. número de cuentas de nuevo. Si no los especificas, el Minutero Seguirá corriendo hasta que lo detengas. Puede elegir cuándo se inicia el temporizador y cuándo se detiene. Puede replicar su comportamiento simplemente usando los sistemas de tiempo de su idioma con el código adicional apropiado, si es necesario.

Saltemos al código!

 package import flash.events.TimerEvent; import flash.utils.Timer; clase pública FinalBonus extiende BaseAttribute private var _timer: Timer; private var _parent: Attribute; función pública FinalBonus (tiempo: int, valor: int = 0, multiplicador: Número = 0) super (valor, multiplicador); _timer = nuevo temporizador (tiempo); _timer.addEventListener (TimerEvent.TIMER, onTimerEnd);  public function startTimer (parent: Attribute): void _parent = parent; _timer.start ();  función privada en TimerEnd (e: TimerEvent): void _timer.stop (); _parent.removeFinalBonus (this); 

En el constructor, la primera diferencia es que las bonificaciones finales ahora requieren un hora parámetro, que mostrará cuánto tiempo duran. Dentro del constructor, creamos un Minutero para esa cantidad de tiempo (asumiendo que el tiempo es en milisegundos), y agregue un detector de eventos.

(Los escuchas de eventos son básicamente lo que hará que el temporizador llame a la función correcta cuando llegue a ese cierto período de tiempo; en este caso, la función que se debe llamar es onTimerEnd ().)

Tenga en cuenta que aún no hemos iniciado el temporizador. Esto se hace en el startTimer () Método, que también requiere un parámetro., padre, que debe ser un Atributo. Esta función requiere el atributo que está agregando el bono para llamar a esa función para activarlo; a su vez, esto inicia el temporizador y le dice a la bonificación qué instancia solicitar para eliminar la bonificación cuando el temporizador haya alcanzado su límite.

La parte de eliminación se realiza en el onTimerEnd () Método, que solo le pedirá al padre del set que lo elimine y detenga el temporizador.

Ahora, podemos usar las bonificaciones finales como bonificaciones cronometradas, lo que indica que durarán solo una cierta cantidad de tiempo.


Comportamiento Extra: Atributos Dependientes

Una cosa que se ve comúnmente en los juegos de rol son los atributos que dependen de otros. Tomemos, por ejemplo, el atributo "velocidad de ataque". No solo depende del tipo de arma que uses, sino también casi siempre de la destreza del personaje..

En nuestro sistema actual, solo permitimos que los bonos sean hijos de Atributo instancias. Pero en nuestro ejemplo, debemos permitir que un atributo sea un hijo de otro atributo. ¿Cómo podemos hacer eso? Podemos crear una subclase de Atributo, llamado DependantAttribute, Y dar a esta subclase todo el comportamiento que necesitamos..

Agregar atributos como hijos es muy simple: todo lo que tenemos que hacer es crear otra matriz para contener atributos y agregar un código específico para calcular el atributo final. Ya que no sabemos si todos los atributos se calcularán de la misma manera (es posible que primero desees usar la destreza para cambiar la velocidad de ataque, y luego verifica las bonificaciones, pero primero usa las bonificaciones para cambiar el ataque mágico y luego usa, por ejemplo, inteligencia), también tendremos que separar el cálculo del atributo final en el Atributo Clase en diferentes funciones. Hagamos eso primero.

En Atributo.as:

 package public class Attribute extiende BaseAttribute private var _rawBonuses: Array; private var _finalBonuses: Array; protegido var _finalValue: int; Atributo de función pública (startingValue: int) super (startingValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  función pública addRawBonus (bonus: RawBonus): void _rawBonuses.push (bonus);  función pública addFinalBonus (bonus: FinalBonus): void _finalBonuses.push (bonus);  función pública removeRawBonus (bonus: RawBonus): void if (_rawBonuses.indexOf (bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (bonus), 1);  función pública removeFinalBonus (bonus: RawBonus): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  función protegida applyRawBonuses (): void // Agregar valor de raw var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; para cada (var bonus: RawBonus en _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier);  función protegida applyFinalBonuses (): void // Agregar valor de final var finalBonusValue: int = 0; var finalBonusMultiplier: Number = 0; para cada (var bonus: RawBonus en _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier);  public function calculaValue (): int _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); return _finalValue;  public function get finalValue (): int return calculaValue (); 

Como se puede ver por las líneas resaltadas, todo lo que hicimos fue crear applyRawBonuses () y applyFinalBonuses () y llámalos al calcular el atributo final en calcular Valor (). Tambien hicimos _valor final Protegido, para que podamos cambiarlo en las subclases..

Ahora, todo está listo para que nosotros creamos el DependantAttribute ¡clase! Aquí está el código:

 package clase pública DependantAttribute extiende el atributo protected var _otherAttributes: Array; función pública DependantAttribute (startingValue: int) super (startingValue); _otherAttributes = [];  public function addAttribute (attr: Attribute): void _otherAttributes.push (attr);  public function removeAttribute (attr: Attribute): void if (_otherAttributes.indexOf (attr)> = 0) _otherAttributes.splice (_otherAttributes.indexOf (attr), 1);  función de anulación pública calculaValue (): int // El código de atributo específico va a alguna parte aquí _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); return _finalValue; 

En esta clase, la addAttribute () y removeAttribute () Las funciones deben ser familiares para usted. Tienes que prestar atención a la anulación calcular Valor () función. Aquí, no utilizamos los atributos para calcular el valor final; debe hacerlo para cada atributo dependiente!

Este es un ejemplo de cómo lo harías para calcular la velocidad de ataque:

 package public class AttackSpeed ​​extiende DependantAttribute public function AttackSpeed ​​(startingValue: int) super (startingValue);  función de anulación pública calculaValue (): int _finalValue = baseValue; // Cada 5 puntos en destreza agrega 1 a la velocidad de ataque var destreza: int = _otherAttributes [0] .calculateValue (); _finalValue + = int (destreza / 5); applyRawBonuses (); applyFinalBonuses (); return _finalValue; 

En esta clase, asumimos que ha agregado el atributo de destreza ya como hijo de AttackSpeed, y que es la primera en el _otrastributos matriz (hay muchas suposiciones que hacer; verifique la conclusión para obtener más información). Después de recuperar la destreza, simplemente la usamos para agregar más al valor final de la velocidad de ataque.


Conclusión

Con todo terminado, ¿cómo usarías esta estructura en un juego? Es muy simple: todo lo que necesita hacer es crear diferentes atributos y asignar a cada uno de ellos un Atributo ejemplo. Después de eso, se trata de agregar y eliminar bonos a través de los métodos ya creados..

Cuando un elemento está equipado o usado y agrega una bonificación a cualquier atributo, debe crear una instancia de bonificación del tipo correspondiente y luego agregarla al atributo del personaje. Después de eso, simplemente vuelva a calcular el valor del atributo final.

También puede ampliar los diferentes tipos de bonos disponibles. Por ejemplo, podría tener un bono que cambia el valor agregado o el multiplicador con el tiempo. También puedes usar bonos negativos (que el código actual ya puede manejar).

Con cualquier sistema, siempre hay más que puedes agregar. Aquí hay algunas mejoras sugeridas que podría hacer:

  • Identificar atributos por nombres
  • Hacer un sistema "centralizado" para gestionar los atributos.
  • Optimice el rendimiento (sugerencia: no siempre tiene que calcular el valor final por completo)
  • Hacer posible que algunas bonificaciones atenúen o fortalezcan otras bonificaciones

Gracias por leer!