Sviluppare un platform nel 2023
Premessa
Credo che i tutorial per lo sviluppo di giochi platform siano i più ricercati per ogni piattaforma di sviluppo, e GameMaker non fa eccezione.
Questo però genera anche un problema: essendocene molti, nessuno si prende più la briga di crearne di nuovi.
Però:
- i software sono progrediti nel tempo, regalandoci nuove funzionalità.
- gli utenti sono diventati più esigenti, visto che anche gli indie come Celeste hanno diverse accortezze lato codice per rendere i giochi più piacevoli.
Date queste premesse, partiamo quindi con la creazione di un platform "moderno".
Se preferite scaricare direttamente il codice sorgente del progetto trovate il link nella sezione Download di questo stesso articolo.
Caratteristiche
Il prodotto finale conterrà
- Accelerazione e decelerazione
- Collisioni con move_and_collide su superfici piane e non
- Coyote Time
- Jump Buffer
- Salti ad altezza variabile
- Animazioni / cambio sprite
I nomi degli oggetti e delle variabili sono compatibili con la configurazione di Feather di default.
Sviluppiamo il progetto
Creiamo un nuovo progetto e inseriamo due oggetti:
- obj_player
- obj_collision
Le basi: movimento e collisioni
Apriamo l'oggetto obj_player e inseriamo queste variabili nel Create Event
grv = 0.2; // Gravity
hsp = 0; // Horizontal Speed
vsp = 0; // Vertical Speed
dir = 1; //Facing direction (1 for right, -1 for left)
hsp_walk = 3; // Walking speed
acc = 0.5; // Horizontal acceleration
dec = 0.23; // Horizontal deceleration
vsp_jump = -4; // Jump speed
collision_objects = [obj_collision] // Array with "solid" objects
Tra nomi delle variabili e commenti dovrebbe essere tutto abbastanza lineare. Faccio solo notare che nelle ultime versioni di GameMaker, nelle funzioni per la gestione delle collisioni, si possono degli array di oggetti (o addirittura di tileset) invece di specificarne uno unico: per questo vedete la riga 9 fatta in questa maniera.
Passiamo ora alla logica. Apriamo lo Step Event:
// Get inputs
var _key_right = keyboard_check(vk_right);
var _key_left = keyboard_check(vk_left);
var _key_jump_pressed = keyboard_check_pressed(ord("Z"));
// Horizontal movement
var _move = _key_right - _key_left;
if (_move == 0) {
hsp = approach(hsp, 0, dec);
} else {
hsp = approach(hsp, _move * hsp_walk, acc);
}
if (hsp !=0) dir = sign(hsp)
Se sulle prime righe non credo ci sia niente di particolare da aggiungere, parliamo di quanto scritto tra le righe 9 e 13: qui viene gestita l'accelerazione e la decelerazione del nostro personaggio mentre ci muoviamo, evitando quel brusco passaggio tra l'immobilità e il movimento, e viceversa.
approach non è una funziona nativa di GameMaker, ma la si usa da tempo immemore: è molto simile al lerp nativo di GM, con la differenza che qui il terzo parametro è un incremento \ decremento statico, mentre nel lerp è percentuale.
E' un approccio anzi molto simile l'utilizzo di una funziona piuttosto che l'altra, ma, con la futura gestione delle animazioni che andremo ad implementare, mi trovo più comodo ad usare questo metodo.
A questo link, condiviso al tempo dal buon Shaun Spalding, potete trovare la funzione da salvare all'interno di un file Script all'interno di GameMaker.
Passiamo ora alla gestione del movimento verticale:
// Vertical movement
vsp += grv;
var _on_ground = place_meeting(x, y+1, collision_objects);
// Check if jump button is pressed
if (_key_jump_pressed && _on_ground) vsp = vsp_jump
Anche qui tutto abbastanza standard: diamo la possibilità al giocatore di saltare solo se siamo "a terra". Al momento va benissimo così, ma immaginerete anche voi che questo sarà uno dei punti da modificare quando dovremo gestire coyote time e jump buffer.
// Horizontal move & collide
var _horizontal_collisions = move_and_collide(hsp, 0, collision_objects, abs(hsp))
// Vertical move & collide
var _vertical_collisions = move_and_collide(0, vsp, collision_objects, abs(vsp)+1 , hsp, vsp, hsp, vsp)
if (array_length(_vertical_collisions) > 0) {
vsp = 0;
}
Quest'ultima parte dello Step Event è tutta dedicata alle collisioni, e sono tutte gestite tramite la funzione move_and_collide: questa funzione permette di effettuare un movimento all'interno della room finchè non trova una o più istanze che lo bloccano.
move_and_collide prevede 3 parametri obbligatori: movimento sull'asse X, movimento sull'asse y, oggetto (o array di oggetti, o tileset) che non permettono il movimento. La riga 26 da questo punto di vista rispetta quando appena descritto: l'oggetto si muoverà di hsp pixel orizzontalmente, di 0 px vericalmente, e collision_objects contiene l'array di oggetti con cui verificare le collisioni.
Ci sono 4 ulteriori parametri opzionali che può accettare questa funzione:
- num_iterations, che indica ogni quanti pixel verificare la collisione. (di default è 4). L'operazione effettuata è movimento (asse x o y) diviso num_iterations. Noi vogliamo che il check sia effettuato ad ogni pixel, per cui lo abbiamo valorizzato ad hsp così da avere hsp / hsp = 1 a riga 26, ad esempio.
- xoff, che indica la componente x della direzione nella quale muovere l'oggetto in caso di collisione (di default è perpendicolare alla direzione del movimento)
- yoff, che indica la componente x della direzione nella quale muovere l'oggetto in caso di collisione (di default è perpendicolare alla direzione del movimento)
- max_x_move, la massima velocità con la quale l'istanza dovrebbe muoversi sull'asse x (di default è senza limite)
- max_y_move, la massima velocità con la quale l'istanza dovrebbe muoversi sull'asse y (di default è senza limite)
La funzione move_and_collide ritorna l'array di oggetti con le quali c'è stata una collisione: per questo motivo alla riga 30 verifichiamo che ci sia almeno un riferimento ad un'istanza prima di rimuovere la velocità verticale.
Disegniamo ora le sprite e assegniamole ai vari oggetti: io ad esempio ho creato 3 "muri" (per averne uno orizzontale e due obliqui) e uno per il personaggio. Dobbiamo avere l'accortezza, però, di settare la collision mask a "Precise (slow)" per le sprite non squadrate:
Qui il risultato:
Abbiamo scritto, in pochissime righe di codice, il codice di un platform "base".
Coyote time
Il coyote time è la possibilità data al giocatore di effettuare un salto anche se si è già andati oltre la fine di una piattaforma: il nome deriva da una di quelle scene "classiche" di Will il Coyote
Può sembrare un'aggiunta superflua, ma sono caratteristiche come questa (insieme al jumb buffer) che rendono così piacevole il moveset di Celeste, ad esempio.
Sviluppo
Il concetto è molto semplice: inseriamo un contatore che abbia un valore positivo quando il personaggio è su una piattaforma, ma che diminuisce man mano che passa il tempo lontano dalla piattaforma. Se il contatore ha un valore positivo il personaggio può ancora saltare, altrimenti no. Ovviamente il tempo di cui parlo deve essere solo qualche frame per dare un aiuto al giocatore.
Apriamo l'evento Create di obj_player e inseriamo queste due nuove variabili:
coyote_time = 10;
on_ground = 0;
coyote_time contiene appunto il numero di step per i quali, lasciata la piattaforma, il personaggio potrà ancora saltare. E' un valore volutamente alto per mostrare il funzionamento all'interno del tutorial, scegliete voi quello che sta meglio all'interno del vostro progetto.
Apriamo ora lo step event e modifichiamo la logica con cui usavamo la variabile _on_ground
if (_move == 0) {
hsp = approach(hsp, 0, dec);
} else {
hsp = approach(hsp, _move * hsp_walk, acc);
}
if (hsp!=0) dir = sign(hsp)
// Vertical movement
vsp += grv;
// Check if we ever touch the ground in the last *coyote_time* steps
if place_meeting(x, y+1, collision_objects) on_ground = coyote_time else on_ground--;
// Check if jump button is pressed
if (_key_jump_pressed && on_ground > 0) vsp = vsp_jump
// Horizontal move & collide
var _horizontal_collisions = move_and_collide(hsp, 0, collision_objects, abs(hsp))
// Vertical move & collide
var _vertical_collisions = move_and_collide(0, vsp, collision_objects, abs(vsp)+1, hsp, vsp, hsp, vsp)
if (array_length(vertical_collisions) > 0) {
vsp = 0;
}
Anzichè salvare la variabile _on_ground a true o false in base al fatto che il nostro personaggio fosse su una piattaforma o meno, la valorizziamo con il quantitativo di step salvato in coyote_time. Così facendo "faremo credere" di essere ancora sulla piattaforma al resto del codice (vedi riga 25).
Jump buffer
Il jump buffer è quella tecnica che permette al giocatore di saltare da una piattaforma in cui siamo appena atterrati, anche se abbiamo mancato il tempismo e abbiamo premuto il tasto salto appena prima di toccare terra.
Sviluppo
Anche in questo caso la tecnica prevede un contatore che verificherà quanti step prima di toccare terra abbiamo premuto il tasto per saltare e, nel caso, farci saltare oppure no.
Apriamo l'evento Create di obj_player e inseriamo queste due nuove variabili:
// Jump buffer
can_jump = 0;
jump_buffer = 10;
Apriamo lo Step Event e cambiamo la gestione del salto (riga 24) così:
// Check if jump button is pressed
if _key_jump_pressed can_jump = jump_buffer else can_jump--;
// Check if we can jump
if (can_jump > 0) && (on_ground > 0) {
vsp = vsp_jump;
can_jump = 0;
on_ground = 0;
}
Alla pressione del tasto salto quindi non effettueremo più l'azione, ma "caricheremo" la variabile can_jump: se questa sarà positiva e saremo sul sul terreno (quindi on_ground sarà maggiore di zero, per il discorso fatto sul coyote time), allora effettueremo un salto.
Ovviamente qui è difficile dimostrare il codice con una gif animata: provate a valorizzare jump_buffer con un valore molto alto e premete il tasto salto prima di toccare terra per riuscire ad apprezzarne il funzionamento.
Salti ad altezza variabile
L'altezza del salto in un videogioco spesso è determinata da quanto si tiene premuto il pulsante adibito a questa azione. La realizzazione di qualcosa del genere è molto semplice.
Sviluppo
Nello Step Event di obj_player inseriamo un check per verificare che il pulsante di salto sia costantemente tenuto premuto:
// Get inputs
var _key_right = keyboard_check(vk_right);
var _key_left = keyboard_check(vk_left);
var _key_jump = keyboard_check(ord("Z"));
var _key_jump_pressed = keyboard_check_pressed(ord("Z"));
La logica dietro sarà questa: se rilascio il pulsante di salto durante l'azione, ridurrò (nel nostro caso dimezzandola) la vsp.
// Variable jump height
if (!_key_jump and vsp < 0) vsp *= 0.5;
Anche qui il risultato sarà più apprezzabile provandolo direttamente, ma vi lascio l'anteprima del risultato:
Animazioni / Cambio sprite
Inizio questo paragrafo ringraziando di cuore l'utente Victroium che ha realizzato uno splendido asset pack per la realizzazione di questo tutorial.
Quello che ci servirà per questa parte di tutorial saranno i seguenti sprites:
- spr_player_idle (il personaggio fermo)
- spr_player_walk (personaggio che cammina)
- spr_player_jump (personaggio che salta)
- spr_player_falling (personaggio che torna a terra dopo un salto)
Sviluppo
Apriamo Draw Event di obj_player:
// Movement on ground
if (hsp != 0 && vsp==0 && on_ground>0) sprite_index = spr_player_walk else sprite_index = spr_player_idle
// Movement on air
if (on_ground<coyote_time) {
if (vsp < 0 ) sprite_index = spr_player_jump
else sprite_index = spr_player_falling
}
// Draw sprite
draw_sprite_ext(sprite_index, image_index, x, y, dir, image_yscale, image_angle, image_blend, image_alpha)
Partiamo dai movimenti a terra (riga 2). Se siamo a terra (on_ground > 0), non stiamo saltando (vsp == 0) e siamo in movimento (hsp!=0), allora usiamo lo sprite di camminata, altrimenti usiamo quello di idle.
Il movimento in aria è altrettanto semplice se abbiamo capito il coyote time trattato qualche paragrafo sopra: se non sono a terra (quindi on_ground è minore di coyote_time) e la mia velocità è negativa, allora mi sto muovendo verso l'alto, altrimenti verso il basso.
La riga 11 non è niente di speciale: apprezziamo solo l'utilizzo della variabile dir come valore di image_xscale per permettere al nostro personaggio di "specchiare" lo sprite orizzontalmente quando ci stiamo spostando a destra o sinistra. Ricordo che questa "tecnica" è utilizzabile solo se la origin dello sprite è centrata orizzontalmente.
Con questa implementazione tutto funziona correttamente, se non per un piccolo punto: se abbiamo usato uno sprite con più frame per l'animazione del personaggio in salto o in caduta, questa continuerà a resettarsi fino al termine del salto. Per risolvere il problema ci viene in soccorso l'evento Animation End.
// Maintain the last frame during jump animations
if (sprite_index = spr_player_jump) || (sprite_index = spr_player_falling) {
image_index = sprite_get_number(sprite_index) - 1;
}
Questo evento viene letto quando l'animazione del nostro sprite_index termina. Anche qui la logica è molto semplice, e diciamo a GM di mantenere l'ultimo frame dello sprite che abbiamo in uso.
Se vi state chiedendo perchè ci sia quel "-1", il motivo è semplice: image_index parte a contare i frame da 0.
Ecco qui il risultato finale.
Download
Potete scaricare il codice sorgente di questo progetto da qui: Github
Conclusione
Spero che il tutorial sia risultato chiaro: nessuna di queste tecniche è davvero necessaria per la realizzazione di un platform, ma come avete potuto apprezzare, con poco codice siamo riusciti a creare una base "robusta" e più aggiornata della maggior parte dei tutorial che si trovano in rete.
Per qualsiasi domanda non fatevi problemi a venirci a trovare su Discord .
Alla prossima!