Advertisement
  1. Game Development
  2. Game Mechanics

Come programmare i Bottini (Loot) dai Mostri

Scroll to top
Read Time: 16 min

() translation by (you can also view the original English article)

Una comune meccanica nei giochi d'azione è quella, per i nemici, di lasciar cadere qualche tipo di oggetto o ricompensa al momento di morire. Il personaggio può quindi raccogliere questo bottino per ottenere qualche vantaggio. Si tratta di una meccanica che ci si aspetta in un sacco di giochi, come negli RPG, dal momento che dà al giocatore un incentivo per sbarazzarsi dei nemici con una piccola esplosione di endorfine quando scoprono immediatamente quale è la ricompensa per la loro azione.

In questo tutorial, esamineremo il funzionamento interno di tale meccanica e vedremo come implementarlo, qualunque sia il tipo di gioco e tool/linguaggio utilizzato.

Gli esempi sono stati realizzati usando Construct 2, uno strumento per la creazione di giochi HTML5, ma non sono in alcun modo specifici. Dovreste essere in grado di implementare la stessa meccanica in qualunque altro linguaggio o tool.

Gli esempi sono stati fatti con la r167.2 e possono essere aperti e modificati con la versione gratuita del software. È possibile scaricare l'ultima versione di Construct 2 qui (da quando ho iniziato a scrivere questo articolo, sono state rilasciate almeno due versioni più recenti) e pasticciarci con gli esempi a proprio piacimento. I sorgenti dell'esempio CAPX sono allegati alla presente esercitazione nel file zip.

La Meccanica di Base

Alla morte di un nemico (quindi, quando i suoi HP sono minori o uguali a zero) viene chiamata una funzione. Il ruolo di questa funzione è di determinare se vi sia o meno un rilascio (n.d.a. d'ora in poi lo chiamerò "drop") di oggetti, e, in caso affermativo, il tipo di rilascio che ci dovrebbe essere.

La funzione può anche gestire la creazione della rappresentazione visiva del drop, facendolo apparire alle ex coordinate a schermo del nemico.

Considerate l'esempio seguente:

Cliccate sul pulsante Slay 100 Beasts. Questo avvierà un processo batch che crea 100 bestie casuali, le uccide e visualizza il risultato per ogni bestia (vale a dire, se la bestia droppa un oggetto, e, in caso affermativo, che tipo di oggetto). Le statistiche nella parte inferiore dello schermo mostrano quante bestie droppano oggetti e il numero per ciascun tipo oggetto che è stata droppato.

Questo esempio è strettamente testuale per mostrare la logica che sta dietro la funzione e per dimostrare che questa meccanica può essere applicato a qualsiasi tipo di gioco, che si tratti di un platform dove si schiacciano i nemici (stomp il salto compiuto da Mario nell'uccidere i funghi), o uno shooter top-down o un gioco di ruolo.

Diamo un'occhiata a come funziona questa demo. In primo luogo, i mostri e i drop sono tutti contenuti in un array. Questo è l'array beast:

Index (X)
Name (Y-0)
Drop rate (Y-1)
Item rarity (Y-2)
0 Boar 100 100
1 Goblin 75 75
2 Squire 65 55
3 ZogZog 45 100
4 Owl 15 15
5 Mastodon 35 50

Ed ecco l'array dei drops:

Index (X)
Name (Y-0)
Item rarity (Y-1)
0 Lollipop 75
1 Gold 50
2 Rocks 95
3 Jewel 25
4 Incense 35
5 Equipment 15

Il valore X (la colonna Index) per la matrice agisce come un identificatore univoco per il mostro o il tipo di oggetto. Ad esempio, il mostro di indice 0 è un Boar (Cinghiale). L'oggetto di indice 3 è un Jewel (gioiello).

Questi array agiscono come tabelle di ricerca, contiengono il nome o il tipo di ogni mostro o oggetto e con gli altri valori ci permetteranno di determinare la rarità o il tasso di drop. Nella matrice dei mostri, ci sono due colonne dopo il nome:

Drop rate indica la probabilità del mostro di droppare un elemento quando ucciso. Ad esempio, il cinghiale (boar) avrà una probabilità del 100% di droppare un oggetto quando viene ucciso, mentre il gufo avrà una probabilità del 15% di fare lo stesso.

Rarity definisce quanto rari sono gli elementi che possono essere droppati da questo mostro. Ad esempio, un cinghiale probabilmente dropperà oggetti di un valore 100 di rarità. Ora, se controlliamo l'array drops, possiamo vedere che le rocce sono l'elemento con la più alta rarità (95). (Nonostante il valore di rarità sia alto, per come ho programmato la funzione, il più alto valore di rarità è, il l'oggetto più comune. Ha più possibilità di droppare rocce rispetto ad un elemento con un valore di rarità più basso.)

E questo è interessante per noi da un punto di vista di game design. Per l'equilibrio del gioco, non vogliamo che il giocatore acceda troppo preso a molto materiale o a troppi oggetti di alto livello, altrimenti, il personaggio potrebbe potenziarsi troppo presto e il gioco perdere di interesse nelle partite.

Queste tabelle e i valori sono solo esempi, e ci potete e dovete giocare per adattarli al vostro sistema di gioco e al vostro universo. Tutto dipende dal bilanciamento del sistema. Se volete saperne di più in materia di bilanciamento, vi consiglio di leggere questa serie di tutorial: Balancing Turn-Based RPGs.

Vediamo ora lo (pseudo)codice per la demo:

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
    

In primo luogo, l'azione dell'utente: il click sul pulsante Slay 100 Beasts. Questo tasto richiama una funzione con un parametro 100, ma giusto perché 100 è un bel numero di nemici da uccidere. Naturalmente in un vero e proprio gioco, è più probabile che si uccidano bestie una alla volta.

Da qui, la funzione SlainBeast viene chiamata. Il suo scopo è quello di visualizzare un testo per dare un ritorno agli utenti su ciò che è accaduto. In primo luogo, si pulisce la variabile BeastDrops e l'array aStats, che vengono utilizzati per le statistiche.  In un vero e proprio gioco, è improbabile che ne abbiate bisogno. Pulisce anche il Text facendo in modo che le 100 linee nuove vengano visualizzate per vedere i risultati di questo lotto. Nella funzione stessa, vengono create tre variabili numeriche: BeastType, DropChance, e Rarity.

BeastType sarà l'indice che utilizziamo per far riferimento ad una riga specifica nella matrice aBeast; è principalmente il tipo di mostro che il giocatore ha affrontato e ucciso. Rarity è preso dalla matrice aBeast; è la rarità dell'oggetto che dovrebbe essere lasciato da quel mostro, il valore del campo Item rarity dell'array aBeast.

Infine, DropChance è un numero che prendiamo a caso tra 0 e 100. (La maggior parte dei linguaggi di programmazione avranno una funzione per ottenere un numero casuale da una gamma di valori o almeno per ottenere un numero casuale tra 0 e 1, che poi si potrebbe semplicemente moltiplicare per 100.)

A questo punto, possiamo visualizzare il nostro primo pezzo di informazioni nell'oggetto Text: sappiamo già che tipo di mostro è stato generato e che è stato ucciso. Così, concateniamo al valore corrente del Text.text la BEAST_NAME del BeastType corrente che abbiamo scelto a caso dalla matrice aBeast.

Ora dobbiamo determinare se un oggetto deve essere considerato droppato. Lo facciamo confrontando il valore DropChance al valore BEAST_DROPRATE dalla matrice aBeast. Se DropChance è minore o uguale a questo valore, si droppa un oggetto.

(Ho deciso di usare l'approccio per "minore o uguale a", essendo stato influenzato dai questi giochi di ruolo dal vivo utilizzando il D&D King Arthu: Pendragon set of rules, inerenti al lancio di dadi, ma si potrebbe benissimo codificare la funzione in altro modo, decidendo che il drop si verifica solo quando è "maggiore o uguale". E' solo una questione di valori numerici e di logica. Tuttavia, rimanete coerenti al vostro algoritmo, e non cambiate la logica a metà strada, altrimenti, potreste finire con l'avere problemi durante il debug o la manutenzione.)

Quindi, sono due linee che determinano se un oggetto è droppato o no. Prima:

1
DropChance > aBeast(BeastType,BEAST_DROPRATE)

Con DropChance maggiore delDropRate consideriamo che nessun elemento viene droppato. Da lì, l'unica cosa visualizzata è  "." (punto) di chiusura che termina la frase, "[BeastType] è stato ucciso." prima di passare al successivo nemico nel nostro batch.

D'altra parte:

1
DropChance <= aBeast(BeastType,BEAST_DROPRATE)

Con, DropChance minore o uguale al DropRate per la BeastType corrente, lo consideriamo come oggetto droppato. A tale scopo, verrà eseguito un confronto tra il Rarity dell'oggetto a cui è stato permesso di essere droppato dal BeastType corrente e i vari valori di rarità impostati nella tabella aDrop.

Scorriamo la tabella aDrop, controllando ogni indice per vedere se la sua DROP_RATE è maggiore o uguale a Rarity. (Ricordate intuitivamente il conteggio, più alto è il valore di Rarity, più è comune l'oggetto) ogni indice che corrisponde al confronto lo spingiamo in un array temporaneo, aTemp.

Alla fine del ciclo, dovremmo avere almeno un indice nella matrice aTemp. (In caso contrario, dobbiamo ridisegnare le nostre tabelle aDrop e aBeast!). Quindi creiamo nuova variabile numerica DropType che preleva a caso uno degli indici dell'array aTemp ; questo sarà l'oggetto che droppiamo.

Aggiungiamo il nome dell'oggetto al nostro testo, rendendo la condanna simile a "BeastType è stato ucciso, lasciando cadere un DROP_NAME.". Quindi, per il bene di questo esempio, aggiungiamo alcuni numeri per le nostre statistiche (nella matrice aStats e in BeastDrops).

Infine, dopo 100 ripetizioni, mostriamo le statistiche, il numero di mostri (su 100) che hanno droppato oggetti e il numero di oggetti che sono stati droppati.

Un altro esempio: Dropping visivo di oggetti

Consideriamo un altro esempio:

Premi lo Spazio creare una palla di fuoco che ucciderà il nemico.

Come si può vedere, si crea un nemico casuale (da un bestiario di 11). Il personaggio (a sinistra) può creare un attacco proiettile. Quando il proiettile colpisce il nemico, il nemico muore.

Da lì, una funzione simile a quella che abbiamo visto nel precedente esempio determina se il nemico droppa qualche oggetto o no e ti che tipo è. Questa volta, crea anche la rappresentazione visiva dell'oggetto droppato e aggiorna le statistiche nella parte inferiore dello schermo.

Ecco un'implementazione in pseudocodice:

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"

Date un'occhiata al contenuto delle tabelle aEnemy e aDrop, rispettivamente:

Index (X)
Name (Y-0)
Drop rate (Y-1)
Item rarity (Y-2)
Animation name (Y-3)
0 Healer Female 100 100 Healer_F
1 Healer Male 75 75 Healer_M
2 Mage Female 65 55 Mage_F
3 Mage Male 45 100 Mage_M
4 Ninja Female 15 15 Ninja_F
5 Ninja Male 35 50 Ninja_M
6 Ranger Male 75 80 Ranger_M
7 Townfolk Female 75 15 Townfolk_F
8 Townfolk Male 95 95 Townfolk_M
9 Warrior Female 70 70 Warrior_F
10 Warrior Male 45 55 Warrior_M
Index (X)
Name (Y-0)
Item rarity (Y-1)
0 Apple 75
1 Banana 50
2 Carrot 95
3 Grape 85
4 Empty potion 80
5 Blue potion 75
6 Red potion 70
7 Green potion 60
8 Pink Heart 65
9 Blue pearl 15
10 Rock 100
11 Glove 25
12 Armor 30
13 Jewel 35
14 Mage Hat 65
15 Wood shield 85
16 Iron axe 65

A differenza del precedente esempio, la matrice che contiene i dati del nemico è denominata aEnemy e contiene ancora una riga di dati, ENEMY_ANIM, che ha il nome dell'animazione del nemico. In questo modo, quando appare il nemico, possiamo vederlo e automatizzare la sua visualizzazione grafica.

Allo stesso modo, aDrop ora contiene 16 elementi, invece di sei, e ogni indice si riferisce al frame di animazione dell'oggetto, ma potrei avere diverse animazione così come per i nemici anche per gli oggetti droppati se dovessero essere animati.

Questa volta, ci sono molti più nemici e oggetti che nell'esempio precedente. Potete notare che i dati dei tassi del drop e dei valori di rarità sono ancora lì. Una differenza notevole è che abbiamo separato la riproduzione dei nemici dalla funzione che calcola se vi è un drop o meno. Questo perché, in un vero e proprio gioco, i nemici farebbero probabilmente qualcosa in più che aspettare sullo schermo per essere uccisi!

Così ora abbiamo una funzione SpawnEnemy ed un'altra funzione Drop. Drop è piuttosto simile a come abbiamo gestito i "dadi" dei nostri oggetti da droppare nell'esempio precedente, ma richiede molti più parametri stavolta: due di questi sono del coordinate X e Y del nemico sullo schermo, dato che è il luogo dove si vorrà generare l'oggetto quando c'è un drop; gli altri parametri sono EnemyType, in modo che possiamo cercare il nome del nemico nella tabella aEnemy, e il nome del personaggio come  stringa, per scriver più facilmente il feedback che vogliamo dare al giocatore.

La logica della funzione di Drop è altrimenti simile all'esempio precedente; ciò che cambia è il modo in cui visualizziamo il feedback. Questa volta, invece di visualizzare del testo, abbiamo anche piazzato un oggetto sullo schermo per dare una rappresentazione visiva al giocatore.

(Nota: per piazzare i nemici in diverse posizioni sullo schermo, ho usato come riferimento un oggetto invisibile, Spawn, che si muove continuamente a destra e a sinistra. Ogni volta che la funzione SpawnEnemy viene chiamata, crea il nemico alle coordinate dell'oggetto Spawn, in modo che i nemici appaiono ad una varietà di posizioni orizzontali.)

Un'ultima cosa da discutere è quando viene esattamente chiamata la funzione di Drop. Non la attiviamo direttamente alla morte di un nemico, ma dopo che lo stesso è svanito (l'animazione della morte del nemico). Naturalmente, se preferite, potete chiamarla la drop quando il nemico è ancora visibile sullo schermo; ancora una volta, questo dipende dal vostro game design.

Conclusioni

A livello di design, i nemici che morendo lasciano del bottino dà un incentivo al giocatore per affrontarli e distruggerli. Gli oggetti droppati permettono di avere power-up, statistiche migliori o anche oggetti obiettivo per il giocatore, sia direttamente che indirettamente.

A livello di realizzazione, il drop degli oggetti è gestito attraverso una funzione che il programmatore decide quando richiamare. La funzione fa il lavoro di controllo della rarità degli elementi che dovrebbero essere droppati in base al tipo di nemico ucciso, e può anche determinare dove il drop apparirà sullo schermo se e quando necessario. I dati per gli oggetti ed i nemici possono essere tenuti in strutture dati come array, e ricercati dalla funzione.

La funzione utilizza numeri casuali per determinare la frequenza e il tipo di drop mentre il programmatore controllerà la generazione casuale e la ricerca dei dati adattando i drop in base alla tipo di esperienza nel gioco da parte del giocatore.

Spero che questo articolo vi sia piaciuto e che vi abbia fornito a comprendere come creare i drop per i loot (bottini) nel vostro gioco. Non vedo l'ora di vedere i vostri giochi utilizzare questa meccanica.

Riferimenti

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
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.