In questo capitolo impareremo come creare un formspec e mostrarlo all’utente. Un formspec è il codice di specifica di un modulo (form, da qui form-spec). In Minetest, i moduli sono delle finestre come l’inventario del giocatore e possono contenere un’ampia gamma di elementi, come le etichette, i pulsanti e i campi.
Tieni presente che se non si ha bisogno di ricevere input dal giocatore, per esempio quando si vogliono far apparire semplicemente delle istruzioni a schermo, si dovrebbe considerare l’utilizzo di una HUD (Heads Up Display) piuttosto che quello di un formspec, in quanto le finestre inaspettate (con tanto di mouse che appare) tendono a impattare negativamente sulla giocabilità.
Nelle vecchie versioni di Minetest, i formspec erano incoerenti. Il modo in cui elementi diversi venivano posizionati nel formspec variava in maniere inaspettate; era difficile predirne la collocazione e allinearli correttamente. Da Minetest 5.1.0, tuttavia, è stata introdotta una funzione chiamata Coordinate Reali (real coordinates), la quale punta a correggere questo comportamento tramite l’introduzione di un sistema di coordinate coerente. L’uso delle coordinate reali è caldamente consigliato, onde per cui questo capitolo non tratterà di quelle vecchie.
Usando una versione del formspec maggiore o uguale a 2, esse saranno abilitate di base.
Il formspec è un linguaggio di dominio specifico con un formato insolito. Consiste in un numero di elementi che seguono il seguente schema:
tipo[param1;param2]
Viene prima dichiarato il tipo dell’elemento, seguito dai parametri nelle parentesi quadre. Si possono concatenare più elementi, piazzandoli eventualmente su più linee:
foo[param1]bar[param1]
bo[param1]
Gli elementi sono o oggetti come i campi di testo e i pulsanti, o dei metadati come la grandezza e lo sfondo. Per una lista esaustiva di tutti i possibili elementi, si rimanda a lua_api.md.
L’intestazione di un formspec contiene informazioni che devono apparire prima di tutto il resto. Questo include la grandezza del formspec, la posizione, l’ancoraggio, e se il tema specifico del gioco debba venir applicato.
Gli elementi nell’intestazione devono essere definiti in un ordine preciso, altrimenti ritorneranno un errore. L’ordine è dato nel paragrafo qui in alto e, come sempre, documentato in lua_api.md.
La grandezza è in caselle formspec - un’unità di misura che è circa 64 pixel, ma varia a seconda della densità dello schermo e delle impostazioni del client. Ecco un formspec di 2x2:
formspec_version[4]
size[2,2]
Notare come è stata esplicitamente definita la versione del linguaggio: senza di essa, il sistema datato sarebbe stato usato di base - che avrebbe impossibilitato il posizionamento coerente degli elementi e altre nuove funzioni.
La posizione e l’ancoraggio degli elementi sono usati per collocare il formspec nello schermo. La posizione imposta dove si troverà (con valore predefinito al centro, 0.5,0.5
), mentre l’ancoraggio da dove partire, permettendo di allineare il formspec con i bordi dello schermo. Per esempio, lo si può posizionare ancorato a sinistra in questo modo:
formspec_version[4]
size[2,2]
position[0,0.5]
anchor[0,0.5]
Per l’esattezza è stato messo il centro del formspec sul bordo sinistro dello schermo (position[0, 0.5]
) e poi ne è stato spostato l’ancoraggio in modo da allineare il lato sinistro del formspec con quello dello schermo.
Il modo migliore per imparare è sporcarsi le mani, quindi creiamo un gioco. Il principio è semplice: la mod decide un numero, e il giocatore deve tentare di indovinarlo. La mod, poi, comunica se si è detto un numero più alto o più basso rispetto a quello corretto.
Prima di tutto, costruiamo una funzione per creare il formspec. È buona pratica fare ciò, in quanto rende il riutilizzo più comodo.
indovina = {}
function indovina.prendi_formspec(nome)
-- TODO: comunicare se il numero del tentativo era più alto o più basso
local testo = "Sto pensando a un numero... Prova a indovinare!"
local formspec = {
"formspec_version[4]",
"size[6,3.476]",
"label[0.375,0.5;", core.formspec_escape(testo), "]",
"field[0.375,1.25;5.25,0.8;numero;Numero;]",
"button[1.5,2.3;3,0.8;indovina;Indovina]"
}
-- table.concat è più veloce della concatenazione di stringhe - `..`
return table.concat(formspec, "")
end
Nel codice qui sopra abbiamo inserito un’etichetta (label), un campo (field) e un pulante (button). Un campo ci permete di inserire del testo, mentre useremo il pulsante per inviare il modulo. Noterai che gli elementi sono posizionati attentamente per aggiungere imbottitura e spaziatura (padding e spacing), ma ci arriveremo tra poco.
Come prossima cosa, vogliamo permettere al giocatore di visualizzare il formspec. Il metodo principale per farlo è usare show_formspec
:
function indovina.mostra_a(nome)
core.show_formspec(nome, "indovina:gioco", indovina.prendi_formspec(nome))
end
core.register_chatcommand("gioco", {
func = function(name)
indovina.mostra_a(name)
end,
})
La funzione show_formspec
prende il nome del giocatore, il nome del formspec e il formspec stesso. Il nome di quest’ultimo dovrebbe seguire il formato del nome degli oggetti, tipo nomemod:nomeoggetto
.
L’imbottitura (padding) è lo spazio che intercorre tra il bordo del formspec e i suoi contenuti, o tra elementi non in relazione fra loro - mostrato in rosso. La spaziatura (spacing) è invece lo spazio tra elementi in comune - mostrata in blu.
È abbastanza uno standard avere un’imbottitura di 0.375
e una spaziatura di 0.25
.
Quando show_formspec
viene chiamato, il formspec viene inviato al client per essere visualizzato. Per far sì che i formspec siano utili, le informazioni devono essere ritornate dal client al server. Il metodo per fare ciò è chiamato Campo di Compilazione (formspec field submission), e per show_formspec
quel campo viene ottenuto usando un callback globale:
core.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "indovina:gioco" then
return
end
if fields.indovina then
local p_name = player:get_player_name()
core.chat_send_all(p_name .. " ha tentato di indovinare con il numero " .. fields.numero)
end
end)
La funzione data in core.register_on_player_receive_fields
è chiamata ogni volta che un utente invia un modulo. La maggior parte dei callback necessiteranno di controllare il nome fornito alla funzione, e uscire se non è quello esatto; tuttavia, alcuni potrebbero necessitare di operare su più moduli, se non addirittura su tutti.
Il parametro fields
è una tabella di tutti i valori inviati dall’utente, indicizzati per stringhe. I nomi degli elementi appariranno nel campo con il loro nome, ma solo se sono rilevanti per l’evento che ha causato l’invio. Per esempio, un elemento “pulsante” apparirà nei campi solo se quel particolare pulsante è stato premuto.
Non dovresti mai fidarti di un modulo di compilazione - anche se non hai mai mostrato loro il formspec. Questo significa che dovresti controllarne i privilegi e assicurarti che dovrebbero effettivamente essere in grado di eseguire quest’azione.
Quindi, ora il formspec è stato inviato al client e il client ritorna quelle informazioni. Il prossimo passaggio è generare e ricordare il valore ricevuto, e aggiornare il formspec basandosi sui tentativi. Il modo per fare ciò è usare un concetto chiamato “contesto”.
In molti casi si può desiderare che le informazioni passate da show_formspec
al callback non raggiungano il client. Ciò potrebbe includere con cosa è stato chiamato un comando via chat, o di cosa tratta la finestra di dialogo. In questo caso, il valore che si necessita di ricordare.
Un contesto (context) è una tabella assegnata a ogni giocatore per immagazzinare informazioni, e i contesti di tutti i giocatori sono salvati in una variabile locale di file:
local _contesti = {}
local function prendi_contesto(nome)
local contesto = _contesto[nome] or {}
_contesti[nome] = contesto
return contesto
end
core.register_on_leaveplayer(function(player)
_contexts[player:get_player_name()] = nil
end)
Ora abbiamo bisogno di modificare il codice da mostrare, per aggiornare il contesto prima di mostrare il formspec:
function indovina.mostra_a(nome)
local contesto = prendi_contesto(nome)
contesto.soluzione = contesto.soluzione or math.random(1, 10)
local formspec = indovina.prendi_formspec(nome, contesto)
core.show_formspec(nome, "indovina:gioco", formspec)
end
Abbiamo anche bisogno di modificare la generazione del formspec per usare il contesto:
function indovina.prendi_formspec(nome, contesto)
local testo
if not contesto.tentativo then
testo = "Sto pensando a un numero... Prova a indovinare!"
elseif contesto.tentativo == contesto.soluzione then
testo = "Yeee, hai indovinato!"
elseif contesto.tentativo > contesto.soluzione then
testo = "Troppo alto!"
else
testo = "Troppo basso!"
end
Tieni a mente che quando si ottiene il formspec è buona norma leggerne il contesto, senza però aggiornalo. Questo può rendere la funzione più semplice, e anche più facile da testare.
E in ultimo, abbiamo bisogno di aggiornare il contesto con il tentativo del giocatore:
if fields.indovina then
local nome = player:get_player_name()
local contesto = prendi_contesto(nome)
contesto.tentativo = tonumber(fields.numero)
indovina.mostra_a(nome)
end
Ci sono tre diversi modi per far sì che un formspec sia consegnato al client:
register_on_player_receive_fields
;on_receive_fields
.register_on_player_receive_fields
.core.show_formspec
non è l’unico modo per mostrare un formspec; essi possono infatti essere aggiunti anche ai metadati di un nodo. Per esempio, questo è usato con le casse per permettere tempi più veloci d’apertura - non si ha bisogno di aspettare che il server invii il formspec della cassa al giocatore.
core.register_node("miamod:tastodestro", {
description = "Premimi col tasto destro del mouse!",
tiles = {"miamod_tastodestro.png"},
groups = {cracky = 1},
after_place_node = function(pos, placer)
-- Questa funzione è eseguita quando viene piazzato il nodo.
-- Il codice che segue imposta il formspec della cassa.
-- I metadati sono un modo per immagazzinare dati nel nodo.
local meta = core.get_meta(pos)
meta:set_string("formspec",
"formspec_version[4]" ..
"size[5,5]"..
"label[1,1;Questo è mostrato al premere col destro]"..
"field[1,2;2,1;x;x;]")
end,
on_receive_fields = function(pos, formname, fields, player)
if(fields.quit) then return end
print(fields.x)
end
})
I formspec impostati in questo modo non innescano lo stesso callback. Per far in modo di ricevere il modulo di input per i formspec nei nodi, bisogna includere una voce on_receive_fields
al registrare il nodo.
Questo stile di callback viene innescato al premere invio in un campo, che è possibile grazie a core.show_formspec
; tuttavia, questi tipi di moduli possono essere mostrati solo tramite il premere col tasto destro del mouse su un nodo. Non è possibile farlo programmaticamente.
L’inventario del giocatore è un formspec, che viene mostrato al premere “I”. Il callback globale viene usato per ricevere eventi dall’inventario, e il suo nome è ""
.
Ci sono svariate mod che permettono ad altrettante mod di personalizzare l’inventario del giocatore. La mod ufficialmente raccomandata è SFINV, ed è inclusa in Minetest Game.
owner
nei metadati, e dovrebbe usare show_formspec
per mostrare formspec differenti a giocatori differenti.