Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Game Development
  2. Programming

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

by
Read Time:15 minsLanguages:

Spanish (Español) translation by Juan Pablo Diaz Cuartas (you can also view the original English article)

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 temporizadas y tener en cuenta el efecto de los elementos 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 usando 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 se usan muy comúnmente en los juegos de rol para cuantificar las fortalezas, debilidades y habilidades de los personajes. Si no está familiarizado con ellos, lea la página de Wikipedia para obtener una descripción 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 cualquier otro efecto) 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 bonificaciones y funcionará en cualquier conjunto de atributos que defina.


¿Cuál 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 esto, puede omitir Modelar nuestro problema.

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

Vamos a utilizar un ejemplo ampliamente utilizado para ilustrar esto: piense en una simple aplicación de dibujo. Quiere que le permita dibujar triángulos, cuadrados y círculos, y tratarlos de manera diferente. Pero también quiere 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 un "grupo de dibujos" como un dibujo en sí, uno podría agregar fácilmente cualquier dibujo dentro de este grupo, y el grupo como un todo se vería como un solo dibujo.

En términos de programación, tendríamos una clase base, Drawing, que tiene los comportamientos predeterminados de un dibujo (puedes moverlo, cambiar capas, rotarlo, etc.) y cuatro subclases, Triangle, Square, Circle y Grupo.

En este caso, las primeras tres clases tendrán un comportamiento simple, requiriendo solo la entrada del usuario de los atributos básicos de cada forma. La clase de grupo, sin embargo, tendrá métodos para agregar y eliminar formas, así como realizar una operación en todos ellos (por ejemplo, cambiar el color de todas las formas en un grupo a la vez). Las cuatro subclases seguirán siendo tratadas como un Dibujo, por lo que no tendrá que preocuparse por agregar un código específico para cuando quiera 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, excepto los nodos de grupo, 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 consta de un círculo y un cuadrado:

Using the Composite Design Pattern for an RPG Attributes System

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

Using the Composite Design Pattern for an RPG Attributes System

¿Qué pasaría 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 agregamos, ya que agregaríamos cualquier dibujo dentro de un grupo. Así es como se vería la representación visual:

Using the Composite Design Pattern for an RPG Attributes System

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

Using the Composite Design Pattern for an RPG Attributes SystemUsing the Composite Design Pattern for an RPG Attributes SystemUsing the Composite Design Pattern for an RPG Attributes System

Ahora, imagina 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), así que empezaremos a pensar en el patrón compuesto con la representación de á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 bruto al atributo o aumentarlo en un porcentaje. Hay bonificaciones que se suman al atributo y otras que se calculan después de aplicar todos los primeros bonos (bonos de habilidades, por ejemplo).

Entonces, podemos tener:

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

Puede haber notado que no estamos separando los bonos que agregan un valor al atributo de los bonos que aumentan el atributo en un porcentaje. Esto se debe a que estamos modelando cada bonificación para poder cambiar, ya sea al mismo tiempo. Esto significa que podríamos tener una bonificación que agrega 5 al valor y aumenta el atributo en un 10%. Esto se manejará en el código.

Estos dos tipos de bonificaciones son solo las hojas de nuestro árbol. Se parecen mucho a las clases Triangle, Square y Circle en nuestro ejemplo anterior.

Todavía no hemos creado una entidad que sirva como grupo. ¡Estas entidades serán los atributos mismos! La clase grupal en nuestro ejemplo será simplemente el atributo en sí. Entonces, tendremos una clase de atributo que se comportará como cualquier atributo.

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

Using the Composite Design Pattern for an RPG Attributes SystemUsing the Composite Design Pattern for an RPG Attributes SystemUsing the Composite Design Pattern for an RPG Attributes System

Ahora que todo está decidido, ¿debemos comenzar nuestro código?


Creando las clases base

Utilizaremos 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 este tutorial sin problemas

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

Como puede ver, las cosas son muy simples en esta clase base. Simplemente creamos los campos _value y _multiplier, los asignamos en el constructor y hacemos dos métodos getter, uno para cada campo.

Ahora tenemos que crear las clases RawBonus y FinalBonus. Estas son simplemente subclases de BaseAttribute, sin agregar nada. Puedes expandirlo tanto como quieras, pero por ahora solo haremos estas dos subclases en blanco de BaseAttribute:

RawBonus.as:

FinalBonus.as:

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


La clase de atributo

La clase de atributo 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. Como es una subclase de BaseAttribute, el campo _baseValue de la clase será el valor inicial del atributo.

Al crear la clase, tendremos un problema al calcular el valor final del atributo: dado que no estamos separando los bonos brutos de los bonos finales, no hay manera de que podamos calcular el valor final, porque no sabemos cuándo aplica cada bonificación

Esto puede resolverse haciendo una pequeña modificación al 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 bonificación seguirá siendo un elemento secundario de Attribute, pero estará en diferentes lugares para permitir el cálculo del valor final del atributo.

Con eso explicado, ¡vayamos al código!

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 ao desde la matriz que contiene todas las bonificaciones de ese tipo.

La parte engañosa es el método calculateValue (). Primero, resume todos los valores que los bonos brutos agregan al atributo y también resume todos los multiplicadores. Después de eso, agrega la suma de todos los valores de bonificación sin procesar al atributo de inicio y luego aplica el multiplicador. Más tarde, hace el mismo paso para los bonos finales, pero esta vez aplicando los valores y multiplicadores al valor del atributo final medio calculado.

¡Y terminamos con la estructura! Consulte los siguientes pasos para ver cómo usaría y extiéndalo.


Comportamiento adicional: bonificaciones temporizadas

En nuestra estructura actual, solo tenemos bonos simples en bruto y finales, que actualmente no tienen ninguna diferencia. En este paso, agregaremos un comportamiento adicional a la clase FinalBonus, para que se vea más como bonos que se aplicarían a través de habilidades activas en un juego.

Dado que, como su nombre lo indica, tales 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 usarse, por ejemplo, para bonificaciones agregadas a través del equipo.

Para hacer esto, usaremos la clase Timer. 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 específica después de un período de tiempo especificado, reiniciando de nuevo a 0 y comenzando de nuevo, hasta que alcanza el valor especificado número de conteos de nuevo. Si no los especifica, el temporizador seguirá funcionando hasta que lo detenga. Puede elegir cuándo se inicia el temporizador y cuándo se detiene. Puede replicar su comportamiento simplemente utilizando los sistemas de temporización de su idioma con el código adicional apropiado, si es necesario.

¡Saltemos al código!

En el constructor, la primera diferencia es que los bonos finales ahora requieren un parámetro de tiempo, que mostrará cuánto duran. Dentro del constructor, creamos un temporizador para esa cantidad de tiempo (suponiendo que el tiempo está en milisegundos) y le agregamos 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 a la que se llama es onTimerEnd ()).

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

La parte de eliminación se realiza en el método onTimerEnd (), que simplemente le pedirá al elemento principal que lo elimine y lo detenga.

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


Comportamiento adicional: atributos dependientes

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

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

Agregar atributos como niños es muy simple: todo lo que tenemos que hacer es crear otra matriz para mantener los atributos y agregar un código específico para calcular el atributo final. Dado que no sabemos si cada atributo se calculará de la misma manera (es posible que desee utilizar primero la destreza para cambiar la velocidad de ataque, y luego verificar los bonos, pero primero use los bonos para cambiar el ataque mágico y luego use, por ejemplo, inteligencia), también tendremos que separar el cálculo del atributo final en la clase de Atributo en diferentes funciones. Hagámoslo primero.

En Attribute.as:

Como puede ver por las líneas resaltadas, todo lo que hicimos fue crear applyRawBonuses () y applyFinalBonuses () y llamarlos al calcular el atributo final en calculateValue (). También protegimos _finalValue, por lo que podemos cambiarlo en las subclases.

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

En esta clase, las funciones addAttribute () y removeAttribute () deberían serle familiares. Debe prestar atención a la función overriden calculateValue (). Aquí, no usamos 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:

En esta clase, suponemos que ha agregado el atributo de destreza ya como hijo de AttackSpeed, y que es el primero en la matriz _otherAttributes (hay muchas suposiciones que hacer, consulte 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 asignarles a cada uno de ellos una instancia de atributo. 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 se usa y agrega una bonificación a cualquier atributo, debe crear una instancia de bonificación del tipo correspondiente y luego agregarlo al atributo del personaje. Después de eso, simplemente recalcula el valor final del atributo.

También puede ampliar los diferentes tipos de bonos disponibles. Por ejemplo, podría tener una bonificación que cambie el valor agregado o el multiplicador a lo largo del tiempo. También puede usar bonificaciones negativas (que el código actual ya puede manejar).

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

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

¡Gracias por leer!

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.