Una volta compresa la struttura e le funzionalità di base del game engine possiamo concentrarci sulla sua implementazione e sulla costruzione delle diverse parti che compongono il gioco. Iniziamo col creare un menu principale utilizzando risorse grafiche (sprite) e testi.
Al fine di rendere tutto più semplice, ho sviluppato un platformer in stile Megaman, che fin da adesso prenderemo come riferimento. È possibile provare il gioco, scaricare lo zip in allegato e sentirsi liberi di forkare il repository git contenente il codice sorgente e le risorse grafiche su github.
Considerando come risultato finale questo screenshot:
Il nostro menu, sarà composto da:
- Elementi testuali cliccabili
- Uno sfondo
- Un logo col titolo del gioco + immagine laterale del personaggio
- Eventualmente un footer coi crediti
Il font per i testi
Iniziamo con gli elementi testuali e inseriamo nel nostro index.html
un CSS per ottenere un font personalizzato:
<style>
@font-face
{
font-family: 'PixelFont';
src: url('font/pixelfont.eot') format('embedded-opentype'),
url('font/pixelfont.woff') format('woff'),
url('font/pixelfont.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
</style>
Quindi creiamo una cartella font nel progetto, in cui inseriamo i file del font in tre formati (pixelfont.eot
, pixelfont.ttf
, pixelfont.woff
), per avere compatibilità completa con tutti i browser.
Esistono svariati servizi online che convertono i normali fonts in webfont, ad esempio FontSquirrel. In alternativa potete utilizzare dei webfonts già pronti da Google Fonts
Caricare gli asset grafici
Proseguiamo, caricando le risorse grafiche che ci servono: all'interno della funzione Game
in main.js
inseriamo il seguente codice
rh = new ResourcesHandler( function() {
game.LoadLevel(0);
game.GameLoop();
});
// Ricordiamo la sintassi di LoadSprite:
// ResourceHandler.LoadSprite(src, subimages, callback);
this.sprLogo = rh.LoadSprite("img/logo.png",1);
this.sprSplashLogo = rh.LoadSprite("img/splashLogo.png",1);
//cursore del mouse
this.sprCursor = rh.LoadSprite("img/cursor.png",1);
this.backgroundMenu = rh.LoadSprite("img/backgroundmenu.png", 1, function() {
game.patternMenu = game.ctx.createPattern(game.backgroundMenu,"repeat");
});
Creiamo un istanza di ResourceHandler
e inseriamo come callback una funzione che definiremo successivamente, con il compito di caricare il menu (Livello 0) e avviare la funzione GameLoop
. Carichiamo quindi 2 sprites: il logo e l'immagine affiancata.
Per lo sfondo useremo un pattern (ovvero un immagine ripetuta), ma è necessario che l'immagine sia già caricata, quindi inseriamo in LoadSprite una callback con la funzione di creare un nuovo pattern.
Caricare la musica
Carichiamo anche la musica che sarà riprodotta in loop:
// Suoni
this.sndMusic = rh.LoadSound("audio/datagrove",["ogg", "mp3"]);
Aggiungiamo funzioni al game engine
Nonostante siamo già in piena fase di creazione del gioco, possiamo continuare ad implementare il game engine. Definiamo la funzione Inputs.MouseInsideText in inputs.js
.
Inputs.MouseInsideText = function(str, x, y, col1, col2) {
var w = game.ctx.measureText(str).width;
var h = 30;
var inside = (Inputs.mouseX > x - w/2 && Inputs.mouseY > y - h && Inputs.mouseX < x + w/2 && Inputs.mouseY < y+4 );
if(inside) game.ctx.fillStyle = col2;
else game.ctx.fillStyle = col1;
game.ctx.fillText(str, x, y);
return inside;
};
Questa funzione misura la lunghezza del testo e verifica se le coordinate del mouse sono all'interno del testo, come nel rettangolo mostrato in questa immagine:
Poi disegna il testo alle coordinate x
, y
, del colore "col2
" se il mouse è sopra il testo, altrimenti utilizza "col1
". Infine ritorna true
se il mouse è all'interno del testo.
Nota: purtroppo, in HTML5 non è possibile ottenere l'altezza del testo in modo performante, quindi è consigliabile utilizzare un ulteriore argument nella funzione e definire a mano tale valore.
Torniamo al gioco
Finalmente creiamo un file hud.js che conterrà tutte le funzioni relative all'interfaccia grafica e inseriamo la seguente funzione:
function MainMenu() {
game.sndMusic.loop = true;
game.sndMusic.play();
this.Draw = function() {
// disegna lo sfondo
game.ctx.save();
game.ctx.fillStyle = game.patternMenu;
game.ctx.fillRect(0, 0, game.canvas.width, game.canvas.height);
game.ctx.restore();
// mostra logo e personaggio
game.ctx.drawImage(game.sprLogo, game.canvas.width/2 - game.sprLogo.width/2 , 80);
game.ctx.drawImage(game.sprSplashLogo, 70 , 180);
game.ctx.shadowColor = "#000";
game.ctx.shadowOffsetX = 1;
game.ctx.shadowBlur = 3;
// imposta il font
game.ctx.font = "32pt 'PixelFont'"
game.ctx.textAlign = "center";
// centro del canvas
var cx = game.canvas.width/2;
var cy = game.canvas.height/2;
// disegna il menu e rileva le azioni dell'utente
if(Inputs.MouseInsideText("New Game",cx, cy+10,"#eee", "#ea4") && Inputs.GetMousePress(MOUSE_LEFT)) {
//carica il livello 1
game.LoadLevel(1);
}
if(Inputs.MouseInsideText("Other games",cx, cy+80,"#eee", "#ea4") && Inputs.GetMousePress(MOUSE_LEFT)) {
window.location.href = "http://google.com";
}
game.ctx.shadowOffsetX = 0;
game.ctx.shadowBlur = 0;
}
}
Esaminiamo questa dichiarazione. Quando viene istanziato, l'oggetto MainMenu
imposta l'esecuzione della musica in loop.
Nella funzione Draw definiamo prima di tutto il background: dopo aver impostato il fillStyle
del context, disegnamo un rettangolo grande quanto il canvas, che sarà riempito col pattern (save
e restore
si occupano di salvare e ripristinare lo status del context).
I parametri di createPattern possono essere repeat
, repeat-x
, repeat-y
oppure no-repeat
.
Creato lo sfondo si piazzano sullo schermo il logo e l'immagine del personaggio.
Definire un gradiente in HTML5 Canvas
Se non vogliamo utilizzare un pattern come sfondo, potremmo creare dei gradienti in questo modo:
var gradient = context.createLinearGradient(0, 0, 0, game.canvas.height);
gradient.addColorStop(0, '#171');
gradient.addColorStop(0.33, '#5a5');
gradient.addColorStop(0.66, '#8a8');
gradient.addColorStop(1, '#8a8');
game.ctx.fillStyle = gradient;
Qui createLinearGradient
crea un gradiente da [x1,y1
= 0,0
] a [x2,y2
= 0, game.canvas.height
] con le istruzioniaddColorStop(stop, colore)
aggiungiamo i colori che ci servono e li posizioniamo all'interno della sfumatura, utilizzando un range 0 a 1 (0=inizio della sfumatura e 1=fine).
Una volta definiti i colori, è sufficiente impostare il gradient come fillStyle
del context. Il risultato sarà simile a questo:
Scritte e azioni
Prima del rendering del testo, definiamo i parametri per l'ombreggiatura, il font da usare e l'allineamento. Poi invochiamo Inputs.MouseInsideText
che oltre ad occuparsi del rendering, ritornerà true nel caso in cui il mouse fosse sopra al testo.
Se l'utente preme il tasto sinistro del mouse (Inputs.GetMousePress(MOUSE_LEFT)
), passiamo alla schermata di selezione dei livelli che momentaneamente lasciamo vuota.
In fondo alla funzione, reimpostiamo i valori di shadow a 0 per evitare che l'effetto ombra sia applicato ad altri testi e immagini.
Funzioni per la gestione dei livelli
All'interno di main.js
, inseriamo nella function Game il codice seguente:
this.ResetLevel = function() {
this.mainMenu = null;
this.levelCompleted = null;
this.score = 0;
}
this.LoadLevel = function(lev) {
this.level = lev;
this.ResetLevel();
if(lev == 0) {
this.mainMenu = new MainMenu();
}
else {
//carica un livello di gioco
}
}
this.Draw = function() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
if(this.level == 0) {
//menu principale
this.mainMenu.Draw();
}
else {
//disegna il livello di gioco
}
//disegna il cursore
game.ctx.drawImage(game.sprCursor, Inputs.mouseX - game.sprCursor.width/2, Inputs.mouseY - game.sprCursor.height/2);
}
this.Update = function(){
}
Funzione | Descrizione |
---|---|
ResetLevel | Si occupa di azzerare tutte le variabili al cambio di ogni livello. |
LoadLevel | Crea un'istanza di MainMenu se carichiamo il livello 0, altrimenti carica un livello di gioco. |
Draw | Esegue clearRect per pulire la schermata del canvas dal precedente draw ed effettua il rendering del menu o del livello corrente. |
Nota: la gestione dei livelli potrebbe essere aggiunta al game engine, generalizzando una modalità di gestione delle scene. In questo caso abbiamo scelto di lasciare il motore più snello possibile, lasciando questa parte definita nel gioco.
Nei prossimi capitoli vedremo come caricare una tilemap, creare un personaggio che si muove e può raccogliere monete.