Cómo codificar Monster Loot Drops
Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
Un mecanismo común en los juegos de acción es que los enemigos arrojen algún tipo de objeto o recompensa al morir. El personaje puede recoger este botín para obtener alguna ventaja. Es un mecanismo que se espera en muchos juegos, como los juegos de rol, ya que le da al jugador un incentivo para deshacerse de los enemigos, así como una pequeña explosión de endorfinas al descubrir cuál es la recompensa inmediata por hacerlo.
En este tutorial, revisaremos el funcionamiento interno de dicha mecánica y veremos cómo implementarla, sin importar el tipo de juego y la herramienta / lenguaje de codificación que pueda estar usando.
Los ejemplos que uso para demostrar esto se hicieron usando Construct 2, una herramienta de creación de juegos HTML5, pero no son de ninguna manera específicos para él. Debería poder implementar la misma mecánica sea cual sea su lenguaje o herramienta de codificación.
Los ejemplos se
hicieron en r167.2 y se pueden abrir y editar en la versión gratuita del
software. Puede
descargar la última versión de Construct 2 aquí (desde que comencé a
escribir este artículo, se han lanzado al menos dos versiones más
nuevas) y perder el tiempo con los ejemplos a su gusto. Los ejemplos de
los archivos fuente CAPX se adjuntan a este tutorial en el archivo
zip.
La mecánica básica



A la muerte de un enemigo (entonces, cuando su HP es menor o igual a cero) se llama a una función. La función de esta función es determinar si hay una caída o no, y si es así, el tipo de caída debería ser.
La función también puede manejar la creación de la representación visual de la gota, generándola en las antiguas coordenadas de pantalla del enemigo.
Considere el siguiente ejemplo:
Haga clic en el botón Slay 100 Beasts. Esto ejecutará un proceso por lotes que crea 100 bestias aleatorias, las mata y muestra el resultado para cada bestia (es decir, si la bestia arroja un objeto y, de ser así, qué tipo de elemento). Las estadísticas en la parte inferior de la pantalla muestran cuántas bestias arrojaron elementos y cuántos de cada tipo de elemento se eliminaron.
Este ejemplo es estrictamente de texto para mostrar la lógica detrás de la función, y para mostrar que esta mecánica se puede aplicar a cualquier tipo de juego, ya sea un juego de plataformas en el que pise a los enemigos, o un juego de disparos de vista de arriba hacia abajo, o un juego de roles
Veamos cómo
funciona esta demostración. Primero, las bestias y las gotas están
contenidas en matrices. Aquí está la matriz beast:
| Índice (X) | Nombre
(Y-0) | Tasa de caída (Y-1) | Artículo rareza (Y-2) |
| 0 | Jabali | 100 | 100 |
| 1 | Duende | 75 | 75 |
| 2 | Escudero | 65 | 55 |
| 3 | ZogZog | 45 | 100 |
| 4 | Búho | 15 | 15 |
| 5 | Mastodonte | 35 | 50 |
Y aquí está la matriz drops:
| Índice (X) | Nombre (Y-0) | Raramente del artículo (Y-1) |
| 0 | Chupete | 75 |
| 1 | Oro | 50 |
| 2 | Rocas | 95 |
| 3 | Joya | 25 |
| 4 | Incienso | 35 |
| 5 | Equipo | 15 |
El valor X (la columna Index) de la matriz actúa como un identificador único para la bestia o el tipo de elemento. Por ejemplo, la bestia del índice 0 es un Boar. El ítem del índice 3
es una joya Jewel.
Estas matrices actúan como tablas de búsqueda para nosotros, que contienen el nombre o tipo de cada bestia o elemento, así como otros valores que nos permitirán determinar la rareza o la tasa de abandono. En la matriz de bestias, hay dos columnas más después del nombre:
La Drop rate tasa de caída es la
probabilidad de que la bestia caiga un objeto cuando muere. Por ejemplo,
el jabalí tendrá un 100% de probabilidad de soltar un
objeto cuando muere, mientras que el búho tendrá un 15% de posibilidades
de hacer lo mismo.
Rarity define cuán poco comunes son los elementos que
puede arrojar esta bestia. Por
ejemplo, es probable que un jabalí suelte objetos de un valor de rareza
de 100. Ahora, si comprobamos el array drops, podemos ver que las
rocas son el elemento con mayor rareza (95). (A pesar de que el valor
de rareza es alto, debido a la forma en que
programé la función, cuanto más grande es el número de rareza, más común
es el objeto. Tiene más posibilidades de soltar las rocas que un
elemento con un valor de rareza más bajo).
Y eso es interesante para nosotros desde la perspectiva del diseño del juego. Para el resto del juego, no queremos que el jugador tenga acceso a demasiados equipos o demasiados artículos de alta gama demasiado pronto; de lo contrario, el personaje podría verse dominado demasiado pronto, y el juego será menos interesante para jugar.
Estas tablas y valores son solo ejemplos, y puedes y debes jugar con ellos y adaptarlos a tu propio sistema de juego y universo. Todo depende del equilibrio de tu sistema. Si desea obtener más información sobre el tema del equilibrio, le recomiendo consultar esta serie de tutoriales: Equilibrar los juegos de rol basados en turnos.
Veamos ahora el (pseudo) código para la demostración:
1 |
CONSTANT BEAST_NAME = 0 |
2 |
CONSTANT BEAST_DROPRATE = 1 |
3 |
CONSTANT BEAST_RARITY = 2 |
4 |
CONSTANT DROP_NAME = 0 |
5 |
CONSTANT DROP_RATE = 1 |
6 |
//Those constants are used for a better readability of the arrays |
7 |
|
8 |
On start of the project, fill the arrays with the correct values |
9 |
array aBeast(6,3) //The array that contains the values for each beast |
10 |
array aDrop(6,2) //The array that contains the values for each item |
11 |
array aTemp(0) //A temporary array that will allow us what item type to drop |
12 |
array aStats(6) //The array that will contain the amount of each item dropped |
13 |
|
14 |
On button clicked |
15 |
Call function "SlainBeast(100)" |
16 |
|
17 |
Function SlainBest (Repetitions) |
18 |
int BeastDrops = 0 //The variable that will keep the count of how many beasts did drop item |
19 |
Text.text = "" |
20 |
aStats().clear //Resets all the values contained in this array to make new statistics for the current batch |
21 |
Repeat Repetitions times |
22 |
int BeastType |
23 |
int DropChance |
24 |
int Rarity |
25 |
BeastType = Random(6) //Since we have 6 beasts in our array |
26 |
Rarity = aBeast(BeastType, BEAST_RARITY) //Get the rarity of items the beast should drop from the aBeast array |
27 |
DropChance = ceil(random(100)) //Picks a number between 0 and 100) |
28 |
Text.text = Text.text & loopindex & " _ " & aBeast(BeastType,BEAST_NAME) & "is slain" |
29 |
|
30 |
If DropChance > aBeast(BeastType,BEAST_DROPRATE) |
31 |
//The DropChance is bigger than the droprate for this beast |
32 |
Text.text = Text.text & "." & newline |
33 |
//We stop here, this beast is considered to not have dropped an item. |
34 |
|
35 |
If DropChance <= aBeast(BeastType,BEAST_DROPRATE) |
36 |
Text.text = Text.Text & " dropping " //We will put some text to display what item was dropped |
37 |
//On the other hand, DropChance is less or equal the droprate for this beast |
38 |
aTemp(0) //We clear/clean the aTemp array in which we will push entries to determine what item type to drop |
39 |
For a = 0 to aDrop.Width //We will loop through every elements of the aDrop array |
40 |
aDrop(a,DROP_RATE) >= Rarity //When the item drop rate is greater or equal the expected Rarity |
41 |
Push aTemp,a //We put the current a index in the temp array. We know that this index is a possible item type to drop |
42 |
int DropType |
43 |
DropType = random(aTemp.width) //The DropType is one of the indexes contained in the temporary array |
44 |
Text.text = Text.text & aDrop(DropType, DROP_NAME) & "." & newline //We display the item name that was dropped |
45 |
//We do some statistics |
46 |
aStats(DropType) = aStats(DropType) + 1 |
47 |
BeastDrops = BeastDrops + 1 |
48 |
TextStats.Text = BeastDrops & " beasts dropped items." & newline |
49 |
For a = 0 to aStats.width //Display each item amount that was dropped |
50 |
and aStats(a) > 0 |
51 |
TextStats.Text = TextStats.Text & aStats(a) & " " & aDrop(a,DROP_NAME) & " " |
52 |
Primero, la acción del usuario: hacer clic en el botón Slay 100 Beasts. Este botón llama a una función con un parámetro de 100, solo porque 100
se siente como un buen número de enemigos para matar. En un juego real,
es más probable que mates animales uno por uno, por supuesto.
A partir de
esto, se llama a la función SlainBeast. Su propósito es mostrar un
texto para dar a los usuarios su opinión sobre lo que sucedió. Primero,
limpia la variable BeastDrops y una matriz aStats, que se usan para las
estadísticas. En un juego real, es poco probable que los necesites. También limpia Text, de modo que se mostrarán 100 nuevas líneas para
ver los resultados de este lote. En la función en sí, se crean tres
variables numéricas: BeastType, DropChance y Rarity.
BeastType será el
índice que usemos para referirnos a una fila específica en la matriz
aBeast; es básicamente el tipo de bestia que el jugador tuvo que
enfrentar y matar. La rareza Rarity también se toma de la matriz aBeast; es la
rareza del elemento que esta bestia debería caer, el valor del campo Item rarity en la matriz aBeast.
Finalmente,
DropChance es un número que seleccionamos aleatoriamente entre 0 y 100. (La mayoría de los lenguajes de codificación tendrán una función para
obtener un número aleatorio de un rango, o al menos para obtener un
número aleatorio entre 0 y 1, que luego podría simplemente multiplicarse
por 100.)
En
este punto, podemos mostrar nuestro primer bit de información en el
objeto Text: ya sabemos qué tipo de bestia generó y fue asesinada. Por
lo tanto, concatenamos el valor actual de Text.text BEAST_NAME del
BeastType actual que elegimos al azar, fuera de la matriz aBeast.
A
continuación, tenemos que determinar si un artículo se descartará. Lo
hacemos comparando el valor DropChance con el valor BEAST_DROPRATE de la
matriz aBeast. Si DropChance es menor o igual que este valor, soltamos
un elemento.
(Decidí optar por el enfoque "menor o igual que", después de haber sido influenciado por estos jugadores en vivo usando el conjunto de reglas de D & D King Arthur: Pendragon con respecto a tiras de dados, pero podrías codificar la función al revés , decidiendo que las caídas solo ocurrirán cuando sea "mayor o igual". Es solo una cuestión de valores numéricos y lógica. Sin embargo, manténgase constante a lo largo de su algoritmo y no cambie la lógica a la mitad, de lo contrario, podría terminar con problemas al intentar depurarlo o mantenerlo).
Por lo tanto, dos líneas determinan si un elemento se descarta o no. Primero:
1 |
DropChance > aBeast(BeastType,BEAST_DROPRATE) |
Aquí, DropChance es mayor que DropRate, y consideramos que esto significa que no se descarta ningún elemento. A partir de ahí, lo único que se muestra es un cierre "." (punto) que
termina la oración, "[BeastType] fue asesinado.", antes de pasar al
siguiente enemigo en nuestro lote.
Por otra parte:
1 |
DropChance <= aBeast(BeastType,BEAST_DROPRATE) |
Aquí,
DropChance es menor o igual que el DropRate para el BeastType actual,
por lo que consideramos que significa que un elemento se descarta. Para hacerlo, haremos una comparación entre la rareza Rarity de los ítems que
el "BeastType" actual está "permitido" y los varios valores raros que
hemos configurado en la tabla aDrop.
Recorrimos la tabla aDrop,
verificando cada índice para ver si su DROP_RATE es mayor o igual a
Rarity. (Recuerde, en contra de la intuición, cuanto mayor es el valor
de
Rarity, más común es el elemento) Para cada índice que coincida con la
comparación, colocamos ese índice en una matriz temporal, aTemp.
Al final
del ciclo, deberíamos tener al menos un índice en la matriz aTemp. (Si
no, tenemos que rediseñar nuestras tablas aDrop y aEast). Luego creamos
una nueva variable numérica DropType que selecciona aleatoriamente uno
de los índices de la matriz aTemp; este será el artículo que
soltamos.
Agregamos
el nombre del elemento a nuestro objeto de Texto, haciendo que la
oración sea algo así como "BeastType fue asesinado, dejando caer un
DROP_NAME". Luego, por el bien de este ejemplo, agregamos algunos
números a
nuestras diversas estadísticas (en la matriz aStats y en
BeastDrops).
Finalmente, después de las 100 repeticiones, mostramos esas
estadísticas, el número de bestias (de 100) que arrojaron elementos y el
número de cada elemento que se eliminó.
Otro ejemplo: dejar caer objetos
visualmente
Consideremos otro ejemplo:
Presiona Espacio para crear una bola de fuego que matará al enemigo.
Como puede ver, se crea un enemigo aleatorio (de un bestiario de 11). El personaje jugador (a la izquierda) puede crear un ataque de proyectil. Cuando el proyectil golpea al enemigo, el enemigo muere.
A
partir de ahí, una función similar a la que hemos visto en el ejemplo
anterior determina si el enemigo está eliminando algún elemento o no, y
determina cuál es el elemento. Esta vez, también
crea la representación visual del artículo eliminado y actualiza las
estadísticas en la parte inferior de la pantalla.
Aquí hay una implementación en pseudocódigo:
1 |
CONSTANT ENEMY_NAME = 0 |
2 |
CONSTANT ENEMY_DROPRATE = 1 |
3 |
CONSTANT ENEMY_RARITY = 2 |
4 |
CONSTANT ENEMY_ANIM = 3 |
5 |
CONSTANT DROP_NAME = 0 |
6 |
CONSTANT DROP_RATE = 1 |
7 |
//Constants for the readability of the arrays
|
8 |
|
9 |
int EnemiesSpawned = 0 |
10 |
int EnemiesDrops = 0 |
11 |
|
12 |
array aEnemy(11,4) |
13 |
array aDrop(17,2) |
14 |
array aStats(17) |
15 |
array aTemp(0) |
16 |
|
17 |
On start of the project, we roll the data in aEnemy and aDrop |
18 |
Start Timer "Spawn" for 0.2 second |
19 |
|
20 |
Function "SpawnEnemy" |
21 |
int EnemyType = 0 |
22 |
EnemyType = random(11) //We roll an enemy type out of the 11 available |
23 |
Create object Enemy //We create the visual object Enemy on screen |
24 |
Enemy.Animation = aEnemy(EnemyType, ENEMY_ANIM) |
25 |
EnemiesSpawned = EnemiesSpawned + 1 |
26 |
txtEnemy.text = aEnemy(EnemyType, ENEMY_NAME) & " appeared" |
27 |
Enemy.Name = aEnemy(EnemyType, ENEMY_NAME) |
28 |
Enemy.Type = EnemyType |
29 |
|
30 |
Keyboard Key "Space" pressed |
31 |
Create object Projectile from Char.Position |
32 |
|
33 |
Projectile collides with Enemy |
34 |
Destroy Projectile |
35 |
Enemy start Fade |
36 |
txtEnemy.text = Enemy.Name & " has been vanquished." |
37 |
|
38 |
Enemy Fade finished |
39 |
Start Timer "Spawn" for 2.5 seconds //Once the fade out is finished, we wait 2.5 seconds before spawning a new enemy at a random position on the screen |
40 |
Function "Drop" (Enemy.Type, Enemy.X, Enemy.Y, Enemy.Name) |
41 |
|
42 |
Function Drop (EnemyType, EnemyX, EnemyY, EnemyName) |
43 |
int DropChance = 0 |
44 |
int Rarity = 0 |
45 |
DropChance = ceil(random(100)) |
46 |
Rarity = aEnemy(EnemyType, ENEMY_RARITY) |
47 |
txtEnemy.text = EnemyName & " dropped " |
48 |
|
49 |
If DropChance > aEnemy(EnemyType, ENEMY_DROPRATE) |
50 |
txtEnemy.text = txtEnemy.text & " nothing." |
51 |
//Nothing was dropped
|
52 |
If DropChance <= aEnemy(EnemyType, ENEMY_DROPRATE) |
53 |
aTemp.clear/set size to 0 |
54 |
For a = 0 to aDrop.Width |
55 |
and aDrop(a, DROP_RATE) >= Rarity |
56 |
aTemp.Push(a) //We push the current index into the aTemp array as possible drop index |
57 |
|
58 |
int DropType = 0 |
59 |
DropType = Random(aTemp.Width) //We pick what is the drop index amongst the indexes stored in aTemp |
60 |
aStats(DropType) = aStats(DropType) + 1 |
61 |
EnemiesDrops = EnemiesDrops + 1 |
62 |
Create Object Drop at EnemyX, EnemyY |
63 |
Drop.AnimationFrame = DropType |
64 |
txtEnemy.Text = txtEnemy.Text & aDrop.(DropType, DROP_NAME) & "." //We display the name of the drop |
65 |
txtStats.text = EnemiesDrops & " enemies on " & EnemiesSpawned & " dropped items." & newline |
66 |
For a = 0 to aStats.width |
67 |
and aStats(a) > 0 |
68 |
txtStats.text = txtStats.Text & aStats(a) & " " & aDrop(a, DROP_NAME) & " " |
69 |
|
70 |
Timer "Spawn" |
71 |
Call Function "SpawnEnemy" |
Eche un vistazo al contenido de las tablas aEnemy y aDrop, respectivamente:
| Índice (X) | Nombre (Y-0) | Tasa de caída (Y-1) | Rarasidad del artículo (Y-2) | Nombre de la animación (Y-3) |
| 0 | Sanadora Hembra | 100 | 100 | Healer_F |
| 1 | Sanador Masculino | 75 | 75 | Healer_M |
| 2 | Mago Hembra | 65 | 55 | Mage_F |
| 3 | Mago macho | 45 | 100 | Mage_M |
| 4 | Ninja Mujer | 15 | 15 | Ninja_F |
| 5 | Ninja macho | 35 | 50 | Ninja_M |
| 6 | guardabosques masculino | 75 | 80 | Ranger_M |
| 7 | Gente del pueblo femenina | 75 | 15 | Townfolk_F |
| 8 | Gente del pueblo masculino | 95 | 95 | Townfolk_M |
| 9 | Guerrero femenino | 70 | 70 | Warrior_F |
| 10 | Guerrero Hombre | 45 | 55 | Warrior_M |
| Índice (X) | Nombre (Y-0) | Raramente del artículo (Y-1) |
| 0 | Manzana | 75 |
| 1 | Banana | 50 |
| 2 | Zanahoria | 95 |
| 3 | Uva | 95 |
| 4 | Poción vacía | 80 |
| 5 | Poción azul | 75 |
| 6 | Poción roja | 70 |
| 7 | Poción verde | 60 |
| 8 | Corazón rosado | 65 |
| 9 | Perla azul | 15 |
| 10 | Rock | 100 |
| 11 | Guante | 25 |
| 12 | Armadura | 30 |
| 13 | Joya | 35 |
| 14 | Sombrero de mago | 65 |
| 15 | Escudo de madera | 85 |
| 16 | Hacha de hierro | 65 |
A
diferencia del ejemplo anterior, la matriz que contiene los datos del
enemigo se llama aEnemy y contiene una fila más de datos, ENEMY_ANIM,
que tiene el nombre de la animación del enemigo. De esta forma, cuando generamos al enemigo, podemos buscarlo y
automatizar la visualización gráfica.
En
la misma línea, aDrop ahora contiene 16 elementos, en lugar de seis, y
cada índice se refiere al marco de la animación del objeto, pero también
podría haber tenido varias animaciones, como para los enemigos, si los
elementos abandonados fueran animados.
Esta vez, hay muchos más enemigos y elementos que en el ejemplo anterior. Sin embargo, puede ver que los datos relativos a las tasas de abandono y los valores de rareza todavía existen. Una diferencia notable es que hemos separado el desove de los enemigos de la función que calcula si hay una caída o no. Esto se debe a que, en un juego real, los enemigos probablemente harían algo más que esperar en la pantalla para ser asesinados.
Entonces ahora
tenemos una función SpawnEnemy y otra función Drop. Drop
es muy similar a la forma en que manejamos la "tirada de dados" de
nuestro elemento cae en el ejemplo anterior, pero toma varios parámetros
esta vez: dos de estas son las coordenadas X e Y del enemigo en la
pantalla, ya que ese es el lugar donde querrá engendrar el artículo
cuando haya una caída; los otros parámetros son EnemyType, por lo que
podemos buscar el
nombre del enemigo en la tabla aEnemy, y el nombre del personaje como
una cadena, para que sea más rápido escribir los comentarios que
queremos darle al jugador.
La lógica de la función Drop es similar a la
del ejemplo anterior; lo que más cambia es la forma en que mostramos los
comentarios. Esta vez, en lugar de simplemente mostrar texto, también
generamos un
objeto en la pantalla para darle una representación visual al
jugador.
(Nota:
para engendrar a los enemigos en varias posiciones en la pantalla, usé
un objeto invisible, Spawn, como referencia, que se mueve continuamente
hacia la izquierda y la derecha. Siempre que se llame a la función
SpawnEnemy, crea el enemigo en las coordenadas actuales del objeto Spawn, para que aparezcan los enemigos y una variedad de ubicaciones
horizontales.)
Una última cosa para discutir es cuándo se llama
exactamente la función Drop. No
lo disparo directamente después de la muerte de un enemigo, pero
después de que el enemigo se haya desvanecido (la animación de la muerte
del enemigo). Por supuesto, puede solicitar la caída cuando el enemigo
aún esté visible en la pantalla, si lo prefiere; una vez más, eso
depende del diseño de tu juego.



Conclusión
A nivel de diseño, tener enemigos que tiren un botín incentiva al
jugador para enfrentarlos y destruirlos. Los elementos eliminados te
permiten otorgar poderes, estadísticas o
incluso objetivos al jugador, ya sea de forma directa o indirecta.
En
un nivel de implementación, la eliminación de elementos se gestiona a
través de una función que el programador decide cuándo llamar. La
función hace el trabajo de verificar la rareza de los elementos que se
deben eliminar según el tipo de enemigo muerto, y también puede
determinar dónde generarlo en la pantalla si es necesario. Los datos
para los elementos y enemigos se pueden guardar en
estructuras de datos como matrices, y la función los puede consultar.
La función utiliza números aleatorios para determinar la frecuencia y el tipo de las caídas, y el codificador tiene control sobre esas tiradas aleatorias y los datos que busca, para adaptar la sensación de esas caídas en el juego.
Espero que hayas disfrutado este artículo y entiendas
mejor cómo hacer que tus monstruos caigan en el juego. Estoy deseando
ver tus propios juegos usando esa mecánica.
Referencias
- Crédito de la imagen: Gold Treasure Icons de Clint Bellanger.
- Crédito Sprite: Character sprites de Antifareas.
- Crédito de Sprite: Battle Backgrounds de Trent Gamblin.
- Crédito de Sprite: Pixel Art Icons for RPGs para juegos de rol de 7SoulDesign.



