Ciao, oggi mi diverto un po' provando questa simpatica libreria per lo sviluppo di piccoli videogames. Kaplay è molto interessante, promette uno sviluppo semplice, divertente è dichiarativo. Sul sito ufficiale possiamo trovare la documentazione, una collezione di assets ed un playground per entrare subito nel vivo scrivendo codice live.
Ok, L'idea è quella di seguire il tutorial ufficiale nel creare un clone del famoso giochino Dino di Chrome, Perderò un bel po' di tempo in più per creare degli sprite da zero con Piskel. e cercherò di integrare, al tutorial, la logica per lo spawn degli ostacoli da saltare.
Mi piaceva l'idea di creare un nerd che salta i pc guasti e evita dei floppy "shuriken" volanti anzichè un fagiolo che salta dei muretti proposto nel tutorial ufficiale.
Installazione
Installiamo il pacchetto ufficiale ed avviamo l'ambiente di sviluppo.
npx create-kaplay myGame
cd myGame && npm run dev
Ok, adesso nel file main.js possiamo scrivere il nostro codice "divertente". Kaplay ci ricorda che, se volessimo usare una scrittura globale basta decommentare import "kaplay/global";
Iniziamo
kaplay() è il metodo padre per inizializzare il tutto. Possiamo specificare anche parametri di configurazione:
kaplay({
width: 500,
height: 400,
background: "#a5b8c7",
canvas: document.getElementById("canvas"),
letterbox: false,
debugKey: "w",
});
Importante il parametro debugKey che ci permette di scegliere quale pulsante premete per attivare un'utilissima funzione di debug.
Questa funzione mette in evidenza l'area degli sprite per debuggare le collisioni e controllare altri parametri per un maxi comfort di sviluppo.
Gli Sprite
Gli sprite sono gli elementi visivi e accattivanti del nostro giochino, possiamo caricarli con loadSprite().
Questa funzione accetta due parametri: nome e percorso.
Se i nostri sprite contengono animazioni possiamo specificare il terzo parametro per mappare i frame e definire le opzioni di animazione:
loadSprite("player", "sprites/player.png", {
//sliceX: 0,
sliceY: 4,
anims: {
run: { from: 0, to: 3, loop: true },
jump: { from: 0, to: 0, loop: false },
},
});
In questo caso ho un foglio verticale di 4 frame. Dichiaro l'animazione di corsa definendo l'oggetto 'run' specificando che, i frame per comporla, partono dal fotogramma 0 fino ad arrivare al 3. loop:true renderà l'animazione ciclica.
Per il salto sono pigro quindi uso solo il primo. Facile no?
"Appiccichiamo" lo sprite al nostro quadro di gioco con:
const player = add([sprite("player"), scale(3), pos(80, 40)]);
il metodo scale() ci permette di ridimenzionare gli sprite senza doverli ridisegnare.
Richiamiamo le animazioni settate in questo modo:
player.play("run");
Ehi, Mi sto divertendo 🙂
Gravità
Ok, il nostro player adesso apparirà, sullo stage appeso in alto a sinistra, così senza senso…a noi serve poggiarlo su una piattaforma dove poter correre e saltare. Ci serve della "solida" gravità.
Settiamola subito dopo kaplay()
setGravity(1600);
Dobbiamo ora aggiunguere due metodi fondamentali per rendere il nostro player "consistente".
Il primo è area(): questo parametro dice a Kaplay che il nostro sprite occupa uno spazio, un area specifica ed è soggetto a collisioni.
Questi "confini di collisione" possono essere visti attivando la modalità di debug con il tasto che abbiamo scelto in debugKey.
Il secondo metodo è body() che assegna "peso" e sottopone quello specifico sprite alle leggi di gravità, facendolo cadere inesorabilmente verso il basso, se non c'è un altro elemento con area() e body() sotto i suoi piedi.
//player
const player = add([
pos(30, 220),
scale(3),
sprite("player"),
area(),
body(),
]);
//piattaforma
add([
rect(width(), 48),
pos(0, height() - 48),
outline(4),
area(),
body({ isStatic: true }),
color(61, 83, 107),
]);
Per la piattaforma non usiamo sprite, la disegniamo direttamente tramite rect() prendendo tutta la lunghezza del nostro stage ( width() ) e come altezza un bel 48px fissi.
Importante è isStatic: true che riconosce la "solidità" dell'oggetto ma non lo fa cadere nel vuoto.
Il metodo body() assegnato al nostro player ci "sblocchera" la possibilità di compiere dei salti grazie a jump() e identificare se il protagonista "tocca terra" grazie a isGrounded().
Quest'ultimo metodo, già scritto per noi, è utile proprio per far partire il salto solo se il nostro amico è con i piedi ben saldi sulla piattaforma.
Gli ostacoli
Ok, pensiamo agli ostacoli che dovranno apparire per rendere il gioco non prevedibile e "avvincente".
Dobbiamo realizzare una logica di spawn randomica e bilanciata…variando anche gli sprite.
const enemy = ["pc", "server", "server", "floppy"];
const randomSprite = choose(enemy);
const enemyes = add([
sprite(randomSprite),
scale(randomSprite == "floppy" ? 1.2 : 3),
area(),
pos(width(), height() - (randomSprite == "floppy" ? 180 : FLOOR_HEIGHT)),
anchor("botleft"),
move(LEFT, enemySpeed), //480
offscreen({ destroy: true }),
"enemy",
]);
enemyes.play("danger");
Questa è la nostra base:
Creo una costante con i nomi degli sprite dei nemici e farò scegliere a Kaplay quale usare grazie a choose().
Nel metodo add() gestiamo la posizione e la scala in maniera condizionale. Gestiamo allo stesso modo la posizione visto che, 'floppy' è l'unico ostacolo volante.
Il metodo move() ci permetterà di controllare la direzione del movimento (LEFT) e la velocità. Settiamo offscreen({destroy true}) per cancellare lo sprite quando uscirà dallo schermo per migliorare le performance del gioco.
Per gestire le collisioni impostiamo il metodo area() e assegniamo un tag "enemey" in modo tale da specificare successivamente che: se il nostro player si scontra con un altro sprite che ha il tag "enemy" il gioco ci porta in game over.
Spawn dei nemici
Analizzata questa parte ora ci serve capire come randomizzare il tutto in maniera comoda e veloce. Gli ostacoli da saltare dovranno essere randomici e partire da una velocità base, che andrà ad incrementarsi ogni tot di tempo, e tra un nemico e l'altro dovrà esserci un intervallo di tempo per la loro creazione, in rapporto alla velocità di gioco corrente.
Kapaly offre due soluzioni interessanti i loop e le funzioni wait ricorsive.
Loop
//loop
loop(5, () => {
//aumenta la velocità ogni 5 sec
});
Questo è un loop, il primo parametro sono i secondi che servono per ripetersi. Qui possiamo incrementare la nostra variabile enemySpeed ed incrementarla di 30 ogni 5 secondi.
let enemySpeed = 480;
loop(5, () => {
enemySpeed += 30;
});
Wait
wait(rand(min, max), () => {
//richiama una funzione
});
Per far spawnare i nostri ostacoli con un intervallo di tempo specifico dobbiamo usare wait().
Lo scopo è invocare la funzione che crea l'ostacolo in maniera ricorsiva in un intervallo di tempo dinamico.
Perché dinamico? Semplice: l'intervallo dovrà accorciarsi man mano che la velocità aumenta, altrimenti il gioco diventa troppo facile!
Ecco come possiamo gestire il tutto, subito dopo il loop:
spawnEnemy();
function spawnEnemy() {
const enemy = ["pc", "server", "server", "floppy"];
const randomSprite = choose(enemy);
const enemyes = add([
sprite(randomSprite),
scale(randomSprite == "floppy" ? 1.2 : 3),
area(),
pos(width(), height() - (randomSprite == "floppy" ? 180 : FLOOR_HEIGHT)),
anchor("botleft"),
move(LEFT, enemySpeed), //480
offscreen({ destroy: true }),
"enemy",
]);
enemyes.play("danger");
//calcolo velocità nemici
// Più veloci sono, meno aspettiamo.
// Sottraiamo un valore che cresce con la velocità
let min = 1.2 - (enemySpeed - DEF_SPEED) / 1000;
let max = 1.9 - (enemySpeed - DEF_SPEED) / 1000;
// Applichiamo i limiti per non scendere troppo
if (min < 0.7) min = 0.7;
if (max < 1.1) max = 1.1;
wait(rand(min, max), () => {
spawnEnemy();
});
}
Ok analizziamo questa funzione:
Nella prima parte gestiamo la generazione randomica dello sprite come visto in precedenza. La parte interessante viene subito dopo.
Con le variabili min e max cerchiamo di gestire il rapporto tra tempo e velocità sottraendo il tempo iniziale di 1.2 e 1.9 secondi alla velocità base meno la velocità corrente degli ostacoli che inizialmente sarà 0.
Mettiamo due condizioni per non scendere troppo i 0.7 e 1.1 secondi altrimenti l'intervallo con cui wait chiamerà spawnEnemy() sarà troppo corto e gli ostacoli saranno impossibili da saltare.
Solo alla fine della nostra bella funzione usiamo il metodo wait() per ripetere l'operazione in maniera ricorrente con gli intervalli calcolati.
Salto e Collisioni
Gestire le collisioni è molto semplice, basta applicare il metodo onCollide() su player e specificare come parametro il tag scelto per gli altri sprite che impatteranno sul nostro povero protagonista.
addKaboom() farà apparire uno sprite predefinito con la scritta "Kaboom", shake() crea un effetto vibrazione per sottolineare l'impatto.
player.onCollide("enemy", () => {
addKaboom(player.pos);
shake();
go("gameover", score);
});
Per quanto riguarda il salto del nostro eroe nerd possiamo usare un quasiasi tasto con onKeyPress() o un classico onClick(). Useremo quest'ultimo perchè polivalente sia per il "click" del mouse e per il "tocco" su dispositivi mobili. Nulla ci vieta definirli entrambi.
onClick(() => {
if (player.isGrounded()) {
player.jump();
player.play("jump");
}
});
Da notare la condizione if (player.isGrounded()) che ci permette di far partire l'azione di salto solo se il nostro personaggio non è a mezz'aria.
Score
Non sarebbe un vero gioco senza. In questo caso lo score è relativo al tempo trascorso in gioco prima di schiantarci contro un server o un pc guasto.
Utilizziamo l'evento onUpdate() che resta in ascolto su ogni fotogramma così da incrementare il nostro punteggio.
let score = 0;
const scoreLabel = add([text(score), pos(24, 24)]);
onUpdate(() => {
score++;
scoreLabel.text = score;
});
Scene
Per un corretto "ciclo di gioco" Kaplay mette a disposizione delle scene. Qui possiamo anche specificare ulteriori comandi.
scene("intro", () => {
add([text(`Let's Go`), pos(width() / 2, 180), anchor("center")]);
add([text(`Click to start` , {size:'24'}), pos(width() / 2, 240), anchor("center")]);
onKeyPress("s", () => {
go("game");
});
onClick(() => {
go("game");
});
});
scene("game", () => {
//la logica del gioco qui
});
scene("gameover", (score) => {
add([text(`Game Over`), pos(width() / 2, 140), anchor("center")]);
add([
text(score,{size:'32'}),
pos(width() / 2, 200),
anchor("center"),
]);
add([text(`Click to restart`,{size:'24'}), pos(width() / 2, 300), anchor("center")]);
onKeyPress("s", () => {
go("game");
});
onClick(() => {
go("game");
});
});
Come puoi vedere ho diviso il gioco in tre scenette. intro dove è possibile iniziare a giocare cliccando sullo schermo o premere il tasto S .game conterrà tutta la logica del gioco, mentre gameover riceverà come parametro score così da poterlo stampare anche in questa schermata.
Ora che abbiamo le scene diciamo a Kaplay di iniziare da intro con go("intro"); . Utilizzando questo comando alla fine del nostro file main.js e abbiamo finito.
Qui sotto puoi trovare il risultato. l'invito è andare a leggere la documentazione ufficiale e sperimentare perchè possiamo tirar fuori tanti giochini divertenti. A presto!

