
Volevo un pulsante "Play" sul canvas del mio motore di gioco 2D.
Un pulsante. Uno.
Poi ho aperto il file e ho trovato questo:
import {
BeeUI,
BeeControl,
BeePanel,
BeeStack,
BeeLabel,
BeeUIButton,
BEE_ANCHOR,
BEE_UI_DEFAULTS,
drawNineSlice
} from './src/ui/BeeUI.js';
Nove import per un menu di pausa. Ho riso anch'io. Poi ho capito che ognuno di quei nomi ha un motivo per esistere, e alla fine sono diventati la parte del motore che preferisco. Ve li presento uno per uno, con la serietà che si meritano (poca, ma sufficiente).
Il problema: il canvas non sa cos'è un pulsante
Il canvas HTML5 è un foglio bianco con i pixel. Non ha layout, non ha focus, non sa che il tuo menu dovrebbe restare centrato quando la finestra cambia dimensione. Tutto quello che il DOM ti regala gratis, sul canvas te lo devi costruire.
Quindi sì, servono un po' di classi.
BeeControl: il nonno di tutti
BeeControl è il nodo base. Non disegna niente (il suo draw() è letteralmente vuoto), ma sa fare le cose noiose e importanti:
- tenere una lista di figli e un genitore (con campi privati
#parente#children, così nessuno li tocca a mano); - calcolare il proprio rettangolo a partire da ancore e offset;
- capire se il mouse ci sta sopra con
hit(x, y), partendo dall'ultimo figlio, cioè quello disegnato sopra.
Le ancore sono valori da 0 a 1 rispetto al genitore. Il conto è tutto qui:
const x = parentRect.x + parentRect.w * this.anchorLeft + this.offsetLeft;
const w = parentRect.w * (this.anchorRight - this.anchorLeft)
+ (this.offsetRight - this.offsetLeft);
Ancore uguali a sinistra e destra danno una larghezza fissa. Ancore diverse fanno allargare l'elemento insieme al genitore. Chi ha usato Godot riconoscerà il trucco, e non mi vergogno di averlo copiato.
BEE_ANCHOR: perché nessuno vuole scrivere setAnchors(1, 1, 1, 1)
Ricordarsi i numeri delle ancore è un incubo, quindi ci sono dei preset:
BEE_ANCHOR.TOP_LEFT, TOP, TOP_RIGHT,
LEFT, CENTER, RIGHT,
BOTTOM_LEFT, BOTTOM, BOTTOM_RIGHT,
FULL
Vuoi il punteggio in alto a destra con 16 pixel di margine?
new BeeLabel({ text: 'Score: 0', anchor: BEE_ANCHOR.TOP_RIGHT, x: 16, y: 16, width: 160 });
Ridimensioni la finestra e resta lì. Magia? No, sottrazioni.
BEE_UI_DEFAULTS: le decisioni che non voglio prendere ogni volta
{ gap: 8, padding: 12, buttonWidth: 220, buttonHeight: 44 }
È un oggetto congelato con Object.freeze. Quattro numeri, zero discussioni. 44 pixel di altezza perché è una misura comoda da toccare col dito.
BeePanel: un rettangolo con ambizioni
BeePanel è un controllo che finalmente disegna qualcosa: colore di sfondo, bordo, angoli arrotondati (se il browser supporta roundRect) oppure un'immagine. E se gli passi anche slice, usa…
drawNineSlice: l'eroe silenzioso
Hai una cornice da 48×48 pixel e vuoi un pannello da 400×300 senza che gli angoli si deformino. La tecnica nine-slice taglia l'immagine in 9 pezzi: i 4 angoli restano fissi, i bordi si allungano in una sola direzione e il centro si allunga in tutte e due.
new BeePanel({
anchor: BEE_ANCHOR.CENTER,
width: 400, height: 300,
image: frameImage,
slice: { left: 12, top: 12, right: 12, bottom: 12 }
});
Nove chiamate a drawImage e il tuo menu sembra uscito da un gioco vero.
BeeStack: il flexbox dei poveri (e mi basta)
Mettere i pulsanti uno sotto l'altro calcolando le y a mano è il modo migliore per odiare la gamedev. BeeStack lo fa per te, in verticale ('v') o in orizzontale ('h'):
-
measure()somma le dimensioni dei figli visibili, più spazi e margini interni; -
layout()li posiziona uno dopo l'altro; - se è ancorato al centro, si ricentra da solo in base alla dimensione calcolata.
BeeLabel e BeeUIButton: finalmente quello che volevo
BeeLabel scrive del testo. Fine. BeeUIButton è il pulsante vero: ha stati diversi per mouse sopra, premuto, disabilitato e focus, e si attiva al rilascio del mouse solo se sei ancora sopra al pulsante, esattamente come un pulsante del browser. Se ti penti a metà click, trascini fuori e non succede niente.
BeeUI: il capo
BeeUI è la radice dell'albero. Occupa tutto il canvas e a ogni frame:
- ricalcola il layout solo se qualcosa è cambiato (c'è un flag
#dirty); - gestisce hover e click del mouse;
- gestisce la tastiera: Tab e Shift+Tab, frecce o WASD, Invio e Spazio;
- gestisce il gamepad: croce direzionale e tasto A.
La parte che mi piace di più è focusToward(dx, dy). Quando premi "giù", non va semplicemente al pulsante successivo nella lista: cerca il controllo più vicino in quella direzione, penalizzando quelli troppo spostati di lato. Funziona anche con griglie di pulsanti, non solo con le liste.
Inoltre blocca il comportamento normale del Tab sul canvas, così il focus non scappa nella barra degli indirizzi a metà partita.
Un menu di pausa completo
const ui = new BeeUI({ canvas });
const menu = ui.add(new BeeStack({ anchor: BEE_ANCHOR.CENTER, background: '#0f172acc', radius: 12 }));
menu.add(new BeeLabel({ text: 'PAUSA', align: 'center', font: 'bold 24px monospace' }));
menu.add(new BeeUIButton({ text: 'Riprendi', onClick: () => game.resume() }));
menu.add(new BeeUIButton({ text: 'Opzioni', onClick: () => openOptions() }));
menu.add(new BeeUIButton({ text: 'Esci', onClick: () => game.quit() }));
ui.focusAt(0);
// nel game loop
ui.update(input);
ui.draw(ctx);
Centrato, navigabile con mouse, tastiera e gamepad, e resta al suo posto quando ridimensioni la finestra. Il tutto in circa 700 righe senza dipendenze.
Quindi, nove classi erano troppe?
Probabilmente sì, per un pulsante.
Per un pulsante che si centra da solo, si allarga, si naviga col joystick e non si rompe quando giri il telefono, no.
E la prossima volta che qualcuno mi chiede "perché non usi semplicemente un <button> sopra il canvas?", gli mando questo articolo e vado a giocare.
Top comments (0)