20257 lines
807 KiB
Text
20257 lines
807 KiB
Text
= Riferimento di Singe
|
||
Scott Duensing <scott@kangaroopunch.com>
|
||
:revnumber: 3.00
|
||
:revdate: 2026
|
||
:doctype: book
|
||
:toc: left
|
||
:toclevels: 3
|
||
:sectnums:
|
||
:sectnumlevels: 3
|
||
:title-logo-image: image:images/singeLogo.png[Singe, pdfwidth=2.6in, align=center]
|
||
:source-highlighter: rouge
|
||
:icons: font
|
||
:experimental:
|
||
:lang: it
|
||
:imagesdir: ..
|
||
include::locale.adoc[]
|
||
|
||
|
||
[#_game_development]
|
||
== Sviluppo di giochi
|
||
|
||
[#_lua]
|
||
=== Lua
|
||
|
||
Singe usa il linguaggio di programmazione Lua (http://www.lua.org) per scrivere la
|
||
logica dei giochi. Lua è veloce, leggero, orientato agli oggetti, facile da usare e
|
||
usato davvero nell'industria dei videogiochi. Questo libro non lo insegna; lo fa
|
||
_Impara a programmare con Singe_, nello stesso download di questo, partendo da zero
|
||
fino a un gioco finito in trentuno lezioni. Ci sono video tutorial su Lua sul canale
|
||
YouTube di Kangaroo Punch, e il manuale di riferimento del linguaggio si trova su
|
||
http://www.lua.org.
|
||
|
||
[#_basic_rules]
|
||
=== Regole di base
|
||
|
||
* Singe è multipiattaforma. Cose di cui di solito non devi preoccuparti su
|
||
Windows qui sono importanti. I nomi dei file distinguono tra maiuscole e
|
||
minuscole -- `MyScript.singe` e `myscript.singe` non sono lo stesso file.
|
||
Dovresti anche usare i separatori di percorso UNIX -- `/` invece di `\`. Così il
|
||
codice è più pulito (niente barre rovesciate con doppio escape) e funziona
|
||
ovunque Singe sia supportato.
|
||
* Scegli un nome distintivo per la cartella del gioco. Non usare spazi.
|
||
* Usa il Singe Framework incluso. Non copiare il framework negli script del tuo
|
||
gioco. Includendo il framework fornito aiuti la compatibilità futura man mano
|
||
che Singe viene aggiornato. Inizia (o termina) lo script del gioco con:
|
||
+
|
||
[source,lua]
|
||
----
|
||
dofile("Singe/Framework.singe")
|
||
----
|
||
* Mantieni il gioco autosufficiente. Se lo costruisci su un framework di terze
|
||
parti o condividi codice tra i tuoi giochi, copialo nella cartella del gioco.
|
||
Non fare mai riferimento a una cartella accanto al gioco; l'unico file fuori dal
|
||
gioco che uno script può caricare è `Singe/Framework.singe`. Un gioco in un solo
|
||
file (vedi <<singlefile,Giochi in un solo file>>) impacchetta esattamente una
|
||
cartella, e un gioco che esce da essa non può essere impacchettato.
|
||
* Stai fuori dalla cartella `Singe/`. È gestita da Singe, e qualunque cosa vi sia
|
||
aggiunta o modificata può essere cancellata in futuro.
|
||
* Includi un `games.dat`. È estremamente importante per i nuovi utenti. Anche se
|
||
esistono altri front-end e sistemi di menu basati su Singe, è fondamentale che
|
||
tu includa i dati per il menu integrato. Quasi tutti i problemi di assistenza
|
||
sono dovuti a file `games.dat` mancanti o malformati.
|
||
* Non distribuire file non essenziali. Non distribuire mai i binari di Singe o gli
|
||
script forniti da Singe con il tuo gioco. Non distribuire i file indice generati
|
||
dai video.
|
||
|
||
[#reloading]
|
||
=== Ricaricare mentre lavori
|
||
|
||
Avvia il gioco con `--reload` (`-R`) e Singe sorveglia ogni file di script
|
||
sciolto che il gioco ha caricato. Salva una modifica a uno di essi e il gioco
|
||
riparte dal suo script un attimo dopo: tutto ciò che lo script ha creato se ne va
|
||
(sprite, suoni, font, video, GUI, la scena 3D, la fisica, gli emettitori,
|
||
l'overlay torna alla dimensione predefinita, il volume degli effetti e il mouse
|
||
tornano allo stato di avvio), il motore resta (la finestra, la GPU, i controller),
|
||
il disco viene di nuovo fermato sul suo primo fotogramma come all'avvio, e lo
|
||
script riparte da capo, così un modello da 52 MB torna sullo schermo nel tempo
|
||
necessario a caricarlo invece che nel tempo necessario ad avviare Singe. Un tasto
|
||
o un pulsante tenuto premuto durante il ricaricamento viene ignorato finché non
|
||
viene rilasciato, come a un avvio da zero. `F5` ricarica a richiesta, e uno script
|
||
può chiederlo con `singeReload()`. I giochi impacchettati non hanno niente da
|
||
sorvegliare; l'opzione è per un gioco in una cartella. Un errore di script durante
|
||
il ricaricamento viene stampato e il gioco resta vuoto finché il ricaricamento
|
||
successivo non lo corregge; il file resta sorvegliato, quindi basta salvare la
|
||
correzione. Poiché il ricaricamento esegue la stessa procedura di smontaggio
|
||
dell'uscita dal gioco, qualunque risorsa uno script si lasci sfuggire si vede qui
|
||
per prima.
|
||
|
||
[[zerobrane]]
|
||
=== Fare il debug in ZeroBrane Studio
|
||
|
||
https://studio.zerobrane.com[ZeroBrane Studio] è un IDE Lua gratuito, e Singe
|
||
porta con sé quello che serve per avviarci un gioco e farne il debug:
|
||
breakpoint, esecuzione passo passo, le finestre Stack e Watch, un gioco che
|
||
resta fermo finché è fermo il debugger, una sosta nell'IDE quando un errore sta
|
||
per chiudere il gioco, e l'autocompletamento con i tooltip per tutta l'API, in
|
||
inglese, francese, italiano, tedesco o spagnolo. La parte del debugger è
|
||
MobDebug, integrato in Singe e avviato da `--debug`, quindi a un gioco non serve
|
||
nessuna riga sua; qualsiasi altro strumento che parli il protocollo di MobDebug
|
||
può collegarsi allo stesso modo.
|
||
|
||
[[zerobranesetup]]
|
||
==== Prepararlo
|
||
|
||
Singe decomprime un pacchetto per ZeroBrane Studio nella sua cartella di
|
||
supporto, come `Singe/ZeroBrane/singe.lua`, e lo tiene aggiornato da una
|
||
versione all'altra. Di' a ZeroBrane Studio dove si trova con una sola riga nel
|
||
suo `user.lua` (Modifica > Preferenze > Impostazioni: Utente), poi riavvialo:
|
||
|
||
[source,lua]
|
||
----
|
||
package "/home/me/Singe/Singe/ZeroBrane/singe.lua"
|
||
----
|
||
|
||
Singe compare allora in Progetto > Interprete Lua, con Progetto > Debug con
|
||
Singe e un pulsante Singe nella barra degli strumenti per avviare un gioco sotto
|
||
il debugger in un solo passo. I file `.singe` si aprono come Lua, i documenti
|
||
`.rml` come XML e i fogli di stile `.rcss` come CSS. Fai della cartella del
|
||
gioco il progetto (Progetto > Directory del Progetto > Scegli...). Il pacchetto
|
||
cerca Singe in quella cartella e nelle tre sopra, prendendo la versione più
|
||
recente per la tua macchina, e poi nel `PATH`; se non ne trova nessuno te lo
|
||
chiede la prima volta che avvii un gioco, e se lo ricorda. Progetto > Scegli
|
||
l'eseguibile di Singe... ne indica un altro, e
|
||
`path.singe = "/path/to/Singe"` in `user.lua` ha la meglio su entrambi.
|
||
|
||
[[zerobranerun]]
|
||
==== Avviare e fare il debug
|
||
|
||
Progetto > Lancia (`F6`) avvia il gioco, e Progetto > Inizia il Debug (`F5`) lo
|
||
avvia sotto il debugger. Lo script è il file di partenza del progetto, se ce
|
||
n'è uno (fai clic destro su di esso nell'albero del progetto e scegli Imposta
|
||
come File di Partenza), e altrimenti il file che stai modificando; quando questo
|
||
è uno dei moduli del gioco invece di un file `.singe`, parte l'unico file
|
||
`.singe` del progetto. Singe parte dalla cartella che contiene il suo
|
||
eseguibile, come lo avvierebbe il menu, così `Singe/Framework.singe` e ogni
|
||
nome relativo a quella cartella si risolvono come sempre, e la voce del gioco
|
||
in `games.dat` fornisce il suo video e le sue impostazioni. Progetto > Parametri
|
||
Riga di Comando... aggiunge opzioni tue, tra cui `-v` e un video per un gioco
|
||
senza voce.
|
||
|
||
Fai clic nel margine per mettere un breakpoint. Il gioco si ferma lì con le sue
|
||
variabili locali nella finestra Stack, la finestra Watch valuta espressioni, e
|
||
Step Into (`F10`), Step Over (`Shift+F10`) e Step Out (`Ctrl+F10`) si
|
||
comportano come in qualsiasi programma Lua, anche dentro `singeMain`.
|
||
Progetto > Interrompi ferma un gioco dovunque sia. Finché il gioco è fermo, il
|
||
disco, ogni video e ogni suono vanno in pausa proprio come con il tasto di
|
||
pausa, e poi riprendono dallo stesso punto.
|
||
|
||
Quando un errore sta per chiudere il gioco -- un errore Lua, o una chiamata del
|
||
motore che riceve qualcosa che non sa usare -- l'IDE si ferma prima, nel
|
||
`Debugger.singe` della cartella Singe, e la finestra Output dice che cosa è
|
||
andato storto. La finestra Stack tiene sotto quel file i frame dello script
|
||
stesso, con le loro variabili locali com'erano. Continua, e il gioco finisce
|
||
come sarebbe finito, con l'errore e un traceback nella finestra Output; fai
|
||
doppio clic su una riga del traceback per aprire il file in quel punto.
|
||
|
||
Il gioco gira con `--reload`, quindi salvare uno script fa ripartire il gioco e
|
||
il debugger con lui, breakpoint compresi. Progetto > Ricarica Singe quando cambia
|
||
uno script lo disattiva.
|
||
|
||
[[zerobranecompletion]]
|
||
==== Autocompletamento e Riferimento
|
||
|
||
L'autocompletamento propone ogni funzione, callback e costante dell'API, e un
|
||
tooltip ne mostra gli argomenti e il primo paragrafo della voce.
|
||
Progetto > Analizza (`Shift+F7`) conosce anch'esso l'API, quindi segnala una chiamata scritta male invece di ogni chiamata. È
|
||
l'impostazione della lingua di ZeroBrane Studio a scegliere la lingua dei
|
||
tooltip: `language = "fr"` in `user.lua` dà il francese, e lo stesso vale per
|
||
l'italiano (`it`), il tedesco (`de`) e lo spagnolo (`es`). Fai clic destro su un
|
||
nome e scegli Cerca nel Riferimento di Singe per aprire la sua voce in un
|
||
browser; Aiuto > Riferimento di Singe apre l'inizio. Tutti e due hanno bisogno
|
||
del download della documentazione decompresso accanto a Singe, come
|
||
`Singe-Docs-v3.00` o `Singe-Docs-v3.00-fr` e così via, prendendo per primo
|
||
quello nella lingua di ZeroBrane Studio;
|
||
`singe = { reference = "/path/to/Reference.html" }` in `user.lua` ne indica un
|
||
altro.
|
||
|
||
[[zerobranecabinet]]
|
||
==== Fare il debug di un cabinato
|
||
|
||
Il debugger funziona attraverso una rete. Sulla macchina con ZeroBrane Studio,
|
||
apri la cartella del gioco come progetto e scegli Progetto > Avvia Debugger
|
||
Server. Sul cabinato, avvia il gioco con `--debug=HOST`, dove `HOST` è il nome o
|
||
l'indirizzo di quella macchina; la porta è `8172` a meno che
|
||
`--debug=HOST:PORT` non ne dica un'altra, e la prima macchina deve accettare
|
||
connessioni su di essa. Un cabinato che avvia i suoi giochi dal menu può avere
|
||
invece `debug = "HOST:PORT"` nel suo file delle impostazioni, e ogni gioco che
|
||
avvia si collega, menu compreso. L'IDE ferma ciascuno alla sua prima riga, dove
|
||
Continua (`F5`) lo fa partire; `debugger.runonstart = true` in `user.lua` lo
|
||
lascia andare dritto fino al primo breakpoint. Il cabinato comunica i propri
|
||
percorsi, che ZeroBrane Studio fa corrispondere ai file del progetto la prima
|
||
volta che si ferma, così basta la stessa cartella del gioco su entrambe le
|
||
macchine. Se nessuno è in ascolto, un gioco si gioca come al solito una volta
|
||
che il tentativo di collegarsi rinuncia, il che non richiede più di due secondi.
|
||
|
||
[[zerobranelimits]]
|
||
==== Limiti
|
||
|
||
* Fai il debug di un gioco non impacchettato, dalla sua cartella. Un `.game` impacchettato non ha file su disco che l'IDE possa aprire.
|
||
* Un errore dentro `singeMain` si ferma in `Framework.singe`, che esegue `singeMain` come una coroutine; il messaggio indica la riga del tuo script.
|
||
* Finché l'IDE tiene il gioco fermo, la finestra non si ridisegna, e chiuderla aspetta che tu continui. Ferma il processo (`Shift+F5`) chiude subito il gioco.
|
||
* Singe ferma uno script che non restituisce mai il controllo quando si chiude la sua finestra, ma non sotto il debugger, che possiede lo script mentre gira; Interrompi trova dove è bloccato.
|
||
|
||
[[deterministic]]
|
||
=== Modalità di test deterministica
|
||
|
||
`--deterministic` è uno strumento di test, non un modo di giocare. Fa sì che il
|
||
motore ignori del tutto il tempo reale: invece di chiedere alla macchina che ora
|
||
è, il motore gira su un orologio virtuale che avanza di un passo fisso -- 15
|
||
millisecondi per impostazione predefinita -- esattamente una volta per fotogramma.
|
||
Tutto ciò che è scandito dal tempo segue quell'orologio: i passi della fisica,
|
||
l'animazione di sprite e modelli, le particelle, la navigazione, le animazioni e
|
||
le transizioni della GUI, `os.clock()`, `singeGetTicks()` e un `os.time()` senza
|
||
argomenti (un gioco che inizializza il suo generatore dall'orologio quando comincia
|
||
una partita lo inizializza da quello virtuale, quindi estrae gli stessi numeri a
|
||
ogni esecuzione; `os.time` con una tabella di data la converte come Lua ha sempre
|
||
fatto). Anche il disco lo segue: mentre è in riproduzione, ogni fotogramma video
|
||
resta visibile per tanti fotogrammi del motore quanti ne vale il suo tempo di
|
||
presentazione su quell'orologio (due o tre con il passo predefinito per un disco a
|
||
trenta fotogrammi), come nel gioco reale, e un disco in pausa o dopo un salto
|
||
resta dove è stato messo. Il generatore di Lua e quello del motore partono da un
|
||
seme fisso. Niente aspetta niente, quindi il gioco gira alla massima velocità che
|
||
la macchina gli consente.
|
||
|
||
Il senso di tutto questo è che uno screenshot preso a un numero di fotogramma
|
||
fisso è la stessa immagine in ogni esecuzione. Senza, il fotogramma 40 è un
|
||
momento diverso ogni volta -- la fisica ha fatto un numero diverso di passi e il
|
||
disco è su un fotogramma video diverso -- quindi due esecuzioni dello stesso gioco
|
||
invariato possono differire di un milione di pixel, e una modifica vera può
|
||
nascondersi in quel rumore.
|
||
|
||
Il prezzo è tutto ciò che dipende dal tempo reale. L'audio suona ancora
|
||
sull'orologio del dispositivo, che nessun orologio virtuale raggiunge, quindi il
|
||
suono si allontana dall'immagine e non significa nulla -- a meno che l'esecuzione
|
||
non venga registrata con `--record`, nel qual caso il suono viene mixato in memoria
|
||
un passo alla volta insieme all'immagine, così una registrazione fatta in questo
|
||
modo è esatta fotogramma per fotogramma e campione per campione (vedi
|
||
<<singerecordstart,singeRecordStart>>). Nemmeno la frequenza dei fotogrammi
|
||
significa nulla, né qualsiasi cosa un gioco misuri in secondi. Non riportare mai
|
||
un dato di prestazioni, un problema audio o un problema di tempistica da
|
||
un'esecuzione in questa modalità, e non distribuire mai un launcher che la usi.
|
||
|
||
Il numero che l'opzione porta, se ne porta uno, è sia la dimensione del passo sia
|
||
il seme da cui partono i generatori, da `1` a `1000`:
|
||
|
||
----
|
||
Singe --deterministic il passo predefinito di 15 ms, seme 15
|
||
Singe --deterministic=20 un passo di 20 ms, seme 20
|
||
----
|
||
|
||
Due esecuzioni con lo stesso numero sono identiche; due esecuzioni con numeri
|
||
diversi sono ciascuna ripetibile ma diverse tra loro, ed è così che un test chiede
|
||
un'esecuzione diversa ma ugualmente riproducibile. Si può impostare in un file di
|
||
impostazioni come `deterministic`, come qualsiasi altra opzione.
|
||
|
||
[#_reference_screenshots]
|
||
==== Screenshot di riferimento
|
||
|
||
Le scene in `testScripts` prendono ciascuna uno o due screenshot a un numero di
|
||
fotogramma fisso e si chiudono da sole, quindi in questa modalità producono una
|
||
serie di immagini di riferimento con cui una modifica successiva può essere
|
||
confrontata pixel per pixel. Per registrarle, esegui ogni scena con l'opzione e
|
||
conserva ciò che finisce nella cartella dei dati:
|
||
|
||
[source,sh]
|
||
----
|
||
for n in 2 3 4 5 6 7 8 9 10 11 13 14 15 16 18 19 21 22 23 24 25 26 \
|
||
36 37 38 39 40 41 42 43 44 45 48 49 50 51; do
|
||
Singe -w -d data --deterministic -v Singe/menuBackground.mkv \
|
||
testScripts/scene$n.singe
|
||
done
|
||
----
|
||
|
||
Le scene lasciate fuori da quel ciclo vogliono le loro opzioni, le stesse che assegna
|
||
loro la tabella in `testScripts/README.md`: `scene12` gira senza alcun disco,
|
||
`scene17` con il disco e `scene20` con `-C 720x480`, tutte e tre con
|
||
`SDL_GPU_DRIVER=nothing`; da `scene27` a `scene35` vogliono `-x 1280 -y 720
|
||
-C 1280x720` e nessun disco; `scene46` vuole `-g '10 5'`; e `scene47` vuole
|
||
`--bezeldir=testScripts/bezels --bezel=cabinet.png`. Copia i file `singe*.png`
|
||
fuori da `data/testScripts` dopo ogni esecuzione, perché l'esecuzione successiva
|
||
numera di nuovo i suoi screenshot da zero.
|
||
|
||
Confrontare una build successiva è poi questione di eseguire gli stessi comandi e
|
||
chiedere la differenza, che deve essere nulla:
|
||
|
||
[source,sh]
|
||
----
|
||
compare -metric AE reference/scene36-singe000.png new/scene36-singe000.png null:
|
||
----
|
||
|
||
[#_where_files_live]
|
||
=== Dove stanno i file
|
||
|
||
I percorsi dentro uno script sono relativi alla cartella da cui Singe è stato
|
||
avviato, non allo script. Usa la variabile globale `DIR`, che `Framework.singe`
|
||
imposta sulla cartella dello script stesso, così un gioco funziona ovunque sia
|
||
installato:
|
||
|
||
[source,lua]
|
||
----
|
||
crosshair = spriteLoad(DIR .. "sprite_Crosshair.png")
|
||
----
|
||
|
||
Tutto ciò che Singe scrive va nella cartella dei dati (`singeGetDataPath()`),
|
||
`data/` accanto ai giochi a meno che `--datadir` non dica altrimenti, così un
|
||
gioco può stare su un supporto di sola lettura. Singe 2.x scriveva salvataggi e
|
||
impostazioni nella cartella del gioco stesso; un gioco che lo fa ancora non
|
||
funziona su un'installazione di sola lettura, quindi tieni ogni `io.output` sotto
|
||
`singeGetDataPath()`.
|
||
|
||
[#_packaging_your_game]
|
||
=== Impacchettare il tuo gioco
|
||
|
||
Un gioco finito si distribuisce come un unico file `.game`, descritto qui sotto.
|
||
Non serve nient'altro: il giocatore copia il file accanto al binario di Singe e il
|
||
menu lo elenca. Singe non installa più archivi.
|
||
|
||
[#singlefile]
|
||
=== Giochi in un solo file
|
||
|
||
Un gioco può essere un solo file. `singe --pack` scrive la cartella del gioco in
|
||
un database SQLite, e Singe lo esegue da lì: script, sprite, suoni, font,
|
||
framefile e video si caricano tutti direttamente dal database, e installare il
|
||
gioco significa copiare il file accanto all'eseguibile. Il menu elenca ogni file
|
||
`.game` che trova accanto alle cartelle dei giochi.
|
||
|
||
----
|
||
Singe --pack MyGame MyGame.game Impacchetta la cartella MyGame in MyGame.game
|
||
Singe MyGame.game Esegue la prima voce di games.dat che contiene
|
||
Singe --entry=2 MyGame.game Esegue la sua seconda voce
|
||
Singe --unpack MyGame.game MyGame Riscrive fuori i file
|
||
Singe --patch MyGame.game fixes Sostituisce i file da una cartella (o da un database di patch)
|
||
----
|
||
|
||
Niente in un gioco deve cambiare perché possa essere impacchettato. Ogni nome che
|
||
uno script usa, che sia tramite `dofile`, `require`, `io.open` e i suoi parenti,
|
||
`spriteLoad`, `soundLoad`, `fontLoad`, `videoLoad`, un framefile o un
|
||
`controls.cfg`, passa per un'unica ricerca. Per un gioco sciolto quella ricerca è
|
||
il normale filesystem, relativo alla cartella da cui Singe è stato avviato,
|
||
esattamente come prima. Per un gioco impacchettato un nome viene cercato in tre
|
||
posti, in quest'ordine, e vince il primo che lo trova:
|
||
|
||
. Una cartella sciolta con il nome del database senza estensione
|
||
(`MyGame/` accanto a `MyGame.game`). È così che continui a modificare un gioco
|
||
impacchettato, ed è così che un giocatore ne fa una mod: i file messi lì vincono
|
||
sulle copie impacchettate.
|
||
. La cartella dei dati del gioco, sotto `files/`. Tutto ciò che un gioco scrive
|
||
con `io.output` o `io.open(name, "w")` finisce qui, quindi un `game.cfg` salvato
|
||
vince sull'originale impacchettato e il database non viene mai scritto.
|
||
. Il database stesso.
|
||
|
||
Dentro un database, i nomi vengono confrontati senza distinguere tra maiuscole e
|
||
minuscole, con l'uno o l'altro separatore di percorso, e relativi alla radice del
|
||
gioco. Un primo componente uguale al nome della cartella del gioco stesso viene
|
||
ignorato, quindi `MyGame/Cfg/game.cfg`, `Cfg/game.cfg` e `DIR .. "Cfg/game.cfg"`
|
||
trovano tutti lo stesso file. Il nome del database stesso (`MyGame.game`, con o
|
||
senza separatore finale) è la radice del gioco, come lo è `.` dall'interno. I nomi
|
||
che cominciano con `Singe/` o con la cartella dei dati restano sul filesystem;
|
||
`Singe/Framework.singe` e `singeGetDataPath()` funzionano come sempre, e un tale
|
||
nome può usare `..` purché resti sotto quel prefisso (il `controls.cfg` alla
|
||
radice della cartella dei dati si trova così). Qualsiasi altro nome relativo con un
|
||
componente `..` non porta da nessuna parte dentro un gioco impacchettato:
|
||
`io.open` restituisce `nil` con un messaggio che dice che il nome esce dal gioco,
|
||
`lfs.attributes` restituisce `nil`, e `spriteLoad` e simili segnalano il file come
|
||
mancante. La radice del gioco è una cartella, non un file: `io.open` su di essa
|
||
restituisce `nil`, e `lfs.mkdir` e `lfs.rmdir` la rifiutano. `lfs.dir` su una
|
||
cartella impacchettata elenca l'unione dei tre posti, ordinata senza distinguere
|
||
tra maiuscole e minuscole; quando un file sciolto e uno impacchettato differiscono
|
||
solo per maiuscole e minuscole, viene elencata la grafia di quello sciolto.
|
||
`lfs.attributes` su un file impacchettato risponde come fa il vero `lfs`: l'intera
|
||
tabella con un argomento, un campo per nome (un nome sconosciuto solleva un
|
||
errore) o una tua tabella riempita; un file mancante restituisce `nil`, un
|
||
messaggio e il numero dell'errore.
|
||
`require("name")` trova `name.lua`, `name/init.lua` o `name.singe` sotto la
|
||
cartella dello script, poi sotto la radice del gioco.
|
||
|
||
`io.open`, `io.lines` e `io.input` su un nome che vive solo dentro il database
|
||
restituiscono un file di Singe invece di uno di Lua. L'handle di file di Lua è un
|
||
`FILE *` del C, e una riga di database non è un file, quindi leggerne una
|
||
significava scriverla accanto al gioco e aprire quella copia; l'handle di Singe
|
||
legge la riga dove si trova. Risponde a tutto ciò a cui risponde quello di Lua --
|
||
`read` con ogni formato (`"a"`, `"l"`, `"L"`, `"n"` e un numero di byte), `lines`,
|
||
`seek`, `close`, `flush` e `setvbuf` -- `io.type` lo chiama file, `io.read` e
|
||
`io.close` lo accettano, e si chiude da solo quando viene raccolto dal garbage
|
||
collector o quando una variabile `<close>` esce dal suo ambito. L'unica differenza
|
||
è che scriverci è un errore, perché ciò che sta leggendo è dentro il database e il
|
||
database non viene mai scritto. Un gioco che scrive un file ne ottiene uno vero
|
||
nella sua cartella dei dati esattamente come prima, e lo stesso vale per ogni nome
|
||
che non è impacchettato. `testScripts/packedIo` è il test che tiene tutto questo
|
||
fedele al comportamento di Lua stesso; la sua intestazione spiega come eseguirlo.
|
||
|
||
L'impacchettatore rifiuta una cartella che non ha un `games.dat`, che contiene
|
||
`controls.dat`, `Framework.singe`, un file con estensione `exe`, `sh`, `bat` o
|
||
`cmd`, o un file senza estensione il cui nome comincia con `singe`, che ha una
|
||
voce di primo livello con lo stesso nome della cartella (renderebbe ambigua la
|
||
regola della cartella del gioco stesso), o i cui script e file di dati escono dal
|
||
gioco con `..`; segnala ognuno di questi file, con la riga. Due file i cui nomi
|
||
differiscono solo per maiuscole e minuscole non possono essere impacchettati
|
||
entrambi. I file `.index` obsoleti vengono saltati. `--unpack` rifiuta un database
|
||
i cui nomi memorizzati finirebbero fuori dalla cartella di destinazione (un
|
||
percorso assoluto, una lettera di unità o un componente `..`). Un gioco che fa
|
||
riferimento alla cartella di un altro gioco (un framework condiviso accanto a
|
||
esso) deve prima copiare quella cartella al suo interno. Prima del commit del
|
||
database, il suo `games.dat` viene controllato: ogni voce ha bisogno di uno
|
||
`SCRIPT`, e ogni `SCRIPT`, `VIDEO`, `CABINET`, `MARQUEE` e `ATTRACT` che nomina
|
||
deve essere un file nel database (o sotto `Singe/`); un percorso errato viene
|
||
segnalato con il titolo della voce e l'impacchettamento viene rifiutato. `--patch`
|
||
esegue lo stesso controllo sul gioco modificato, così nemmeno una patch può
|
||
lasciarsi dietro un percorso che non porta da nessuna parte.
|
||
|
||
`--patch` accetta o una cartella organizzata come il gioco, i cui file
|
||
sostituiscono quelli impacchettati, o un file `.patch` creato da `--pack` da una
|
||
tale cartella (`Singe --pack fixes MyGame-1.1.patch`; una patch non ha bisogno di
|
||
`games.dat`). I file che la patch non nomina restano come sono. In entrambi i casi
|
||
la modifica è un'unica transazione: se viene interrotta, il gioco resta intatto.
|
||
Una patch può anche portare una tabella `removed(path)` che nomina i file da
|
||
cancellare. Un file `.patch` può essere spacchettato come un gioco, ma non può
|
||
essere eseguito e il menu lo ignora.
|
||
|
||
Una patch che sostituisce un file grande con uno piccolo lascia dentro il database
|
||
lo spazio che occupavano i vecchi contenuti, dove SQLite lo tiene per la prossima
|
||
cosa scritta invece di restituirlo. Va bene per una patch che modifica uno script,
|
||
ed è uno spreco per uno sviluppatore che reimpacchetta un video una dozzina di
|
||
volte: il `.game` può finire per essere diverse volte più grande del suo
|
||
contenuto. Così `--patch` guarda lo spazio libero dopo il commit della transazione
|
||
e, quando ce n'è almeno un megabyte, compatta il database e dice quanto ha
|
||
recuperato:
|
||
|
||
----
|
||
>>> Recovered 8007680 bytes of space the old contents were using
|
||
----
|
||
|
||
Il risultato è, byte per byte, grande quanto un impacchettamento nuovo degli
|
||
stessi file. La compattazione riscrive l'intero file, quindi viene saltata quando
|
||
c'è poco da guadagnare. Nient'altro in Singe ne ha bisogno; un gioco viene scritto
|
||
soltanto dall'impacchettatore.
|
||
|
||
La cartella dei dati di un gioco impacchettato prende il nome dal database e poi
|
||
dalla cartella dello script al suo interno: `MyGame/` per un `MyGame.game` il cui
|
||
script sta alla sua radice, sotto la cartella dei dati (`data/MyGame/` per
|
||
impostazione predefinita), che è dove la ha il `MyGame/MyGame.singe` sciolto, così
|
||
salvataggi e impostazioni sono condivisi tra un'installazione sciolta e una
|
||
impacchettata; uno script impacchettato in una sottocartella (`Game/Main.singe`)
|
||
ottiene `MyGame/Game/`. Anche l'indice del video va lì.
|
||
|
||
NOTE: Le build 3.00 precedenti davano a un gioco impacchettato il cui script stava
|
||
in una sottocartella la cartella dei dati `Game/` sotto la base, condivisa da ogni
|
||
archivio con quella struttura. I salvataggi di un gioco del genere ora stanno sotto
|
||
`MyGame/Game/`; spostali a mano. Le build 3.00 precedenti tenevano anche la
|
||
cartella del gioco stesso, quella sotto cui il suo `games.dat` nomina lo script,
|
||
quindi un `MyGame.game` impacchettato da `MyGame/` scriveva in `MyGame/MyGame/`;
|
||
sposta ciò che c'è lì su di un livello.
|
||
|
||
Quando Singe esegue un gioco da un database il menu passa `CONTAINER` nella voce di
|
||
`games.dat` che lancia; il campo viene impostato dal menu, mai a mano.
|
||
|
||
Per i tuoi dati, `require("sqlite3")` dà agli script il binding
|
||
https://lua.sqlite.org[lsqlite3] allo stesso SQLite che usa il motore. Tieni i
|
||
database che il gioco scrive in `singeGetDataPath()`:
|
||
|
||
[source,lua]
|
||
----
|
||
local sqlite3 = require("sqlite3")
|
||
local db = sqlite3.open(singeGetDataPath() .. "scores.db")
|
||
db:exec("CREATE TABLE IF NOT EXISTS scores (name TEXT, score INTEGER)")
|
||
for row in db:nrows("SELECT name, score FROM scores ORDER BY score DESC LIMIT 10") do
|
||
print(row.name, row.score)
|
||
end
|
||
db:close()
|
||
----
|
||
|
||
`sqlite3.open` trova un nome come fa `io.open`, quindi un database può essere
|
||
distribuito dentro il gioco -- tabelle dei livelli, dialoghi, elenchi di oggetti --
|
||
e aprirsi comunque quando il gioco è impacchettato. Aperto in sola lettura, un
|
||
database impacchettato viene letto dove si trova, senza copiare nulla:
|
||
|
||
[source,lua]
|
||
----
|
||
local levels = sqlite3.open(DIR .. "levels.db", sqlite3.OPEN_READONLY)
|
||
----
|
||
|
||
Aperto in scrittura, che è il comportamento predefinito di `sqlite3.open`, un
|
||
database impacchettato viene copiato una volta nella cartella dei dati del gioco,
|
||
sotto `files/` come tutto il resto che un gioco impacchettato scrive, e viene
|
||
aperta la copia. Da lì in poi il nome indica la copia, in sola lettura o no, e
|
||
l'originale impacchettato non viene mai modificato. Il database di un gioco
|
||
sciolto si apre sul posto esattamente come prima. `testScripts/packedSql` è il
|
||
test; la sua intestazione spiega come eseguirlo.
|
||
|
||
Il formato del file, per chi scrive strumenti: tre tabelle.
|
||
`meta(key, value)` contiene `version` (1), `gamedir` (il nome della cartella
|
||
impacchettata), `chunk` (la dimensione dei blocchi in byte, 4194304) e `packer`.
|
||
`assets(path, name, size, data)` ha una riga per file, dove `path` è la chiave in
|
||
minuscolo con le barre normali e `name` la grafia dell'autore; i file non più
|
||
grandi della dimensione dei blocchi stanno in `data`, quelli più grandi hanno
|
||
`data` NULL e vivono in `chunks(path, chunk, data)` come pezzi numerati
|
||
consecutivi. Singe apre il database in sola lettura e come immutabile, così gira da
|
||
supporti di sola lettura e non scrive mai un journal accanto a esso.
|
||
|
||
[#_event_driven_or_not]
|
||
=== Guidato dagli eventi... o no?
|
||
|
||
Tradizionalmente, Singe usava un modello di programmazione guidato dagli eventi --
|
||
Singe gestisce tutto e chiama il tuo codice solo quando deve dirti qualcosa o
|
||
aggiornare lo schermo. È efficiente ed è il modo in cui funziona la maggior parte
|
||
dei motori di gioco basati su Lua, ma può essere prolisso. Da Singe 2.10 c'è un
|
||
nuovo modello _a thread_ («threaded») che ti permette di scrivere codice
|
||
procedurale.
|
||
|
||
[#_event_driven]
|
||
==== Guidato dagli eventi
|
||
|
||
Con il modello di programmazione guidato dagli eventi, Singe controlla il ciclo
|
||
principale del programma ed è responsabile dell'ordine di esecuzione. Singe
|
||
gestisce automaticamente tutti i dettagli della decodifica e della presentazione
|
||
di video e audio. Gestisce controller, mouse e input da tastiera. Quando Singe ha
|
||
bisogno di qualcosa di specifico del gioco, chiama una parte del tuo script. Lo
|
||
script Singe più elementare che mostra tutte le callback esistenti è questo:
|
||
|
||
[source,lua]
|
||
----
|
||
-- Scheletro di un gioco Singe.
|
||
|
||
-- Carica il Singe Framework.
|
||
dofile("Singe/Framework.singe")
|
||
|
||
-- Dichiara qui le variabili globali che ti servono.
|
||
|
||
function onControllerMoved(axis, value, which)
|
||
--[[
|
||
Riporta quale asse del controller è stato mosso e il suo valore attuale.
|
||
(Intervallo: da -32768 a 32767.) Si usa per i dispositivi analogici.
|
||
L'input digitale è gestito da onInput e onKey.
|
||
--]]
|
||
end
|
||
|
||
function onInputPressed(what, device)
|
||
--[[
|
||
In modalità tastiera MODE_NORMAL, gli eventi di input arrivano qui quando
|
||
il tasto o il pulsante viene premuto. Per l'elenco completo di tasti,
|
||
pulsanti e controller, vedi Singe/Framework.singe. In MODE_FULL, questo
|
||
evento viene chiamato con il keysym del tasto premuto. Quando l'interruttore
|
||
viene da un mouse o da una pistola ottica, device è l'indice di quel
|
||
dispositivo (0 per il primo), come lo passa Hypseus, così un gioco a due
|
||
pistole può distinguere i suoi giocatori; per un tasto o un pad è nil.
|
||
--]]
|
||
end
|
||
|
||
function onInputReleased(what, device)
|
||
--[[
|
||
In modalità tastiera MODE_NORMAL, gli eventi di input arrivano qui quando
|
||
il tasto o il pulsante viene rilasciato. Per l'elenco completo di tasti,
|
||
pulsanti e controller, vedi Singe/Framework.singe. In MODE_FULL, questo
|
||
evento viene chiamato con il keysym del tasto rilasciato. device è come
|
||
per onInputPressed.
|
||
--]]
|
||
end
|
||
|
||
function onKeyPressed(key, scancode)
|
||
--[[
|
||
In modalità tastiera MODE_FULL, gli eventi di input arrivano qui quando il
|
||
tasto viene premuto. Vengono restituiti sia il keysym sia lo scancode. Per
|
||
l'elenco degli scancode disponibili, vedi Singe/Framework.singe.
|
||
--]]
|
||
end
|
||
|
||
function onKeyReleased(key, scancode)
|
||
--[[
|
||
In modalità tastiera MODE_FULL, gli eventi di input arrivano qui quando il
|
||
tasto viene rilasciato. Vengono restituiti sia il keysym sia lo scancode.
|
||
Per l'elenco degli scancode disponibili, vedi Singe/Framework.singe.
|
||
--]]
|
||
end
|
||
|
||
function onMouseMoved(x, y, xr, yr, which)
|
||
--[[
|
||
Chiamata quando il mouse viene mosso. In modalità SINGLE_MOUSE vengono
|
||
restituiti i valori assoluti di X e Y e la variazione relativa della
|
||
posizione. In modalità MANY_MOUSE è disponibile solo la variazione
|
||
relativa, insieme a quale mouse è stato mosso.
|
||
--]]
|
||
end
|
||
|
||
function onOverlayUpdate()
|
||
--[[
|
||
Questo è l'unico posto in cui puoi disegnare in sicurezza.
|
||
Se vuoi mostrare un mirino, dovrai salvare la posizione del
|
||
mouse da onMouseMoved in variabili globali e poi usarle qui
|
||
per disegnare il cursore.
|
||
--]]
|
||
|
||
-- Di' a Singe se abbiamo cambiato o no lo schermo.
|
||
return(OVERLAY_UPDATED) -- Oppure OVERLAY_NOT_UPDATED se non si è disegnato nulla.
|
||
end
|
||
|
||
function onShutdown()
|
||
-- Chiamata quando l'utente esce dal gioco. Libera qui le risorse caricate.
|
||
end
|
||
|
||
function onSoundCompleted(channel)
|
||
-- Il suono sul canale "channel" ha appena finito di suonare.
|
||
end
|
||
|
||
-- Nota: non esiste un evento "onStartup".
|
||
-- Il codice di avvio che ti serve può stare qui.
|
||
----
|
||
|
||
[#threaded]
|
||
==== A thread
|
||
|
||
Da Singe 2.10 puoi usare il classico modello di programmazione procedurale. Per
|
||
usare questo modello, dichiara una funzione `singeMain()` e includi il Singe
|
||
Framework alla *fine* del programma invece che all'inizio. Lua non è davvero
|
||
multithread, quindi questo modello si affida allo sviluppatore del gioco perché
|
||
faccia multitasking cooperativo chiamando `singeYield()` ovunque il codice consumi
|
||
una quantità di tempo considerevole. Come con il modello guidato dagli eventi,
|
||
Singe continua a gestire tutto l'input, la decodifica video e la decodifica audio.
|
||
Un programma a thread di esempio è questo:
|
||
|
||
[source,lua]
|
||
----
|
||
function singeMain()
|
||
local x = 10
|
||
local y = 10
|
||
|
||
while true do
|
||
colorBackground(0, 0, 0, 255)
|
||
overlayClear()
|
||
|
||
colorForeground(255, 255, 255, 255)
|
||
overlayPrint(x, y, "+")
|
||
|
||
if keyboardGetLastUp() == SCANCODE.LEFT.value then x = x - 1 end
|
||
if keyboardGetLastUp() == SCANCODE.RIGHT.value then x = x + 1 end
|
||
if keyboardGetLastUp() == SCANCODE.UP.value then y = y - 1 end
|
||
if keyboardGetLastUp() == SCANCODE.DOWN.value then y = y + 1 end
|
||
|
||
singeYield()
|
||
end
|
||
end
|
||
|
||
dofile("Singe/Framework.singe")
|
||
----
|
||
|
||
[#_hybrid]
|
||
==== Ibrido
|
||
|
||
I due modelli si combinano liberamente. Quando `singeMain` esiste,
|
||
`Framework.singe` installa il proprio `onOverlayUpdate`, ma ogni altra callback
|
||
continua a essere chiamata: definisci `onInputPressed`, `onSoundCompleted` o
|
||
`onShutdown` accanto a `singeMain` e verranno chiamate tra uno yield e l'altro
|
||
della coroutine. Una divisione tipica mantiene procedurale il flusso del gioco e
|
||
intanto reagisce agli eventi di input e audio:
|
||
|
||
[source,lua]
|
||
----
|
||
local fired = false
|
||
|
||
function onInputPressed(what)
|
||
if what == SWITCH_BUTTON1 then
|
||
fired = true
|
||
end
|
||
end
|
||
|
||
function onSoundCompleted(channel)
|
||
if channel == gunshotChannel then
|
||
soundPlay(reloadSound)
|
||
end
|
||
end
|
||
|
||
function singeMain()
|
||
while true do
|
||
if fired then
|
||
fired = false
|
||
gunshotChannel = soundPlay(gunshotSound)
|
||
end
|
||
singeYield()
|
||
end
|
||
end
|
||
|
||
dofile("Singe/Framework.singe")
|
||
----
|
||
|
||
Non definire tu stesso `onOverlayUpdate` in questo modello; disegna invece
|
||
dall'interno di `singeMain`. Un errore sollevato dentro `singeMain` termina il
|
||
gioco con un traceback, e il ritorno da `singeMain` fa uscire in modo pulito.
|
||
|
||
[#pausing]
|
||
=== La pausa
|
||
|
||
Ogni gioco ha gratis una pausa funzionante. Finché il tasto di pausa è abilitato
|
||
(l'impostazione predefinita), premere il tasto assegnato a `INPUT_PAUSE` fa sì che
|
||
il motore metta in pausa il disco, ogni video caricato e ogni suono, disegni un
|
||
indicatore PAUSED sopra l'ultimo fotogramma e smetta di eseguire lo script: niente
|
||
`onOverlayUpdate`, nessuna ripresa di `singeMain`, nessuna callback di input,
|
||
mouse, controller o suono. I timer costruiti su librerie Lua si fermano con lui,
|
||
perché niente li fa girare. Premere di nuovo il tasto riprende tutto ciò che era
|
||
in riproduzione, e lo script continua da dove si era fermato. Solo gli interruttori
|
||
del motore stesso (pausa, uscita, screenshot, cattura) funzionano ancora durante
|
||
il blocco.
|
||
|
||
Gli input restano veritieri attraverso il blocco. Tutto ciò che lo script crede
|
||
premuto viene rilasciato con `onInputReleased` (o `onKeyReleased` in `MODE_FULL`)
|
||
quando la pausa comincia, e tutto ciò che è ancora fisicamente premuto quando la
|
||
pausa finisce viene premuto di nuovo. Un joystick tenuto spinto durante una pausa
|
||
non resta mai bloccato.
|
||
|
||
Un gioco che vuole un proprio comportamento di pausa (un menu di pausa, un blocco
|
||
della storia che il giocatore non può saltare) chiama
|
||
`singeSetPauseKeyEnabled(false)`. Il tasto viene allora consegnato allo script come
|
||
`SWITCH_PAUSE` e il gioco gestisce `singeSetPauseFlag` da sé, il che mette in
|
||
pausa i media senza bloccare lo script.
|
||
|
||
In `MODE_FULL` la tastiera appartiene interamente al gioco, quindi i tasti
|
||
assegnati agli interruttori del motore lì non fanno nulla (una `p` digitata
|
||
nell'inserimento di un punteggio record deve restare una `p`). I pulsanti del
|
||
controller e del mouse assegnati a quegli interruttori continuano a funzionare in
|
||
entrambe le modalità, perché non si possono digitare.
|
||
|
||
[#gamesdat]
|
||
=== games.dat
|
||
|
||
Il file `games.dat` permette a Singe di trovare automaticamente i nuovi giochi
|
||
quando l'utente finale li installa. Questo file è estremamente importante e deve
|
||
essere incluso in ogni gioco Singe. Metti `games.dat` nella cartella di livello
|
||
più alto del tuo gioco. Un esempio che contiene più giochi:
|
||
|
||
[source,lua]
|
||
----
|
||
GAMES = {
|
||
{
|
||
TITLE = "Asteroid Patrol",
|
||
SCRIPT = "Collection/AsteroidPatrol.singe",
|
||
VIDEO = "Collection/frame_AsteroidPatrol.txt",
|
||
STRETCH = false,
|
||
NO_MOUSE = false,
|
||
RESOLUTION_X = 720,
|
||
RESOLUTION_Y = 480,
|
||
SINDEN_GUN = "",
|
||
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10",
|
||
CABINET = "Collection/cabinet_AsteroidPatrol.png",
|
||
MARQUEE = "Collection/marquee_Collection.png",
|
||
ATTRACT = "Collection/video_AsteroidPatrol.mkv",
|
||
ATTRACT_START = 3000,
|
||
ATTRACT_END = 3500,
|
||
YEAR = 2026,
|
||
PLATFORM = "Laserdisc",
|
||
DEVELOPER = "Your Studio",
|
||
PUBLISHER = "Your Studio",
|
||
GENRE = "Shooter",
|
||
DESCRIPTION = "Keep the asteroids away from the station for as long as you can.",
|
||
CREATOR = "Scott Duensing",
|
||
SOURCE = "http://kangaroopunch.com"
|
||
},
|
||
-- ... altre voci ...
|
||
}
|
||
----
|
||
|
||
Le chiavi `SCRIPT`, `VIDEO`, `CANVAS_X`, `CANVAS_Y`, `STRETCH`, `NO_MOUSE`,
|
||
`RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, `AUDIO_DELAY`,
|
||
`AUDIO_SUFFIX`, `GAME_ID`, `LEGACY_SPRITE_ARGS`, `REQUIRES` e `TIER` vengono lette
|
||
dal motore quando il menu (o il tuo script, tramite `scriptExecute` /
|
||
`scriptPush`) lancia la voce; prevalgono sulla riga di comando. La riga `VIDEO` è
|
||
il disco: una voce che ne nomina uno è un gioco laserdisc, e una voce che la omette
|
||
o la lascia vuota gira senza disco e può impostare `CANVAS_X` / `CANVAS_Y`
|
||
(720x480 per impostazione predefinita); vedi <<withoutadisc,Giochi senza disco>>.
|
||
`AUDIO_SUFFIX = "-es"` riproduce `<base>-es.ogg` accanto al video del disco invece
|
||
dell'audio contenuto in esso, per un'edizione le cui altre lingue sono distribuite
|
||
come file separati, la stessa cosa che fa `--altaudio`.
|
||
`LEGACY_SPRITE_ARGS = true` esegue un gioco scritto per Singe 2.10 con il vecchio
|
||
ordine degli argomenti degli sprite (vedi <<migrating,Migrazione da Singe 2.10>>). Le
|
||
chiavi restanti vengono lette dal menu per la visualizzazione.
|
||
|
||
`REQUIRES` dice di che cosa il gioco ha bisogno da una macchina, come elenco di
|
||
nomi: `"SCENE"` (la scena 3D), `"GUI"` (i documenti GUI), `"SHADOWS"`, `"POST"`
|
||
(il bloom e gli altri effetti che vogliono render target in virgola mobile),
|
||
`"SKIN"` (modelli con skinning) e `"MORPH"` (morph target). Nessun `REQUIRES`
|
||
significa 2D. Il menu segnala un gioco che questa macchina non può eseguire e dice
|
||
perché invece di avviarlo, e raramente scrivi tu stesso l'elenco: il motore nota
|
||
che cosa usa un gioco mentre gira, dice nella traccia quando il gioco usa qualcosa
|
||
che `REQUIRES` non elenca, e `--pack` scrive l'elenco in una voce che non ce l'ha.
|
||
`TIER` (da 0 a 3) è quanta macchina vuole il gioco; il menu misura la fascia di
|
||
questa macchina dal proprio sfondo e segnala un gioco che vuole di più come uno che
|
||
potrebbe girare lentamente. Vedi <<singecapabilities,singeCapabilities>>.
|
||
|
||
[source,lua]
|
||
----
|
||
REQUIRES = { "SCENE", "SHADOWS" },
|
||
TIER = 1,
|
||
----
|
||
|
||
Uno script lanciato direttamente, per nome o tramite la sua cartella, riceve lo
|
||
stesso trattamento: Singe cerca un `games.dat` nella cartella dello script e fino
|
||
a tre cartelle sopra di essa, e se lo `SCRIPT` di una voce nomina lo script
|
||
lanciato (i percorsi delle voci contano a partire dalla cartella sopra il
|
||
`games.dat`, quindi `Collection/games.dat` nomina
|
||
`Collection/AsteroidPatrol.singe`), si applicano le chiavi di quella voce. L'unica
|
||
differenza rispetto a un lancio dal menu è la precedenza: un'opzione digitata sulla
|
||
riga di comando (`--framefile`, `--disc`, `--stretch`, `--nomouse`,
|
||
`--xresolution`, `--yresolution`, `--sindengun`, `--audio`, `--audiodelay`,
|
||
`--altaudio`, `--canvas`) mantiene il suo valore, perché chi l'ha digitata lo
|
||
voleva. Un valore da un file di impostazioni no: è un valore predefinito, e la voce
|
||
descrive meglio il gioco (vedi xref:Manual.adoc#settings[Il file delle impostazioni]).
|
||
Un `games.dat` che non si riesce a eseguire viene segnalato e ignorato per un
|
||
lancio diretto.
|
||
|
||
[#withoutadisc]
|
||
=== Giochi senza disco
|
||
|
||
Singe non presuppone più un laserdisc. Un gioco è un gioco laserdisc solo quando ne
|
||
nomina uno: un `VIDEO` nella sua voce di `games.dat`, o `--disc` (o un
|
||
`--framefile`) sulla riga di comando. Tutto il resto gira senza disco, e il mondo
|
||
è una *tela* invece di un fotogramma video: `CANVAS_X` / `CANVAS_Y` in `games.dat`
|
||
o `--canvas=WxH` sulla riga di comando, 720x480 per impostazione predefinita, così
|
||
un gioco senza disco ha l'aspetto di un gioco laserdisc a definizione standard a
|
||
meno che non chieda altrimenti.
|
||
|
||
Senza disco il motore disegna sul nero, l'overlay è per impostazione predefinita
|
||
grande metà della tela come al solito, e le funzioni `disc*` e `vldp*` diventano
|
||
innocue: `discGetState` riporta `DISC_EJECTED`, `discGetFrame` restituisce `0`,
|
||
`discGetWidth` / `discGetHeight` restituiscono la tela, `vldpGetPixel` restituisce
|
||
nero, e le chiamate di trasporto lasciano una riga nella traccia e non fanno nulla.
|
||
Tutto il resto -- l'overlay, gli sprite, i font, i suoni, i video aggiuntivi
|
||
tramite `videoLoad`, l'input ed entrambi i modelli di programmazione -- è identico.
|
||
La variabile globale `SINGE_DISC` dice a uno script come quale tipo di gioco sta
|
||
girando.
|
||
|
||
[#gui]
|
||
=== GUI
|
||
|
||
Menu, pagine di opzioni, HUD e inserimento dei punteggi record si costruivano con
|
||
`fontPrint`, `overlayBox` e un cursore fatto a mano. Singe 3 aggiunge una GUI: un
|
||
documento scritto in RML (un markup simile all'HTML) e con lo stile dato da RCSS
|
||
(un foglio di stile simile al CSS), impaginato e disegnato da RmlUi, con a capo
|
||
automatico del testo, flexbox, pannelli nine-slice fatti con la tua grafica,
|
||
transizioni, animazioni, controlli dei moduli e un modello di focus che un
|
||
controller può pilotare. Una *GUI* è un contesto RmlUi di dimensione fissa
|
||
disegnato nella propria texture, ridisegnato ogni volta che la sua immagine cambia
|
||
(vedi <<guirefresh,guiRefresh>>), e quella texture va ovunque possa andare una
|
||
texture: composta sopra l'overlay con `guiDraw`, o mostrata su un materiale nella
|
||
scena 3D con `materialSetGui`, così la stessa pagina di opzioni funziona piatta
|
||
sullo schermo e sul monitor di un cabinato modellato. Una GUI ha bisogno del
|
||
dispositivo GPU che usa la scena 3D, quindi non è disponibile su una macchina che
|
||
non ne ha uno (vedi <<sceneperformance,Prestazioni e requisiti>>); `guiNew`
|
||
termina lo script invece di rispondere false, quindi chiedi prima a
|
||
<<singehasgpu,singeHasGpu>> se il tuo gioco può farne a meno. Il menu incluso fa
|
||
esattamente così, e quando la risposta è no si disegna invece nell'overlay (vedi
|
||
xref:Manual.adoc#menurenderers[I due renderer]). Ogni chiamata nominata qui ha una
|
||
voce nella sezione <<guiref,Gui>> del riferimento.
|
||
|
||
Il riferimento dei linguaggi RML e RCSS è la documentazione di RmlUi stessa su
|
||
https://mikke89.github.io/RmlUiDoc/, e vale la pena leggerla prima del primo
|
||
documento; questo capitolo tratta ciò che Singe aggiunge sopra.
|
||
|
||
[#_documents_and_style_sheets]
|
||
==== Documenti e fogli di stile
|
||
|
||
`guiNew(width, height)` crea una GUI e restituisce un handle intero; ne possono
|
||
esistere fino a sedici alla volta. `guiLoad(gui, name)` vi carica un documento
|
||
RML, lo mostra e restituisce un handle di documento (fino a trentadue per GUI);
|
||
`guiHide`, `guiShow` e `guiClose` fanno ciò che dicono i loro nomi, e `guiDelete`
|
||
elimina la GUI, i suoi documenti e la sua texture. Un documento è di solito grande
|
||
quanto la sua GUI, e una GUI è di solito grande quanto l'overlay per una pagina a
|
||
schermo intero o quanto il pannello per un elemento dell'HUD:
|
||
|
||
[source,lua]
|
||
----
|
||
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
|
||
local doc = guiLoad(gui, DIR .. "options.rml")
|
||
----
|
||
|
||
Il nome del documento passa per il file system virtuale come quello di uno sprite,
|
||
quindi può stare sciolto sul disco o dentro un gioco impacchettato, e viene
|
||
risolto relativamente alla cartella da cui Singe è stato avviato, quindi anteponi
|
||
`DIR` per i file distribuiti con il gioco, esattamente come per `spriteLoad`. I
|
||
nomi *dentro* un documento (lo `href` di un foglio di stile, un'immagine in un
|
||
decoratore, lo `src` di uno sprite sheet) si risolvono relativamente al file che
|
||
li nomina, come fa RmlUi, e poi tramite la stessa ricerca, quindi un documento, il
|
||
suo foglio di stile e le sue immagini stanno uno accanto all'altro nella cartella
|
||
del gioco e un gioco impacchettato li trova tutti. Due tipi di nome vengono presi
|
||
così come sono: i file del motore stesso sotto `Singe/`, e i percorsi assoluti.
|
||
|
||
La stessa regola letta al contrario permette a un documento che sta sotto `Singe/`
|
||
di raggiungere i file di un gioco: un nome relativo si risolve dalla cartella del
|
||
documento stesso, quindi da `Singe/Menu.rml` il nome `../Collection/cabinet.png` è
|
||
`Collection/cabinet.png` nella cartella dei giochi, e un nome sotto
|
||
`../MyGame.game/` è un file dentro quel gioco impacchettato. Il menu incluso fa
|
||
esattamente questo per la grafica del cabinato e del marquee del gioco
|
||
selezionato, impostando da Lua un decoratore di immagine sul pannello della
|
||
grafica, così l'immagine viene adattata al suo interno qualunque sia la sua
|
||
dimensione:
|
||
|
||
[source,lua]
|
||
----
|
||
-- Menu.singe: adatta l'immagine di un gioco a uno dei pannelli della grafica.
|
||
function menuArt(id, file)
|
||
local path = tostring(file or "")
|
||
if path:sub(1, 6):lower() ~= "singe/" then
|
||
path = "../" .. path
|
||
end
|
||
menuElement(id).style.decorator = 'image("' .. path .. '" contain)'
|
||
end
|
||
----
|
||
|
||
Un documento RML è un `<head>` con un titolo, collegamenti a fogli di stile o uno
|
||
`<style>` in linea, e un `<body>` di elementi. Uno minimo, che usa il tema
|
||
distribuito:
|
||
|
||
[source,html]
|
||
----
|
||
<rml>
|
||
<head>
|
||
<title>Options</title>
|
||
<link type="text/rcss" href="Singe/gui.rcss"/>
|
||
<style>
|
||
body { width: 100%; height: 100%; }
|
||
.panel { margin: 30dp; }
|
||
</style>
|
||
</head>
|
||
<body id="options">
|
||
<div class="panel">
|
||
<h1>Options</h1>
|
||
<p class="muted" id="status">Choose a difficulty and press Start.</p>
|
||
<button id="go">Start</button>
|
||
</div>
|
||
</body>
|
||
</rml>
|
||
----
|
||
|
||
RmlUi non disegna niente di cui non gli sia stata detta la dimensione, e ogni
|
||
elemento è `display: inline` finché uno stile non dice altrimenti, quindi un
|
||
foglio di stile non è facoltativo. `Singe/gui.rcss` è il tema che il motore
|
||
distribuisce: dà al body, ai titoli e ai paragrafi una dimensione e un colore,
|
||
rende `div` un blocco e dà uno stile a ogni controllo dei moduli. Collegalo per
|
||
primo e ridefinisci ciò che vuoi in un foglio tuo o in uno `<style>` in linea;
|
||
vince la regola successiva, come in CSS. Le lunghezze nel tema sono in `dp`, il
|
||
pixel indipendente dalla densità di RmlUi, che è un pixel alla dimensione propria
|
||
della GUI.
|
||
|
||
Il testo ha bisogno di un carattere. Il motore carica `Singe/FreeSansBold.ttf` per
|
||
ogni GUI come carattere *di riserva* (il carattere che fornisce ogni glifo che
|
||
nessun altro carattere ha); il suo nome di famiglia è `FreeSans` e il suo peso è
|
||
grassetto, che è ciò che chiede la regola `body` del tema. `guiLoadFont(name)`
|
||
aggiunge uno dei caratteri del gioco per ogni GUI, e `guiLoadFont(name, true)` lo
|
||
rende invece quello di riserva. Un documento nomina un carattere con il nome di
|
||
famiglia contenuto nel file, non con il nome del file, quindi carica i font prima
|
||
dei documenti che li usano:
|
||
|
||
[source,lua]
|
||
----
|
||
guiLoadFont(DIR .. "fonts/Orbitron-Bold.ttf")
|
||
----
|
||
|
||
[source,css]
|
||
----
|
||
h1 { font-family: Orbitron; font-weight: bold; }
|
||
----
|
||
|
||
Gli effetti RCSS di RmlUi vengono disegnati per intero. I figli di un elemento
|
||
arrotondato o trasformato vengono ritagliati sulla sua forma (un elenco a
|
||
scorrimento con `border-radius` incontra la sua cornice sulla curva),
|
||
`filter: opacity()` dissolve un gruppo come un tutt'uno (la proprietà `opacity`
|
||
dissolve ogni pezzo per conto suo, come la definisce RmlUi), `mask-image` ritaglia
|
||
un elemento secondo l'alfa di un'immagine, `box-shadow` proietta ombre nette o
|
||
morbide, e `filter` e `backdrop-filter` accettano `blur`, `drop-shadow`,
|
||
`brightness`, `contrast`, `invert`, `grayscale`, `sepia`, `hue-rotate` e
|
||
`saturate`. I decoratori `linear-gradient`, `radial-gradient` e `conic-gradient`
|
||
(ripetuti o no) vengono disegnati come tali; i pannelli, i pulsanti e le barre di
|
||
avanzamento del tema distribuito li usano. Tre limiti: il decoratore personalizzato
|
||
`shader` non è supportato (viene rifiutato con un avviso), un gradiente porta al
|
||
massimo sedici punti di colore (quelli in più vengono scartati), e gli effetti si
|
||
annidano al massimo per otto livelli (un elemento più profondo viene disegnato
|
||
senza il proprio livello). Ogni effetto viene disegnato attraverso un livello
|
||
grande quanto la GUI, e una sfocatura attraverso diverse passate, quindi su un Pi
|
||
limitali a pochi elementi e tieni d'occhio <<guigetstats,guiGetStats>>, che conta
|
||
i livelli, le passate e i byte che è costato un fotogramma.
|
||
|
||
[#_theming]
|
||
==== Temi
|
||
|
||
Un tema è una cartella di RCSS e immagini, e un gioco distribuisce il proprio
|
||
aspetto come distribuisce gli sprite. La forma abituale è un foglio collegato dopo
|
||
`Singe/gui.rcss` che ridefinisce lo stile delle classi del tema e aggiunge le
|
||
proprie, con la grafica accanto. Il tema distribuito definisce:
|
||
|
||
* `.panel` -- un riquadro con bordo, arrotondato e traslucido, con margine interno:
|
||
la cornice di una pagina o di una finestra di dialogo.
|
||
* `.list` -- un riquadro a scorrimento i cui elementi `div` figli sono righe:
|
||
prendono il focus, si evidenziano al passaggio del mouse e con il focus, e una
|
||
riga con la classe `selected` resta evidenziata. Una barra di scorrimento
|
||
verticale compare quando le righe non ci stanno.
|
||
* `.muted` -- testo più tenue, per suggerimenti e righe di stato.
|
||
* I controlli dei moduli: `button` e `input.submit`, `input.text`,
|
||
`input.password` e `textarea`, `input.checkbox` e `input.radio`, `input.range`
|
||
con la sua guida e il suo cursore, `select` con la sua freccia e il suo riquadro
|
||
a discesa, e `progress` con il suo riempimento; ognuno con gli stati hover, focus
|
||
e active, e ognuno in grado di ricevere il focus con `tab-index: auto`, così che
|
||
un controller possa raggiungerlo.
|
||
|
||
Un pannello fatto con la tua grafica è un decoratore nine-slice sopra uno sprite
|
||
sheet, e le animazioni sono keyframe RCSS, quindi una barra di caricamento che si
|
||
riempie da sola non ha bisogno di alcuno script:
|
||
|
||
[source,css]
|
||
----
|
||
@spritesheet crate {
|
||
src: crate.png;
|
||
crate-outer: 0px 0px 40px 40px;
|
||
crate-inner: 10px 10px 20px 20px;
|
||
}
|
||
|
||
.crate {
|
||
display: block;
|
||
decorator: ninepatch(crate-outer, crate-inner, 1.0);
|
||
padding: 14dp;
|
||
}
|
||
|
||
@keyframes fill {
|
||
from { width: 0%; }
|
||
to { width: 100%; }
|
||
}
|
||
|
||
.bar fill {
|
||
animation: 3s cubic-in-out infinite alternate fill;
|
||
}
|
||
----
|
||
|
||
[#_scripting_a_document]
|
||
==== Programmare un documento
|
||
|
||
Ci sono due API, e aiuta avere chiaro quale è quale. Le chiamate piatte `gui*`
|
||
nello stile di Singe creano una GUI, caricano e disegnano documenti, instradano
|
||
l'input e raggiungono gli elementi *per id*: `guiGetValue(gui, doc, id)` legge il
|
||
valore di un controllo di un modulo, o l'RML interno di qualsiasi altro elemento,
|
||
come stringa; `guiSetValue(gui, doc, id, value)` ne scrive uno; e `guiSetHandler(gui, doc,
|
||
id, event, function)` chiama una funzione Lua quando l'elemento genera un evento
|
||
(`"click"`, `"change"`, `"submit"` o qualsiasi altro nome di evento RmlUi). La
|
||
funzione riceve `(gui, document, id, event, value)`, dove `value` è il valore
|
||
dell'elemento come stringa al momento dell'evento: il testo di un campo di testo,
|
||
il valore di un'opzione di `select`, un range nella formattazione propria di RmlUi
|
||
(`"48.000000"`), l'RML interno di un pulsante. Tutto ciò di cui una pagina ha
|
||
bisogno ogni giorno è coperto da queste tre chiamate:
|
||
|
||
[source,lua]
|
||
----
|
||
guiSetHandler(gui, doc, "go", "click", function(g, d, id, event, value)
|
||
local name = guiGetValue(g, d, "name")
|
||
local hard = guiGetValue(g, d, "difficulty") == "hard"
|
||
startGame(name, hard)
|
||
end)
|
||
guiSetHandler(gui, doc, "volume", "change", function(g, d, id, event, value)
|
||
soundSetVolume(math.floor(tonumber(value)))
|
||
guiSetValue(g, d, "status", "Volume " .. math.floor(tonumber(value)))
|
||
end)
|
||
----
|
||
|
||
Tutto il resto dentro un documento è l'API Lua di RmlUi, che gira sullo stato Lua
|
||
del gioco stesso: un blocco `<script>` in un documento, e `onclick` e gli altri
|
||
attributi `on*`, girano con le variabili globali del gioco, quindi possono chiamare
|
||
sia le tue funzioni sia le chiamate `gui*`. Dal lato dello script gli stessi
|
||
oggetti sono raggiungibili tramite la variabile globale `rmlui`: i contesti si
|
||
chiamano `gui0`, `gui1`, ... come i loro handle, e i documenti sono indicizzati per
|
||
l’`id` del loro `<body>`, quindi il pulsante qui sopra è
|
||
`rmlui.contexts["gui0"].documents["options"]:GetElementById("go")`, e
|
||
`:DispatchEvent("click", {})` su di esso lo preme da script (utile per test che
|
||
girano senza mouse). Gli elementi, i loro attributi, classi, stili e figli, i
|
||
binding `data-model` che rispecchiano una tabella Lua in un documento, e il resto
|
||
del modello a oggetti sono documentati insieme al plugin Lua di RmlUi su
|
||
https://mikke89.github.io/RmlUiDoc/; `guiSetHandler` è la scorciatoia per id, non
|
||
un sostituto. Un documento chiuso tramite quella API (`document:Close()`) viene
|
||
dimenticato dal motore, e da quel momento il suo handle solleva un errore.
|
||
|
||
[#_input]
|
||
==== Input
|
||
|
||
Una GUI nuova si limita a mostrare, il che va bene per un HUD o un'insegna;
|
||
`guiSetInput(gui, true)` le fa accettare input, e `guiSetInput(gui, false)` lo
|
||
disattiva di nuovo. Per una GUI che accetta input:
|
||
|
||
* *Il mouse e una pistola ottica* (che è un mouse) arrivano in coordinate
|
||
dell'overlay, convertite attraverso il bordo Sinden e la scala di overscan
|
||
esattamente come per `onMouseMoved`, in modalità `MOUSE_SINGLE`. Il motore trova
|
||
la GUI sotto il puntatore -- l'ultima disegnata piatta lì in questo fotogramma
|
||
sta sopra, e in mancanza di questa una superficie nella scena il cui materiale ne
|
||
mostra una (vedi sotto) -- e scala la posizione nei pixel propri di quella GUI.
|
||
Una pressione su un elemento appartiene alla GUI; i pulsanti sinistro, destro e
|
||
centrale fanno clic, e la rotella scorre.
|
||
* *I tasti* vanno a ogni GUI che accetta input, e un tasto che un elemento con il
|
||
focus ha usato (un carattere in un campo di testo, una freccia che ha spostato il
|
||
focus, Invio su un pulsante) viene consumato. Mentre un campo di testo o di
|
||
password o una `textarea` ha il focus, il motore attiva l'input di testo di SDL,
|
||
quindi il testo digitato arriva come testo, nel rispetto del layout di tastiera e
|
||
di eventuali tastiere a schermo o IME.
|
||
* *Gli interruttori del controller e del mouse* (le assegnazioni `SWITCH_*` del
|
||
framework da un pulsante del controller o del mouse) vengono tradotti nei tasti
|
||
che un documento capisce: le quattro direzioni diventano i tasti freccia,
|
||
`INPUT_ACTION_1` è Invio e `INPUT_ACTION_2` è Esc. Un interruttore assegnato a un tasto della
|
||
tastiera raggiungeva già la GUI come quel tasto.
|
||
|
||
La regola che rende sicuro un HUD è che *un evento usato da un elemento non
|
||
raggiunge mai il gioco*: un tasto consumato non arriva come `onInputPressed` o
|
||
`onKeyPressed`, un clic su un pulsante non arriva come interruttore del mouse, una
|
||
direzione tradotta che ha spostato il focus non arriva come `SWITCH_UP`. Tutto ciò
|
||
che un documento non ha usato passa alle solite callback, quindi un HUD che accetta
|
||
input lascia comunque passare il pulsante di fuoco, e una pagina in cui niente ha
|
||
il focus lascia passare ogni tasto.
|
||
|
||
Il focus e la navigazione sono affare del documento. Il tema distribuito dà a ogni
|
||
controllo e a ogni riga di `.list` `tab-index: auto`, così Tab si sposta tra di
|
||
essi, Invio attiva il controllo con il focus, e i tasti freccia si spostano tra i
|
||
controlli con le proprietà `nav` di RmlUi (`nav-up`, `nav-down`, `nav-left`,
|
||
`nav-right`, o `nav: auto` per tutte e quattro). Una pagina per un cabinato con una
|
||
levetta e due pulsanti le vuole su tutto ciò che si aspetta che il giocatore
|
||
raggiunga:
|
||
|
||
[source,css]
|
||
----
|
||
button, .list div { nav: auto; }
|
||
----
|
||
|
||
[#guiscene]
|
||
==== Una GUI nella scena
|
||
|
||
`materialSetGui(material, gui)` mostra la texture di una GUI come texture del
|
||
colore di base di un materiale, aggiornata a ogni fotogramma, accanto a
|
||
`materialSetVideo` e `materialSetView`: un monitor, il pannello di una cabina di
|
||
pilotaggio, un'insegna. Abbinala a `materialSetUnlit` quando l'immagine non deve
|
||
essere ombreggiata, e lasciala illuminata quando deve esserlo. Nient'altro cambia;
|
||
il materiale viene illuminato, fuso e riflesso come qualsiasi altro, e la stessa
|
||
GUI può essere disegnata piatta sopra l'overlay nello stesso fotogramma.
|
||
|
||
`sceneProbeGui(x, y)` risponde quale GUI c'è sulla superficie sotto un punto
|
||
dell'overlay e dove si trova su di essa: un raggio dalla telecamera attraverso il
|
||
punto contro ogni mesh visibile il cui materiale mostra una GUI, che restituisce
|
||
l'handle e il pixel nelle coordinate proprie di quella GUI, o `nil` quando il punto
|
||
è fuori da ogni superficie del genere. Il motore le fa la stessa domanda quando il
|
||
mouse o una pistola ottica punta alla scena e sotto il puntatore non c'è niente
|
||
disegnato piatto, così un clic sul monitor in un cabinato 3D preme il pulsante
|
||
sotto di esso senza alcun intervento dello script; la chiamata c'è per un gioco
|
||
che vuole saperlo da sé, per un mirino laser, un effetto al passaggio del mouse o
|
||
un test.
|
||
|
||
[source,lua]
|
||
----
|
||
local screen = materialNew()
|
||
materialSetGui(screen, gui)
|
||
materialSetUnlit(screen, true)
|
||
nodeSetMesh(monitor, meshPlane(3.0, 2.0), screen)
|
||
nodeSetRotation(monitor, 90, 0, 0)
|
||
----
|
||
|
||
[#_reloading_performance_and_limits]
|
||
==== Ricaricamento, prestazioni e limiti
|
||
|
||
Una GUI appartiene allo script che l'ha creata. L'uscita, `scriptPush`,
|
||
`scriptExecute` e un ricaricamento (vedi <<reloading,Ricaricare mentre lavori>>)
|
||
smontano ogni GUI e ogni documento insieme al resto dello stato dello script, e lo
|
||
script li ricrea quando gira; niente del modello a oggetti `rmlui` sopravvive,
|
||
quindi non tenere quegli oggetti in variabili globali che un ricaricamento
|
||
riutilizzerebbe.
|
||
|
||
Ogni GUI si aggiorna a ogni fotogramma, che sia disegnata da qualche parte o no,
|
||
quindi un documento continua a far girare le sue animazioni e i suoi script fuori
|
||
schermo, e una GUI che non serve più va eliminata invece che semplicemente
|
||
nascosta. Viene ridisegnata nella sua texture solo quando la sua immagine cambia,
|
||
quindi una pagina o un HUD che resta fermo non costa quasi nulla alla GPU (vedi
|
||
<<guirefresh,guiRefresh>>). Il costo di un fotogramma che cambia dipende dalla
|
||
dimensione della texture e dalla complessità del documento: su un desktop una
|
||
pagina a schermo intero costa poco, su un Raspberry Pi tieni una GUI grande quanto
|
||
l'overlay o più piccola e preferisci una GUI con diversi documenti a diverse GUI.
|
||
Ogni carattere costa un atlante per ogni dimensione a cui viene usato, quindi una
|
||
pagina che usa un carattere a tre dimensioni paga tre atlanti; una dozzina di
|
||
dimensioni sono una dozzina. Possono esistere sedici GUI alla volta, ognuna con
|
||
trentadue documenti, e vengono eseguite sedici chiamate `guiDraw` per fotogramma.
|
||
|
||
Ciò che un documento può mostrare, e ciò che non può:
|
||
|
||
* *Le immagini vengono dai file.* Le immagini, gli sprite sheet e i decoratori di
|
||
immagine caricano PNG e gli altri formati che Singe legge, dal disco o da un
|
||
gioco impacchettato. Un documento non può mostrare uno sprite di Singe, un video,
|
||
la scena 3D o qualsiasi cosa disegnata mentre il gioco gira, e uno script non può
|
||
passargli dei pixel. Al contrario funziona: la texture di una GUI va su un
|
||
materiale 3D con `materialSetGui`.
|
||
* *Decoratori:* `image`, `text`, `tiled-horizontal`, `tiled-vertical`,
|
||
`tiled-box`, `ninepatch`, il `gradient` a due colori (e i suoi nomi
|
||
`horizontal-gradient` e `vertical-gradient`), e `linear-gradient`,
|
||
`radial-gradient` e `conic-gradient` con le loro forme `repeating-`, con al
|
||
massimo sedici punti di colore. Il decoratore personalizzato `shader` viene
|
||
rifiutato.
|
||
* *Filtri:* `opacity`, `blur`, `drop-shadow`, `brightness`, `contrast`, `invert`,
|
||
`grayscale`, `sepia`, `hue-rotate` e `saturate`. Qualsiasi altro nome viene
|
||
ignorato con un avviso nella traccia.
|
||
* *Livelli:* i filtri, `opacity` sotto 1, le immagini maschera e le ombre dei
|
||
riquadri vengono disegnati ciascuno attraverso un livello grande quanto la GUI, e
|
||
al massimo otto si impilano insieme; uno in più viene disegnato senza il suo
|
||
livello, e quindi senza il suo effetto.
|
||
* *Le immagini* più grandi di 64 MB vengono rifiutate.
|
||
* *RML e RCSS* sono quelli di RmlUi, vicini a HTML e CSS ma non uguali: c'è
|
||
flexbox e non c'è la griglia CSS, e un documento si programma in Lua, tramite
|
||
`guiSetHandler` e l'oggetto `rmlui`, invece che in JavaScript. La documentazione
|
||
di RmlUi è l'autorità su ciò che contengono i linguaggi.
|
||
|
||
[#_a_worked_example]
|
||
==== Un esempio completo
|
||
|
||
Una pagina di opzioni sopra il disco: un pannello del tema distribuito con un campo
|
||
per il nome, un cursore per il volume, un elenco della difficoltà e due pulsanti,
|
||
disegnata sopra l'overlay e pilotata dal mouse o dalla levetta. Il documento,
|
||
`options.rml`, accanto allo script:
|
||
|
||
[source,html]
|
||
----
|
||
<rml>
|
||
<head>
|
||
<title>Options</title>
|
||
<link type="text/rcss" href="Singe/gui.rcss"/>
|
||
<style>
|
||
body { width: 100%; height: 100%; }
|
||
.panel { margin: 30dp; }
|
||
.field { display: block; margin: 6dp 0; }
|
||
label { display: inline-block; width: 140dp; }
|
||
input.text { width: 300dp; }
|
||
button, input { nav: auto; }
|
||
#status { min-height: 30dp; }
|
||
</style>
|
||
</head>
|
||
<body id="options">
|
||
<div class="panel">
|
||
<h1>Options</h1>
|
||
<div class="field"><label>Name</label><input type="text" id="name" value="Dirk"/></div>
|
||
<div class="field"><label>Volume</label><input type="range" id="volume" min="0" max="63" step="1" value="48"/></div>
|
||
<div class="field"><label>Difficulty</label>
|
||
<select id="difficulty">
|
||
<option value="easy">Easy</option>
|
||
<option value="normal" selected>Normal</option>
|
||
<option value="hard">Hard</option>
|
||
</select>
|
||
</div>
|
||
<div class="field"><label>Attract</label><input type="checkbox" id="attract" checked/> plays the intro</div>
|
||
<div class="field"><button id="go">Start</button><button id="back">Back</button></div>
|
||
<p id="status" class="muted">Press Start when ready.</p>
|
||
</div>
|
||
</body>
|
||
</rml>
|
||
----
|
||
|
||
Lo script lo carica in una GUI grande quanto l'overlay, collega i tre elementi che
|
||
gli interessano e lo disegna a ogni fotogramma finché il giocatore non preme
|
||
Start; il disco viene riprodotto sotto per tutto il tempo:
|
||
|
||
[source,lua]
|
||
----
|
||
dofile("Singe/Framework.singe")
|
||
|
||
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
|
||
local doc = guiLoad(gui, DIR .. "options.rml")
|
||
local settings = { name = "Dirk", volume = 48, hard = false }
|
||
local inMenu = true
|
||
|
||
local function report(text)
|
||
guiSetValue(gui, doc, "status", text)
|
||
end
|
||
|
||
guiSetHandler(gui, doc, "volume", "change", function(g, d, id, event, value)
|
||
settings.volume = math.floor(tonumber(value))
|
||
soundSetVolume(settings.volume)
|
||
report("Volume " .. settings.volume)
|
||
end)
|
||
|
||
guiSetHandler(gui, doc, "difficulty", "change", function(g, d, id, event, value)
|
||
settings.hard = (value == "hard")
|
||
report("Difficulty " .. value)
|
||
end)
|
||
|
||
guiSetHandler(gui, doc, "go", "click", function(g, d)
|
||
settings.name = guiGetValue(g, d, "name")
|
||
guiHide(g, d)
|
||
guiSetInput(g, false)
|
||
inMenu = false
|
||
discPlay()
|
||
end)
|
||
|
||
guiSetHandler(gui, doc, "back", "click", function()
|
||
singeQuit()
|
||
end)
|
||
|
||
function onInputPressed(what)
|
||
-- Qui arriva solo ciò che la pagina non ha usato: niente finché ha il
|
||
-- focus, tutto una volta che è nascosta.
|
||
if not inMenu and what == SWITCH_BUTTON1 then
|
||
fire()
|
||
end
|
||
end
|
||
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
if inMenu then
|
||
guiDraw(gui)
|
||
else
|
||
fontPrint(10, 10, settings.name .. " " .. (settings.hard and "HARD" or "NORMAL"))
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
La stessa pagina sullo schermo di un cabinato modellato si ottiene con le cinque righe
|
||
di <<guiscene,Una GUI nella scena>> al posto di `guiDraw`, e un clic sul monitor
|
||
preme il pulsante sotto di esso.
|
||
|
||
[#subtitles]
|
||
=== Sottotitoli
|
||
|
||
Un gioco può distribuire un file di sottotitoli SubRip e farlo mostrare da Singe sopra il disco mentre viene riprodotto. Tre chiamate fanno tutto il lavoro: <<srtload,`srtLoad`>> legge il file, <<srtenable,`srtEnable`>> lo attiva e <<srtposition,`srtPosition`>> dice dove stanno le righe. <<srtclear,`srtClear`>> toglie di nuovo qualunque cosa sia visibile. La funzionalità viene da Hypseus Singe, e le quattro chiamate sono le sue.
|
||
|
||
[source,lua]
|
||
----
|
||
if srtLoad("subs/english.srt") then
|
||
srtPosition(80)
|
||
srtEnable(true)
|
||
end
|
||
----
|
||
|
||
[#_the_file_format]
|
||
==== Il formato del file
|
||
|
||
SubRip (`.srt`) è un file di testo semplice fatto di battute numerate:
|
||
|
||
----
|
||
1
|
||
00:00:04,120 --> 00:00:06,880
|
||
Stay away from the caves.
|
||
|
||
2
|
||
00:00:07,000 --> 00:00:09,500
|
||
I mean it -- the caves
|
||
are where they nest.
|
||
----
|
||
|
||
Ogni battuta è un numero su una riga a sé, poi una riga che contiene due marche temporali separate da `-->`, poi una o più righe di testo, poi una riga vuota. Una marca temporale è `HH:MM:SS,mmm`: ore, minuti e secondi separati da due punti, e i millisecondi dopo una virgola. Il numero della battuta viene letto e scartato, quindi i numeri non devono essere in ordine e nemmeno corretti.
|
||
|
||
Il parser è volutamente tollerante nei punti in cui un file modificato a mano va storto. I ritorni a capo (carriage return), gli spazi iniziali e quelli finali vengono tolti da ogni riga, quindi un file scritto su Windows si carica senza modifiche. Una battuta la cui riga dei tempi non ha `-->`, o le cui marche temporali non si riescono a interpretare, viene saltata e il caricamento del file continua. Una battuta senza testo viene saltata. Le righe di una battuta vengono unite con degli a capo e mostrate come sono scritte, e `<`, `>` e `&` arrivano sullo schermo come tali invece di essere presi per markup.
|
||
|
||
Ciò che *non* è supportato: le coordinate di posizionamento che alcuni strumenti aggiungono alla riga dei tempi vengono ignorate (`srtPosition` decide l'altezza), e gli occasionali tag `<i>` e `<b>` di SubRip vengono mostrati come testo invece di essere rispettati. Le battute possono sovrapporsi nel tempo; vince l'ultima che è cominciata.
|
||
|
||
`srtLoad` risponde `false` e non carica nulla quando il file non si può leggere, quando niente al suo interno si riesce a interpretare o quando non c'è un disco -- quindi un file rotto è un `false` su cui un gioco può agire, e non un crash. In ogni caso stampa un avviso che nomina il file.
|
||
|
||
[#_the_timing]
|
||
==== La tempistica
|
||
|
||
Le marche temporali vengono trasformate in *numeri di fotogramma del disco* quando il file viene caricato, usando la frequenza dei fotogrammi del disco caricato in quel momento, e il motore confronta ogni fotogramma disegnato con quei numeri di fotogramma. Ne seguono due cose.
|
||
|
||
Primo, **un sottotitolo sopravvive a un salto.** Mandare il disco ovunque -- `discSearch`, `discSkipForward`, un passo, un framefile che passa in un altro segmento -- mostra la battuta che copre il punto in cui è arrivato, al fotogramma disegnato successivo, in avanti o all'indietro. Non bisogna ricaricare niente né azzerare alcun orologio.
|
||
|
||
Secondo, **il file deve essere temporizzato sul disco, non sull'orologio.** Un `.srt` preparato per un video con una frequenza dei fotogrammi diversa finisce nel posto sbagliato, e uno caricato prima che il disco sia stato aperto non si carica affatto. Carica i sottotitoli dopo il disco, il che per un gioco normale significa dal corpo dello script.
|
||
|
||
Una battuta viene mostrata finché il fotogramma del disco è compreso tra i suoi due fotogrammi, estremi inclusi. Il sottotitolo viene ridisegnato solo quando la battuta cambia, quindi i sottotitoli non costano nulla mentre una riga è visibile.
|
||
|
||
[#_styling]
|
||
==== Stile
|
||
|
||
Le righe sono un documento RmlUi, non una riga di testo copiata con un blit, quindi ricevono lo stile come una GUI. Due file vengono distribuiti nella cartella `Singe` e vengono estratti accanto agli altri al primo avvio:
|
||
|
||
* `Singe/subtitle.rml` -- il documento. Ha un solo elemento, `#slot`, in cui il motore scrive la battuta o il banner attuale.
|
||
* `Singe/subtitle.rcss` -- l'aspetto. `.cue` è il blocco che porta l'altezza chiesta da `srtPosition`, come `top` in linea; `.cue .text` è la barra di testo al suo interno, che per impostazione predefinita è bianca su un pannello scuro arrotondato che si stringe attorno al testo invece di attraversare l'immagine.
|
||
|
||
Entrambi vengono riscritti da capo a ogni avvio, come tutto ciò che sta nella cartella `Singe`, quindi modificarli non cambia nulla a lungo. Per cambiare lo stile, un gioco distribuisce le proprie copie: `subtitle.rml` accanto al suo script, con il collegamento a un foglio tutto suo (`subtitle.rcss` accanto a esso, per esempio), e Singe disegna quel documento al posto del proprio; la traccia del programma lo dice. Il motore imposta sempre e solo il contenuto di `#slot` e il `top` di `.cue`, quindi tutto il resto -- il carattere, la dimensione, i colori, il pannello, un bordo, un'ombra -- appartiene al foglio del gioco e sopravvive a un aggiornamento del motore. Il documento viene disegnato sopra l'immagine, sopra l'overlay e sotto le GUI nello spazio dello schermo, segue il rettangolo del video ovunque lo mettano `--scalefactor`, `--shiftx`, un ritaglio del bezel (la cornice decorativa) o `--rotate`, e non accetta input dal mouse, quindi un clic lo attraversa direttamente.
|
||
|
||
<<overlaybanner,`overlayBanner`>> usa lo stesso documento, quindi un banner e un sottotitolo hanno lo stesso aspetto, e un gioco che cambia lo stile dell'uno lo cambia a entrambi.
|
||
|
||
Su una macchina senza GPU non ci sono affatto GUI; `srtLoad` interpreta comunque il file e risponde in modo veritiero, e non viene disegnato nulla. Un avviso lo dice una volta.
|
||
|
||
[#scenes3d]
|
||
=== Scene 3D
|
||
|
||
Singe 3 disegna una scena 3D tra il video del laserdisc e l'overlay 2D. Tutto ciò
|
||
che sai già vale ancora: gli sprite, il testo e l'overlay stanno sopra la scena, e
|
||
il video si vede ovunque lo sfondo della scena sia trasparente. Un gioco che non
|
||
chiama mai una funzione 3D non ne è toccato, e un gioco senza disco (vedi
|
||
<<withoutadisc,Giochi senza disco>>) può essere un gioco 3D a tutti gli effetti.
|
||
|
||
Le sezioni qui sotto costruiscono una scena nell'ordine in cui ne scriverai una:
|
||
i nodi, poi le mesh e i materiali, le luci, la telecamera, l'aspetto del
|
||
fotogramma nel suo insieme, i modelli e infine le cose che stanno sopra tutto
|
||
questo. Ogni chiamata nominata qui ha una voce nel
|
||
<<reference,Riferimento dell'API Lua>>.
|
||
|
||
[#_what_a_scene_is_made_of]
|
||
==== Di che cosa è fatta una scena
|
||
|
||
Tutto in una scena è un *nodo* in un unico albero, e il nodo `0` è la radice. Un
|
||
nodo ha una posizione, una rotazione e una scala relative al suo genitore, e può
|
||
portare una sola cosa: una mesh con un materiale, una luce, uno sprite o una riga
|
||
di testo. Un modello caricato diventa un sottoalbero di nodi sotto un'unica radice,
|
||
quindi le stesse chiamate muovono allo stesso modo un personaggio modellato e un
|
||
cubo creato da script.
|
||
|
||
La scena è destrorsa, con +Y in alto e -Z in avanti, come glTF e l'esportazione di
|
||
Blender. Le unità sono quelle che usano i tuoi modelli, e le primitive usano le
|
||
stesse unità; la fisica (vedi <<physics,Fisica>>) presuppone i metri. Le rotazioni
|
||
sono in gradi, date come tre angoli di Eulero applicati come imbardata, beccheggio
|
||
e rollio, o come quaternione quando il valore viene da un modello. L'avanti proprio
|
||
di un nodo è la sua -Z: la telecamera guarda lungo di essa, le luci direzionali e
|
||
spot illuminano lungo di essa, e `nodeLookAt` la gira verso un punto del mondo.
|
||
|
||
Ci sono due modi per riempire una scena, liberamente mescolabili:
|
||
|
||
* *Costruirla da script* con le mesh primitive (`meshBox`, `meshSphere`,
|
||
`meshPlane`, `meshCylinder`, `meshCone`, `meshTorus`, `meshHeightmap`) o con i
|
||
tuoi dati dei vertici (`meshNew`), più materiali, luci e una telecamera.
|
||
* *Caricare modelli* in formato binario glTF 2.0 (`.glb`) con `modelLoad`, poi
|
||
collocarli con `modelInstance` tutte le volte che vuoi. Blender, Maya, 3ds Max e
|
||
tutti i motori principali esportano `.glb`.
|
||
|
||
Una scena minima è una mesh, una luce e una telecamera:
|
||
|
||
[source,lua]
|
||
----
|
||
sceneEnable(true)
|
||
sceneSetBackground(0, 0, 0, 0) -- Trasparente: il disco si vede dietro la scena.
|
||
|
||
local red = materialNew()
|
||
materialSetColor(red, 220, 40, 40)
|
||
|
||
local cube = nodeNew()
|
||
nodeSetMesh(cube, meshBox(1, 1, 1), red)
|
||
|
||
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||
nodeSetPosition(sun, 2, 4, 3)
|
||
nodeLookAt(sun, 0, 0, 0)
|
||
|
||
local camera = nodeNew()
|
||
nodeSetPosition(camera, 0, 1.5, 5)
|
||
nodeLookAt(camera, 0, 0, 0)
|
||
cameraSet(camera)
|
||
|
||
function onOverlayUpdate()
|
||
nodeRotate(cube, 0, 1, 0) -- Un grado per fotogramma attorno alla propria Y.
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
`sceneEnable(true)` è la chiamata che ogni gioco 3D fa, e su una macchina che non
|
||
può disegnare in 3D è la chiamata che fallisce (vedi
|
||
<<sceneperformance,Prestazioni e requisiti>>). Senza telecamera la scena è vista da
|
||
`(0, 0, 5)` guardando verso l'origine, e senza una chiamata per lo sfondo lo sfondo
|
||
della scena è nero opaco.
|
||
|
||
[#nodes]
|
||
==== Nodi
|
||
|
||
La trasformazione di un nodo si imposta direttamente (`nodeSetPosition`,
|
||
`nodeSetRotation`, `nodeSetQuaternion`, `nodeSetScale`) o si ritocca (`nodeMove`,
|
||
`nodeRotate`), sempre nello spazio del genitore. `nodeSetParent` appende un nodo a
|
||
un altro genitore, mantenendo la sua trasformazione locale; `nodeGetWorldPosition`
|
||
legge dove è finito dopo che ogni genitore ha detto la sua. `nodeSetVisible`
|
||
nasconde un nodo e tutto ciò che sta sotto di esso, e `nodeDelete` libera lui e i
|
||
suoi figli, insieme a qualunque corpo, emettitore o suono collegato a essi.
|
||
|
||
I nomi vengono dai modelli: `nodeFind(name, root)` trova un giunto o una parte per
|
||
il nome che aveva nel file, sotto l'istanza data come `root`. Anche un nodo creato
|
||
da script può avere un nome (`nodeSetName`), ed è lì che cerca `nodeFind` senza una
|
||
radice.
|
||
|
||
[source,lua]
|
||
----
|
||
-- Una torretta su un carro armato: la canna è figlia della torretta, la torretta dello scafo,
|
||
-- quindi girare la torretta porta con sé la canna e muovere lo scafo le porta entrambe.
|
||
local hull = nodeNew()
|
||
local turret = nodeNew()
|
||
local barrel = nodeNew()
|
||
nodeSetMesh(hull, meshBox(2, 0.8, 3.2), olive)
|
||
nodeSetMesh(turret, meshCylinder(0.7, 0.5, 16), olive)
|
||
nodeSetMesh(barrel, meshCylinder(0.1, 1.8, 8), steel)
|
||
nodeSetParent(turret, hull)
|
||
nodeSetParent(barrel, turret)
|
||
nodeSetPosition(turret, 0, 0.65, 0)
|
||
nodeSetPosition(barrel, 0, 0, -1.2)
|
||
nodeSetRotation(barrel, 90, 0, 0) -- Stendi il cilindro lungo -Z.
|
||
|
||
function onOverlayUpdate()
|
||
local stick = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_X) / 32767 -- Da -1 a 1.
|
||
nodeRotate(turret, 0, stick * 2, 0)
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
Due chiamate fanno da ponte tra la scena e l'overlay. `sceneProject(x, y, z)`
|
||
trasforma un punto del mondo in coordinate dell'overlay, così un'etichetta con il
|
||
nome o una barra della salute disegnata con `fontPrint` può seguire un oggetto 3D.
|
||
`sceneUnproject(sx, sy, distance)` fa il contrario: dà il punto del mondo a quella
|
||
distanza lungo il raggio che passa per un pixel dell'overlay, il che, insieme a
|
||
`physicsRaycast`, è la selezione con il mouse.
|
||
|
||
[source,lua]
|
||
----
|
||
function onMouseMoved(x, y, dx, dy, which)
|
||
-- Il mouse punta lungo un raggio dalla telecamera; ciò che colpisce è il bersaglio.
|
||
local cx, cy, cz = nodeGetWorldPosition(camera)
|
||
local fx, fy, fz = sceneUnproject(x, y, 50)
|
||
local hit = physicsRaycast(cx, cy, cz, fx - cx, fy - cy, fz - cz, 50)
|
||
highlighted = hit
|
||
end
|
||
----
|
||
|
||
[#_meshes_and_materials]
|
||
==== Mesh e materiali
|
||
|
||
Una mesh è geometria; un materiale è il suo aspetto; un nodo ne unisce uno di
|
||
ciascuno (`nodeSetMesh(node, mesh, material)`). Condividili entrambi liberamente:
|
||
un solo `meshBox` e un solo materiale possono stare su mille nodi, ed è questa
|
||
condivisione che rende veloce il renderer (vedi
|
||
<<sceneperformance,Prestazioni e requisiti>>). `meshNew` costruisce una mesh da
|
||
tabelle di posizioni, normali, coordinate di texture e indici quando le primitive
|
||
non bastano.
|
||
|
||
Un materiale parte da un colore di base con alfa (`materialSetColor`) e
|
||
dall'aspetto metallic-roughness di glTF: `materialSetMetallic` e
|
||
`materialSetRoughness` da `0` a `1`. Una rugosità vicina a `0` dà un punto luce
|
||
nitido come in uno specchio e `1` una superficie opaca; un metallo tinge con il
|
||
proprio colore i punti luce e i riflessi e non ha un colore diffuso suo. Oltre a
|
||
questo:
|
||
|
||
* `materialSetEmissive` fa emettere a una superficie luce del suo colore (uno
|
||
schermo, una lampada, metallo rovente); non illumina altre cose, ma produce
|
||
bloom (vedi <<scenelook,L'aspetto del fotogramma>>).
|
||
* `materialSetUnlit` mostra il colore così com'è, ignorando ogni luce: elementi
|
||
dell'HUD nella scena, il bagliore intrinseco del vetro di una lampadina.
|
||
* `materialSetDoubleSided` disegna il retro delle forme aperte, come un piano
|
||
visto da sotto o una bandiera.
|
||
* `materialSetBlend` fa contare l'alfa, per vetro, fantasmi e acqua. Le mesh con
|
||
fusione vengono disegnate dopo tutto ciò che è opaco, ordinate per distanza, e
|
||
non proiettano ombre.
|
||
* `materialSetCutoff` tiene solo i texel il cui alfa raggiunge la soglia e butta
|
||
via il resto, ed è così che si disegnano foglie, recinzioni, grate e reti
|
||
metalliche: un solo quad, in gran parte sparito. A differenza della fusione non
|
||
richiede ordinamento e proietta comunque un'ombra, e anche l'ombra viene
|
||
ritagliata, così un albero proietta ombre di foglie invece dell'ombra del suo
|
||
quad. Ciò che sopravvive alla soglia è opaco: un texel semitrasparente viene
|
||
tenuto intero o scartato, mai fuso, quindi niente dietro la scena (l'immagine del
|
||
disco, soprattutto) tinge una foglia.
|
||
Una soglia di 0 (il valore predefinito) disattiva il mascheramento. Un modello
|
||
caricato da glTF porta la sua: `MASK` imposta la soglia dal file, `BLEND` attiva
|
||
la fusione e `OPAQUE` non fa nessuna delle due cose.
|
||
|
||
La texture di un materiale può essere l'immagine di qualsiasi sprite caricato
|
||
(`materialSetTexture`), il laserdisc stesso o un video caricato
|
||
(`materialSetVideo`), l'immagine di una seconda telecamera (`materialSetView`, vedi
|
||
<<scenecamera,La telecamera e le viste>>), o un documento GUI (`materialSetGui`,
|
||
vedi <<gui,GUI>>). Un cabinato modellato può riprodurre il disco sul proprio
|
||
schermo, o mostrarci la sua pagina di opzioni. Le coordinate di texture vengono
|
||
dalla mesh; le primitive stendono un'immagine una volta su ogni faccia, e
|
||
`materialSetTiling` la ripete, che è ciò che vogliono il terreno e i muri. Le
|
||
texture hanno le mipmap e sono filtrate in modo anisotropo, così un pavimento resta
|
||
nitido in lontananza; `materialSetFilter(material, FILTER_NEAREST)` mantiene invece
|
||
squadrata la pixel art.
|
||
|
||
[source,lua]
|
||
----
|
||
local stone = materialNew()
|
||
materialSetTexture(stone, spriteLoad(DIR .. "flagstones.png"))
|
||
materialSetTiling(stone, 12, 12) -- Dodici ripetizioni sul piano.
|
||
materialSetRoughness(stone, 0.9)
|
||
|
||
local floor = nodeNew()
|
||
nodeSetMesh(floor, meshPlane(24, 24), stone)
|
||
|
||
local glass = materialNew()
|
||
materialSetColor(glass, 180, 220, 255, 90) -- Alfa 90 su 255.
|
||
materialSetBlend(glass, true)
|
||
materialSetRoughness(glass, 0.05)
|
||
----
|
||
|
||
Altre quattro mappe danno a una superficie un dettaglio che la sua geometria non
|
||
ha, e un modello caricato le porta con sé quando il suo file le contiene:
|
||
|
||
* `materialSetNormalMap` piega l'illuminazione con una mappa delle normali nello
|
||
spazio tangente, così i mattoni sporgono da un muro piatto. Un'intensità sopra
|
||
`1` la esagera.
|
||
* `materialSetOcclusionMap` scurisce le fessure che la luce ambiente e il cielo non
|
||
raggiungono.
|
||
* `materialSetMetallicRoughnessMap` fa variare metallicità e rugosità sulla
|
||
superficie, combinate alla maniera di glTF (rugosità nel verde, metallicità nel
|
||
blu) e moltiplicate per i fattori propri del materiale, quindi imposta questi a
|
||
`1` quando deve comandare la mappa.
|
||
* `materialSetEmissiveMap` dice quali parti brillano, tinte e scalate da
|
||
`materialSetEmissive`.
|
||
|
||
[source,lua]
|
||
----
|
||
local brick = materialNew()
|
||
materialSetTexture(brick, spriteLoad(DIR .. "brick.png"))
|
||
materialSetNormalMap(brick, spriteLoad(DIR .. "brickNormal.png"), 1.5)
|
||
materialSetMetallicRoughnessMap(brick, spriteLoad(DIR .. "brickMetalRough.png"))
|
||
materialSetMetallic(brick, 1)
|
||
materialSetRoughness(brick, 1)
|
||
materialSetEmissiveMap(brick, spriteLoad(DIR .. "brickEmissive.png"))
|
||
materialSetEmissive(brick, 180, 100, 40) -- La malta brilla d'arancione.
|
||
----
|
||
|
||
.Texture compresse
|
||
Un PNG viene decodificato in quattro byte per texel sulla GPU, più un altro terzo
|
||
per le sue mipmap; un livello grande quanto Sponza occupa così 390 MB di texture,
|
||
più di quanto un Raspberry Pi 4 condivida con lo schermo. Le texture distribuite
|
||
come file KTX2 (Basis Universal, creati da `util/compressTextures.py` a partire da
|
||
PNG o da un intero `.gltf`) restano compresse sulla GPU a un quarto della
|
||
dimensione o meno, in qualunque formato a blocchi abbia la macchina: BC7 sui
|
||
desktop, ASTC sulle GPU Apple e sul Pi 4, ETC2 su qualsiasi chip OpenGL ES 3 (il
|
||
Mali di ogni console portatile, il Pi sotto GLES), BC3 sulle schede più vecchie e
|
||
RGBA semplice dove non esiste altro, tutto dallo stesso file. Dai un nome `.ktx2`
|
||
al posto di uno sprite a `materialSetTexture` e alle chiamate delle mappe, e un
|
||
modello il cui file usa `KHR_texture_basisu` le carica da solo. `sceneGetStats`
|
||
riporta la memoria delle texture in uso, così puoi vedere la differenza.
|
||
|
||
[source,lua]
|
||
----
|
||
materialSetTexture(ground, DIR .. "flagstones.ktx2")
|
||
local draws, drawn, calls, textureKB = sceneGetStats()
|
||
debugPrint("textures: " .. textureKB .. " KB")
|
||
----
|
||
|
||
[#_lights_and_shadows]
|
||
==== Luci e ombre
|
||
|
||
Possono illuminare insieme fino a otto luci, e ognuna è un nodo, quindi le luci si
|
||
appendono a un genitore e si animano come qualsiasi altra cosa:
|
||
|
||
* `LIGHT_DIRECTIONAL` è un sole: conta solo la sua direzione, e tutto è illuminato
|
||
allo stesso modo. Un'intensità di `1` porta al bianco una superficie bianca
|
||
rivolta verso di essa.
|
||
* `LIGHT_POINT` è una lampadina, che si attenua con la distanza, e `lightSetRange`
|
||
la spegne del tutto oltre una certa distanza. La sua intensità è la sua
|
||
luminosità nella lampadina stessa, la metà a un'unità di distanza, quindi una lampadina che deve illuminare una
|
||
stanza ha bisogno di un numero nell'ordine delle decine.
|
||
* `LIGHT_SPOT` è un cono con un angolo interno completamente illuminato e un
|
||
angolo esterno dove la luce si è attenuata fino a sparire (`lightSetCone`).
|
||
|
||
`sceneSetAmbient` aggiunge un po' di luce da ogni direzione così le ombre non sono
|
||
nere; un cielo lo fa meglio (vedi <<scenelook,L'aspetto del fotogramma>>). I
|
||
colori dati da `0` a `255` sono sRGB, come tutto il resto in Singe, e vengono
|
||
convertiti in luce lineare all'ingresso, quindi una luce di `128, 128, 128` è
|
||
luminosa un quarto del bianco, non la metà.
|
||
|
||
Qualsiasi luce può proiettare ombre dai bordi morbidi da tutto ciò che è opaco
|
||
(`lightSetShadow`). Le luci direzionali e spot proiettano tramite una mappa delle
|
||
ombre; le luci puntiformi tramite una mappa cubica che raggiunge ogni direzione,
|
||
quindi una lampadina può stare dentro ciò che illumina (una stanza, un paralume, il
|
||
cabinato): le mesh chiuse proiettano anche dalle loro facce posteriori, e
|
||
l'intervallo di profondità dell'ombra viene adattato agli oggetti che proiettano
|
||
ombra attorno alla luce. `nodeSetShadow(node, false)` dispensa una mesh dal
|
||
proiettare: il vetro della lampadina stessa, un'insegna luminosa, un'etichetta con
|
||
il nome. `sceneSetShadowSize` imposta la risoluzione della mappa (da `256` a
|
||
`4096`, predefinita `1024`) e scambia nitidezza con velocità.
|
||
|
||
L'ombra di una luce direzionale è divisa in cascate lungo la vista
|
||
(`sceneSetShadowCascades`, tre per impostazione predefinita), ognuna una mappa
|
||
adattata a una fetta di ciò che vede la telecamera, così in un livello grande le
|
||
ombre restano nitide da vicino e arrivano comunque fino a `sceneSetShadowDistance`.
|
||
Una sola cascata adatta un'unica mappa all'intera scena, il che va bene per scene
|
||
piccole e telecamere ortografiche. Una vista (`viewNew`) disegna con le stesse
|
||
mappe, prendendo per ogni punto la cascata più fine che lo copre, così un monitor
|
||
mostra le stesse ombre della finestra.
|
||
|
||
[source,lua]
|
||
----
|
||
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||
nodeSetPosition(sun, 30, 40, 20)
|
||
nodeLookAt(sun, 0, 0, 0)
|
||
lightSetIntensity(sun, 2.5)
|
||
lightSetColor(sun, 255, 240, 220)
|
||
lightSetShadow(sun, true)
|
||
sceneSetShadowSize(2048)
|
||
sceneSetShadowDistance(80) -- Tre cascate fino a 80 unità.
|
||
|
||
local lamp = lightNew(LIGHT_POINT)
|
||
nodeSetPosition(lamp, 3, 3.1, -2)
|
||
lightSetColor(lamp, 255, 230, 190)
|
||
lightSetIntensity(lamp, 12)
|
||
lightSetRange(lamp, 14)
|
||
lightSetShadow(lamp, true) -- Una mappa cubica: sei passate per fotogramma.
|
||
nodeSetShadow(bulbGlass, false) -- Il vetro attorno non proietta nulla.
|
||
----
|
||
|
||
[#scenecamera]
|
||
==== La telecamera e le viste
|
||
|
||
Qualsiasi nodo può essere la telecamera (`cameraSet`); guarda lungo la propria -Z,
|
||
quindi `nodeLookAt` la punta, e appenderla sotto un nodo in movimento crea una
|
||
telecamera che insegue. `cameraSetPerspective(fovDegrees, near, far)` è l'obiettivo
|
||
predefinito; `cameraSetOrthographic(height, near, far)` disegna senza prospettiva,
|
||
per diagrammi, scene simili a un HUD e giochi visti di lato. Tieni `near` grande
|
||
quanto la scena consente, perché la precisione del buffer di profondità si spende
|
||
tra `near` e `far`.
|
||
|
||
[source,lua]
|
||
----
|
||
local camera = nodeNew()
|
||
nodeSetParent(camera, car)
|
||
nodeSetPosition(camera, 0, 2.5, 7) -- Dietro e sopra l'auto, nel suo spazio.
|
||
nodeLookAt(camera, 0, 1, 0)
|
||
cameraSet(camera)
|
||
cameraSetPerspective(55, 0.1, 300)
|
||
----
|
||
|
||
Una *vista* è una seconda telecamera disegnata in una texture a ogni fotogramma e
|
||
mostrata su un materiale: un monitor di sorveglianza, uno specchietto retrovisore,
|
||
un portale, un picture in picture. `viewNew(width, height)` ne crea una a quella
|
||
risoluzione, `viewSetCamera` le dà un nodo telecamera, e `materialSetView` mette la
|
||
sua immagine su qualsiasi mesh. Le viste condividono le ombre del fotogramma e
|
||
saltano il bloom, e ognuna disegna di nuovo la scena, quindi tienile poche e
|
||
piccole. Ogni vista gira i billboard (vedi
|
||
<<scenesprites,Sprite e testo nella scena>>) verso la propria telecamera e disegna
|
||
da essa i materiali trasparenti da dietro in avanti, così un monitor vede le
|
||
etichette con il nome di fronte e il vetro nell'ordine giusto.
|
||
|
||
[source,lua]
|
||
----
|
||
local rearCamera = nodeNew()
|
||
nodeSetParent(rearCamera, car)
|
||
nodeSetPosition(rearCamera, 0, 1.2, -2)
|
||
nodeSetRotation(rearCamera, 0, 180, 0) -- Guarda indietro.
|
||
local rear = viewNew(384, 256)
|
||
viewSetCamera(rear, rearCamera)
|
||
|
||
local mirror = materialNew()
|
||
materialSetView(mirror, rear)
|
||
materialSetUnlit(mirror, true)
|
||
local panel = nodeNew()
|
||
nodeSetParent(panel, car)
|
||
nodeSetMesh(panel, meshPlane(0.6, 0.2), mirror)
|
||
nodeSetPosition(panel, 0, 1.6, 0.5)
|
||
nodeSetRotation(panel, 90, 0, 0) -- Il piano guarda in alto; raddrizzalo verso il guidatore.
|
||
----
|
||
|
||
[#scenelook]
|
||
==== L'aspetto del fotogramma
|
||
|
||
La scena è illuminata in luce lineare. I colori che dai da `0` a `255` sono sRGB e
|
||
vengono decodificati all'ingresso, le texture vengono decodificate dalla GPU mentre
|
||
vengono campionate, le luci si sommano senza saturare, e alla fine del fotogramma
|
||
una curva tonale riporta il risultato allo schermo. Che cosa significa in pratica:
|
||
|
||
* `sceneSetExposure(stops)` schiarisce o scurisce l'intero fotogramma come
|
||
l'esposizione di una macchina fotografica: `+1` raddoppia la luce, `-1` la
|
||
dimezza, così una grotta e una spiaggia possono condividere le stesse luci e gli
|
||
stessi materiali.
|
||
* `sceneSetTonemap` sceglie la curva: `TONEMAP_NEUTRAL` (il valore predefinito, che
|
||
tiene i colori dove li tengono i visualizzatori glTF e comprime solo la luce più
|
||
intensa), `TONEMAP_ACES` (un aspetto cinematografico con più contrasto) o
|
||
`TONEMAP_NONE` (taglia al bianco).
|
||
* `sceneSetBloom(threshold, strength)` fa brillare tutto ciò che è più luminoso
|
||
della soglia: il sole attraverso una finestra, una lampadina, un'insegna
|
||
emissiva, una fiamma. Il bagliore viene preso dalla luce della scena prima della
|
||
curva tonale, così fa bloom solo ciò che è davvero luminoso; una soglia di `1` è
|
||
il bianco.
|
||
|
||
.Il cielo e la sua luce
|
||
`sceneSetSky(file)` avvolge la scena in un panorama equirettangolare. Un `.hdr`
|
||
Radiance mantiene tutta la sua gamma; funziona anche un PNG o un JPEG. Il cielo
|
||
viene disegnato dietro a tutto e, a meno di `sceneSetEnvironment(false)`, illumina
|
||
anche la scena: il suo colore complessivo da ogni direzione sostituisce la luce
|
||
ambiente uniforme, e il suo riflesso si vede sui metalli e sulle superfici lucide,
|
||
nitido su uno specchio e sfocato man mano che la rugosità aumenta. Senza un cielo,
|
||
i metalli non hanno niente da riflettere e solo la luce ambiente e le luci danno
|
||
loro forma. `sceneSetSkyIntensity` scala insieme il cielo e la sua luce, così un
|
||
solo panorama serve per mezzogiorno e per il tramonto.
|
||
|
||
.Nebbia
|
||
`sceneSetFog(r, g, b, near, far)` fa sfumare tutto, particelle comprese, verso un
|
||
colore tra due distanze. Il cielo non viene mai annebbiato, quindi abbina il
|
||
colore della nebbia all'orizzonte del cielo e il bordo lontano della scena sparisce
|
||
dentro di esso.
|
||
|
||
[source,lua]
|
||
----
|
||
sceneSetSky(DIR .. "harbor.hdr")
|
||
sceneSetSkyIntensity(0.8)
|
||
sceneSetFog(190, 200, 215, 40, 160) -- Il colore del cielo all'orizzonte.
|
||
sceneSetExposure(-0.5) -- Un po' più scuro di quanto lo renderebbe il sole.
|
||
sceneSetBloom(1.0, 0.35) -- Brilla solo ciò che è più luminoso del bianco.
|
||
----
|
||
|
||
[#_models_and_animation]
|
||
==== Modelli e animazione
|
||
|
||
`modelLoad` prende un `.glb` con lo stesso nome relativo al gioco che prende
|
||
`spriteLoad`, e un `.game` impacchettato lo porta con sé. Il file deve essere
|
||
autosufficiente: mesh, skin, animazioni e texture tutte dentro lo stesso file, che
|
||
è l'esportazione predefinita di Blender. Un modello che fa riferimento a un `.bin` o
|
||
a un'immagine accanto a sé viene rifiutato con un messaggio che nomina il file, per
|
||
la stessa ragione per cui un gioco impacchettato deve essere autosufficiente. I
|
||
materiali usano il modello metallic-roughness con `KHR_materials_emissive_strength`
|
||
e `KHR_texture_basisu`; una texture indirizzata tramite un secondo insieme di coordinate di texture legge il
|
||
primo insieme, e `KHR_texture_transform` viene ignorato, ognuno con una riga nella
|
||
traccia del programma. Una skin può avere fino a `128` giunti. Il caricamento è la
|
||
parte costosa; `modelInstance` colloca poi una copia dell'albero di nodi del
|
||
modello, tutte le volte che vuoi, e ogni istanza condivide le mesh e i materiali
|
||
del file. `modelDelete` libera il modello; le istanze tengono i loro nodi, spogli.
|
||
|
||
[source,lua]
|
||
----
|
||
local foxModel = modelLoad(DIR .. "Fox.glb")
|
||
local pack = {}
|
||
for i = 1, 6 do
|
||
local fox = modelInstance(foxModel)
|
||
nodeSetScale(fox, 0.02) -- Il file è in centimetri.
|
||
nodeSetPosition(fox, i * 1.5 - 5, 0, 0)
|
||
pack[i] = fox
|
||
end
|
||
----
|
||
|
||
Le animazioni di un modello sono elencate da `modelGetAnimations` e pilotate per
|
||
istanza con `animationPlay(node, nameOrIndex, loop, speed, fade)` sul nodo radice
|
||
dell'istanza, più le chiamate per pausa, ripresa, arresto e salto. L'animazione dei
|
||
nodi (cose che si muovono), lo skinning (personaggi che si deformano) e i morph
|
||
target (blend shape: volti, sincronizzazione labiale, muscoli che si gonfiano)
|
||
funzionano tutti. Le animazioni avanzano con il tempo reale e si fermano mentre il
|
||
gioco è in pausa. Il peso di un morph si può anche impostare da script con
|
||
`nodeSetMorph`, per nome o per numero, sul nodo che porta la mesh.
|
||
|
||
Un `fade` in secondi fa una dissolvenza incrociata da qualunque cosa fosse in
|
||
riproduzione, così una camminata diventa una corsa senza scatti. Quando non c'era
|
||
niente in riproduzione (dopo `animationStop`, o quando un ragdoll è stato
|
||
disattivato), la dissolvenza parte dalla posa in cui si trovano i nodi, così un
|
||
personaggio si rialza dolcemente da dove è caduto. Fino a quattro altre clip
|
||
possono essere riprodotte insieme su livelli sopra quello di base
|
||
(`animationPlayLayer`), ognuna miscelata con `animationSetLayerWeight` e limitata a
|
||
una parte dello scheletro da `animationSetLayerMask`: uno sguardo intorno sul collo
|
||
mentre le gambe camminano, un saluto con un braccio sopra una corsa.
|
||
|
||
[source,lua]
|
||
----
|
||
animationPlay(fox, "Walk", true)
|
||
animationPlayLayer(fox, 1, "Survey", true, 1, 0.5) -- La testa si gira, sfumata in mezzo secondo
|
||
animationSetLayerMask(fox, 1, nodeFind("b_Neck_04", fox)) -- ... solo sul collo e sopra.
|
||
|
||
function onCollision(a, b, x, y, z, speed)
|
||
if (a == fox or b == fox) and speed > 3 then
|
||
animationPlay(fox, "Run", true, 1.2, 0.4) -- Più veloce, in dissolvenza incrociata in 0.4 s.
|
||
end
|
||
end
|
||
----
|
||
|
||
[#_sound_in_the_scene]
|
||
==== Il suono nella scena
|
||
|
||
Un effetto sonoro può essere collocato nella scena, così si sente da dove si trova.
|
||
`soundSetPosition(channel, x, y, z)` mette un canale in riproduzione in un punto
|
||
del mondo, `soundSetNode(channel, node)` gli fa invece seguire un nodo, e
|
||
`soundSetRange(channel, near, far)` dice dove è a volume pieno e dove è sfumato
|
||
fino al silenzio. I suoni si sentono dalla telecamera a meno che
|
||
`soundSetListener` non nomini un altro nodo (la testa del giocatore, per un gioco
|
||
in terza persona), e si spostano a sinistra e a destra mentre l'ascoltatore si
|
||
gira. Un canale non collocato nella scena suona come ha sempre fatto, con
|
||
`soundSetPan` per lo stereo.
|
||
|
||
[source,lua]
|
||
----
|
||
local crackle = soundPlay(fireSound, -1) -- Ripete all'infinito.
|
||
soundSetNode(crackle, brazier)
|
||
soundSetRange(crackle, 2, 18) -- Pieno entro 2 unità, sparito a 18.
|
||
soundSetListener(hero)
|
||
----
|
||
|
||
[#scenesprites]
|
||
==== Sprite e testo nella scena
|
||
|
||
Qualsiasi nodo può mostrare un'immagine invece di una mesh. `nodeSetSprite(node, sprite,
|
||
height)` mette uno sprite caricato (ogni fotogramma di uno animato, con
|
||
`nodeSetSpriteFrame` a scegliere) su un quad alto altrettante unità del mondo,
|
||
illuminato come qualsiasi altra cosa e che proietta ombre a meno che
|
||
`nodeSetShadow` non dica altrimenti. `nodeSetText(node, text, height)` fa lo
|
||
stesso con una riga di testo nel font e nel colore selezionati, senza
|
||
illuminazione. Per il resto del motore entrambi sono mesh: si appendono a un
|
||
genitore, si scalano e accettano la fusione di un materiale.
|
||
|
||
`nodeSetBillboard` gira un nodo verso la telecamera a ogni fotogramma:
|
||
`BILLBOARD_ALL` di fronte (etichette con il nome, bagliori, impostor),
|
||
`BILLBOARD_Y` solo attorno al proprio asse verticale (alberi, barre della salute,
|
||
qualsiasi cosa debba restare dritta), e `BILLBOARD_NONE` per smettere. Funziona
|
||
anche sulle mesh.
|
||
|
||
[source,lua]
|
||
----
|
||
local tag = nodeNew()
|
||
nodeSetParent(tag, fox)
|
||
nodeSetPosition(tag, 0, 110, 0) -- Nei centimetri di Fox, sopra la sua testa.
|
||
nodeSetScale(tag, 50) -- Annulla la scala 0.02 dell'istanza.
|
||
nodeSetText(tag, "FOX", 0.6)
|
||
nodeSetBillboard(tag, BILLBOARD_ALL)
|
||
nodeSetShadow(tag, false)
|
||
|
||
local icon = nodeNew()
|
||
nodeSetParent(icon, fox)
|
||
nodeSetPosition(icon, 0, 150, 0)
|
||
nodeSetScale(icon, 50)
|
||
nodeSetSprite(icon, spriteLoad(DIR .. "crate.png"), 0.5)
|
||
nodeSetBillboard(icon, BILLBOARD_Y)
|
||
----
|
||
|
||
[#scenefront]
|
||
==== Davanti all'overlay
|
||
|
||
La scena normalmente viene disegnata sotto l'overlay, quindi qualsiasi cosa
|
||
disegnata sull'overlay la copre. `nodeSetLayer(node, LAYER_FRONT)` sposta invece un
|
||
nodo, e tutto ciò che vi è appeso, nel livello frontale: la stessa telecamera lo
|
||
disegna dopo l'overlay, illuminato dalle stesse luci e rifinito dalla
|
||
stessa esposizione, curva tonale e bloom, così un logo che gira, un titolo 3D o un
|
||
modello in una tabella dei punteggi record può stare sopra una grafica 2D. Il
|
||
livello frontale è un'immagine a sé, svuotata a trasparente, quindi l'overlay si
|
||
vede ovunque esso non disegna nulla, e niente nella scena dietro l'overlay può
|
||
nasconderlo. Le GUI e le particelle disegnate sopra l'overlay gli stanno comunque
|
||
sopra.
|
||
|
||
I nodi frontali proiettano comunque ombre sulla scena. Il cielo, le particelle 3D
|
||
e le linee di debug restano nella scena, e le viste (`viewNew`) disegnano solo i
|
||
nodi della scena. Il livello costa una seconda passata di rendering, e solo nei
|
||
fotogrammi in cui c'è qualcosa.
|
||
|
||
[source,lua]
|
||
----
|
||
-- Una moneta d'oro che gira sopra la schermata del titolo 2D.
|
||
local coin = nodeNew()
|
||
nodeSetMesh(coin, meshCylinder(1, 0.2, 48), gold)
|
||
nodeSetRotation(coin, 90, 0, 0)
|
||
nodeSetLayer(coin, LAYER_FRONT)
|
||
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
spriteDraw(titleArt, 0, 0)
|
||
nodeRotate(coin, 0, 2, 0)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#sceneterrain]
|
||
==== Terreno
|
||
|
||
`meshHeightmap(image, sizeX, sizeY, sizeZ)` trasforma un'immagine in scala di
|
||
grigi in un terreno ondulato: un vertice per pixel, il nero in basso e il bianco
|
||
alto `sizeY`, con le normali ricavate dalle pendenze e le coordinate di texture
|
||
stese sull'insieme. Dai al suo materiale una texture e `materialSetTiling` per
|
||
ripeterla ogni poche unità. Le immagini possono arrivare a 1025 pixel per lato; una
|
||
mappa da 257 pixel su 120 unità dà un vertice ogni mezza unità, che basta e avanza
|
||
per le colline.
|
||
|
||
Un corpo `BODY_STATIC` con `SHAPE_MESH` su quel nodo diventa un campo di altezze,
|
||
molto meno costoso di quanto lo sarebbero i triangoli, così le auto ci guidano
|
||
sopra e i giocatori ci camminano senza bisogno d'altro.
|
||
`terrainGetHeight(node, x, z)` dice quanto è alto il terreno in un punto, per
|
||
collocarci sopra delle cose senza un raycast.
|
||
|
||
[source,lua]
|
||
----
|
||
local ground = materialNew()
|
||
materialSetTexture(ground, DIR .. "grass.ktx2")
|
||
materialSetTiling(ground, 24, 24)
|
||
local hills = nodeNew()
|
||
nodeSetMesh(hills, meshHeightmap(DIR .. "hills.png", 120, 14, 120), ground)
|
||
bodyNew(hills, BODY_STATIC, SHAPE_MESH) -- Un campo di altezze.
|
||
|
||
for i = 1, 20 do
|
||
local x, z = math.random(-50, 50), math.random(-50, 50)
|
||
local tree = modelInstance(treeModel)
|
||
nodeSetPosition(tree, x, terrainGetHeight(hills, x, z), z)
|
||
end
|
||
----
|
||
|
||
[#_navigation]
|
||
==== Navigazione
|
||
|
||
Una navmesh (mesh di navigazione) dice ai personaggi dove possono camminare. `navNew(radius,
|
||
height, maxSlope, maxStep)` ne avvia una per una data dimensione dell'agente, `navAddNode`
|
||
le fornisce la geometria del livello (la radice di un'istanza di modello fornisce
|
||
l'intero modello), e `navBuild` precalcola la superficie percorribile: pavimenti,
|
||
rampe e scale che l'agente può salire, meno un margine attorno a ogni muro e
|
||
colonna. Precalcolare Sponza richiede un decimo di secondo su un desktop; `navSave`
|
||
e `navLoad` tengono una navmesh precalcolata nella cartella dei dati del gioco per
|
||
le macchine su cui ci vuole di più, e una navmesh distribuita con il gioco si
|
||
carica allo stesso modo. `navDraw` mostra la navmesh sopra la scena mentre stai
|
||
sistemando i parametri.
|
||
|
||
Le interrogazioni rispondono poi alle domande che un gioco fa: `navPath`
|
||
restituisce gli angoli del percorso più breve tra due punti, `navNearest` aggancia
|
||
un punto alla navmesh, `navRaycast` dice se una camminata in linea retta è
|
||
bloccata, e `navRandomPoint` sceglie un posto verso cui vagare.
|
||
|
||
Gli agenti (`navAgentNew`) percorrono la navmesh al posto tuo. `navAgentMoveTo` ne
|
||
manda uno da qualche parte, e lui aggira gli altri agenti lungo la strada, muovendo
|
||
il suo nodo (o pilotando un controller del giocatore su di esso, con
|
||
`navAgentSetPlayer`) e chiamando `onNavArrived` quando arriva.
|
||
`navAgentGetVelocity` ti dice se riprodurre la camminata o la corsa.
|
||
|
||
[source,lua]
|
||
----
|
||
local nav = navNew(0.4, 1.6, 45, 0.3) -- Un agente largo 0.8, alto 1.6, che sale 0.3.
|
||
navAddNode(nav, level)
|
||
navBuild(nav)
|
||
|
||
local guards = {}
|
||
for i = 1, 6 do
|
||
local guard = modelInstance(guardModel)
|
||
local x, y, z = navRandomPoint(nav)
|
||
nodeSetPosition(guard, x, y, z)
|
||
animationPlay(guard, "Walk", true)
|
||
local agent = navAgentNew(nav, guard, 0.35, 1.6, 1.4)
|
||
navAgentMoveTo(agent, navRandomPoint(nav))
|
||
guards[agent] = guard
|
||
end
|
||
|
||
function onNavArrived(agent)
|
||
navAgentMoveTo(agent, navRandomPoint(nav)) -- Via verso il prossimo punto.
|
||
end
|
||
----
|
||
|
||
[#sceneperformance]
|
||
==== Prestazioni e requisiti
|
||
|
||
La scena ha bisogno di una GPU che parli Vulkan (Linux), Direct3D 12 (Windows 10 e
|
||
successivi), Metal (macOS) o OpenGL ES 3.1. Il backend ES è quello che porta dentro
|
||
le console portatili Linux economiche e il Raspberry Pi, i cui chip Mali e
|
||
VideoCore hanno un driver GLES maturo e un Vulkan assente o immaturo; viene scelto
|
||
automaticamente quando nessuno degli altri tre è disponibile, e disegna la stessa
|
||
immagine con gli stessi shader. Il Raspberry Pi 4 resta il Pi minimo per il 3D: il
|
||
Pi 3 ha solo OpenGL ES 2.0, che non può esprimere lo skinning né i morph target
|
||
della scena. Su una macchina senza nessuno di essi, i giochi 2D girano esattamente
|
||
come prima (fisica 2D compresa) e la prima chiamata 3D termina il gioco con un
|
||
errore che nomina il problema.
|
||
|
||
Le mesh fuori dalla vista della telecamera vengono saltate, e le copie della stessa
|
||
mesh con lo stesso materiale vengono disegnate come un'unica chiamata con istanze,
|
||
così una foresta di un solo albero costa più o meno quanto costa un albero.
|
||
`sceneGetStats` riporta i disegni raccolti, quanti erano in vista, quante chiamate
|
||
di disegno hanno richiesto e la memoria delle texture in uso. Per aiutarlo:
|
||
|
||
* Condividi mesh e materiali tra i nodi (un solo `meshBox`, molti nodi; un solo
|
||
modello, molte chiamate a `modelInstance`) invece di crearne uno per oggetto.
|
||
* Le mesh con skinning, quelle con morph e quelle con fusione vengono disegnate una
|
||
alla volta; limitale a ciò di cui la scena ha bisogno.
|
||
* L'antialiasing (multisampling 4x) è attivo ovunque la GPU lo offra;
|
||
`sceneSetAntialias(false)` fa recuperare velocità su un Pi, come anche un
|
||
`sceneSetShadowSize` più piccolo e meno luci puntiformi che proiettano ombre,
|
||
ognuna delle quali costa sei passate.
|
||
* Tieni d'occhio il numero di triangoli e la dimensione delle texture sulle schede
|
||
piccole, distribuisci texture KTX2, e carica un modello una volta invece di due.
|
||
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
local draws, drawn, calls, textureKB = sceneGetStats()
|
||
overlayClear()
|
||
local text = string.format("%d/%d meshes, %d calls, %d KB", drawn, draws, calls, textureKB)
|
||
fontPrint(10, 10, text)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#physics]
|
||
=== Fisica
|
||
|
||
Corpi, giunti, trigger e raycast vivono nella scena 3D e si appoggiano a Jolt
|
||
Physics. Un corpo si collega a un nodo come fa una mesh, e da quel momento il
|
||
nodo e il corpo sono una cosa sola: la forma del corpo è dimensionata in unità
|
||
del mondo e scalata dal nodo, e qualunque dei due si muova, l'altro lo segue.
|
||
Le unità sono metri, chilogrammi e secondi, e la gravità predefinita è
|
||
`0, -9.81, 0`. La fisica non ha bisogno di una GPU; vedi <<physics2d,Giochi 2D>>.
|
||
|
||
[#_bodies]
|
||
==== Corpi
|
||
|
||
`bodyNew(node, type, shape, a, b, c)` dà a un nodo un corpo di uno di tre
|
||
tipi:
|
||
|
||
* Un corpo *statico* (`BODY_STATIC`) resta dov'era il nodo quando il corpo è
|
||
stato creato: pavimenti, muri, il cabinato. Spostare il suo nodo in seguito
|
||
non sposta il corpo.
|
||
* Un corpo *cinematico* (`BODY_KINEMATIC`) segue il suo nodo ovunque lo
|
||
spostino lo script o un'animazione, e spinge via i corpi dinamici senza
|
||
essere spinto a sua volta: una racchetta, una piattaforma mobile, la mano di
|
||
un personaggio.
|
||
* Un corpo *dinamico* (`BODY_DYNAMIC`) è mosso dalla simulazione e guida il
|
||
suo nodo: cade, rimbalza, scivola e viene spinto. Non spostare tu il suo
|
||
nodo; dagli invece velocità, forze o impulsi
|
||
(`bodySetVelocity`, `bodyApplyForce`, `bodyApplyImpulse`).
|
||
|
||
La forma è un parallelepipedo, una sfera, una capsula o un cilindro
|
||
dimensionati da `a`, `b` e `c` (`SHAPE_BOX` prende larghezza, altezza e
|
||
profondità; `SHAPE_SPHERE` un raggio; `SHAPE_CAPSULE` e `SHAPE_CYLINDER` un
|
||
raggio e un'altezza lungo Y), un inviluppo convesso della mesh del nodo
|
||
(`SHAPE_HULL`, tutto ciò che sta sotto il nodo, quindi va bene anche la radice
|
||
di un'istanza di modello), oppure i triangoli esatti della mesh
|
||
(`SHAPE_MESH`, solo per corpi statici e cinematici; su una mesh da heightmap è
|
||
un campo di altezze, vedi <<sceneterrain,Terreno>>).
|
||
|
||
[source,lua]
|
||
----
|
||
floor = nodeNew()
|
||
nodeSetMesh(floor, meshBox(14, 0.2, 14), stone)
|
||
nodeSetPosition(floor, 0, -1.9, 0)
|
||
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 14, 0.2, 14)
|
||
|
||
for i = 1, 6 do
|
||
local crate = nodeNew()
|
||
nodeSetMesh(crate, meshBox(0.6, 0.6, 0.6), wood)
|
||
nodeSetPosition(crate, -1, -1.5 + (i - 1) * 0.62, 0)
|
||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
|
||
bodySetMass(crate, 5)
|
||
end
|
||
|
||
paddle = nodeNew()
|
||
nodeSetMesh(paddle, meshBox(0.3, 1.2, 1.6), red)
|
||
nodeSetPosition(paddle, -4, -1.2, 0)
|
||
bodyNew(paddle, BODY_KINEMATIC, SHAPE_BOX, 0.3, 1.2, 1.6)
|
||
|
||
function onOverlayUpdate()
|
||
nodeMove(paddle, 0.06, 0, 0) -- La racchetta cinematica attraversa la pila.
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
.Massa e materiali
|
||
La massa di un corpo dinamico deriva dal volume della sua forma alla densità
|
||
dell'acqua, quindi un parallelepipedo grande come una porta pesa 140
|
||
chilogrammi e una spintarella non lo smuove. Imposta `bodySetMass` su tutto ciò
|
||
che vuoi che sembri leggero, e commisura gli impulsi alla massa: un impulso di
|
||
`mass * speed` imprime quella velocità. `bodySetFriction` e `bodySetBounce`
|
||
fanno il ghiaccio e la gomma; `bodySetEnabled` toglie un corpo dal mondo e lo
|
||
rimette senza perderlo.
|
||
|
||
.Tempistica
|
||
Il mondo avanza a passo fisso, sessanta volte al secondo, in base
|
||
all'orologio, dopo che le animazioni hanno messo in posa i loro nodi e prima
|
||
del rendering della scena, così un corpo cinematico guidato da un'animazione
|
||
spinge con la velocità giusta. Mettere in pausa il gioco mette in pausa la
|
||
fisica; `physicsSetEnabled(false)` mette in pausa solo lei. `bodyIsResting`
|
||
dice quando un corpo si è assestato, cioè quando una cosa lanciata si è
|
||
fermata e si può contare un punteggio. Un corpo, un giocatore, un ragdoll, un
|
||
corpo morbido o un veicolo il cui nodo è stato eliminato viene rilasciato
|
||
all'inizio del fotogramma successivo, abilitato o no e in pausa o no, così il
|
||
suo posto torna libero; un handle di nodo che `nodeNew` assegna di nuovo in
|
||
seguito non si porta dietro nulla della fisica del vecchio nodo, e
|
||
`playerExists` e i suoi simili rispondono `false` finché non ne riceve una
|
||
propria.
|
||
|
||
[#_events_and_queries]
|
||
==== Eventi e interrogazioni
|
||
|
||
Definisci `onCollision(nodeA, nodeB, x, y, z, speed)` e Singe la chiama una
|
||
volta per ogni nuovo contatto, con il punto e la velocità a cui i due corpi si
|
||
sono incontrati, così un urto e uno schianto possono suonare diversi. Un corpo
|
||
reso trigger con `bodySetTrigger` non spinge niente ma segnala ciò che vi entra
|
||
e ne esce tramite `onTrigger(trigger, other, entered)`: un traguardo, una
|
||
porta, una zona di pericolo. `physicsRaycast` trova il primo corpo lungo un
|
||
raggio; con `sceneUnproject` a fornire il raggio, questa è la selezione con il
|
||
mouse (vedi <<nodes,Nodi>>).
|
||
|
||
[source,lua]
|
||
----
|
||
finish = nodeNew()
|
||
nodeSetPosition(finish, 0, 1, -40)
|
||
bodyNew(finish, BODY_STATIC, SHAPE_BOX, 12, 2, 0.5)
|
||
bodySetTrigger(finish, true)
|
||
|
||
function onTrigger(trigger, other, entered)
|
||
if trigger == finish and other == car and entered then
|
||
lapDone()
|
||
end
|
||
end
|
||
|
||
function onCollision(a, b, x, y, z, speed)
|
||
if speed > 6 then
|
||
local channel = soundPlay(crashSound)
|
||
soundSetPosition(channel, x, y, z)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#_joints]
|
||
==== Giunti
|
||
|
||
`jointHinge`, `jointBall` e `jointSlider` collegano due corpi, o un corpo e il
|
||
mondo quando il secondo nodo è `-1`: porte, pendoli, cassetti, le ruote di
|
||
un'auto vista di lato. L'ancora e l'asse si danno nello spazio del mondo, dove
|
||
si trovano i corpi quando il giunto viene creato. Cerniere e guide accettano
|
||
dei limiti (`jointSetLimits`): un angolo positivo della cerniera è una
|
||
rotazione destrorsa del primo nodo attorno all'asse, e una distanza positiva
|
||
della guida è lungo l'asse.
|
||
|
||
[source,lua]
|
||
----
|
||
door = nodeNew()
|
||
nodeSetMesh(door, meshBox(0.9, 2.0, 0.05), oak)
|
||
nodeSetPosition(door, 0.45, 1.0, 0)
|
||
bodyNew(door, BODY_DYNAMIC, SHAPE_BOX, 0.9, 2.0, 0.05)
|
||
bodySetMass(door, 20)
|
||
local hinge = jointHinge(door, -1, 0, 1.0, 0, 0, 1, 0) -- Sullo stipite, attorno a Y.
|
||
jointSetLimits(hinge, 0, 110) -- Si apre in un solo verso, 110 gradi.
|
||
----
|
||
|
||
I corpi sulle istanze di modello funzionano come tutti gli altri: `bodyNew(instanceRoot,
|
||
BODY_DYNAMIC, SHAPE_HULL)` avvolge l'intero modello in un inviluppo convesso.
|
||
Per un personaggio con skin la scelta migliore è un giocatore (vedi sotto) o
|
||
una capsula cinematica che spinge le cose anziché essere spinta.
|
||
|
||
[#physics2d]
|
||
==== Giochi 2D
|
||
|
||
La fisica non è legata alla scena 3D. I nodi esistono su ogni macchina, con o
|
||
senza GPU, quindi un gioco 2D può dare dei corpi ai nodi, chiamare
|
||
`physicsSet2D(true)` perché si muovano solo in X e Y e ruotino solo attorno a
|
||
Z, lavorare in coordinate dell'overlay con la gravità rivolta verso il basso
|
||
dello schermo, e disegnare gli sprite dove si trovano i nodi. Questa è la
|
||
forma che gira su un Raspberry Pi 3, che non ha il 3D.
|
||
|
||
[source,lua]
|
||
----
|
||
physicsSet2D(true)
|
||
physicsSetGravity(0, 900, 0) -- Pixel al secondo quadrato; la Y dell'overlay va verso il basso.
|
||
|
||
floor = nodeNew()
|
||
nodeSetPosition(floor, 360, 470, 0)
|
||
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 720, 20, 50)
|
||
|
||
crate = nodeNew()
|
||
nodeSetPosition(crate, 200, -40, 0)
|
||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 40, 40, 50)
|
||
bodySetMass(crate, 1)
|
||
|
||
function onOverlayUpdate()
|
||
local x, y = nodeGetPosition(crate)
|
||
local _, _, angle = nodeGetRotation(crate)
|
||
spriteRotate(crateArt, angle)
|
||
spriteDraw(crateArt, x, y, true)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
Dai a ogni corpo 2D un po' di profondità (la terza misura) così che le forme si
|
||
sovrappongano in Z; `50` va bene. I cerchi sono `SHAPE_SPHERE`. Anche i
|
||
giocatori e gli emettitori 2D (vedi <<particles,Particelle>>) funzionano in
|
||
questo mondo, e un platform è un giocatore con `playerMove(node, vx)`.
|
||
|
||
[#_players]
|
||
==== Giocatori
|
||
|
||
Un corpo va dove lo mandano le forze; un *giocatore* va dove dice lo script.
|
||
`playerNew` mette un controller del personaggio su un nodo: una capsula per
|
||
impostazione predefinita (`playerNew(node, radius, height)`), oppure qualsiasi
|
||
forma convessa di `bodyNew` (`playerNew(node, SHAPE_BOX, w, h, d)` per un
|
||
robot, `playerNew(node,
|
||
SHAPE_SPHERE, r, 0)` per qualcosa che rotola ma sale comunque i gradini,
|
||
`SHAPE_HULL` per il contorno del modello stesso; tre argomenti significano
|
||
sempre una capsula, quindi una sfera scrive le sue misure per intero). La forma
|
||
poggia sull'origine del nodo, che sono i piedi. A ogni fotogramma lo script dà
|
||
a `playerMove` la velocità orizzontale che vuole, e il controller cammina sui
|
||
pavimenti, sale i gradini fino a `playerSetStep`, scivola lungo i muri, rifiuta
|
||
i pendii più ripidi di `playerSetSlope`, cade per gravità, viaggia sulle
|
||
piattaforme mobili e spinge via i corpi dinamici leggeri (`playerSetPush`).
|
||
`playerJump` funziona solo con il terreno sotto i piedi e dice se ha saltato.
|
||
La rotazione del nodo spetta allo script: giralo verso la direzione in cui
|
||
cammina, e appendi il modello sotto di esso come figlio.
|
||
|
||
[source,lua]
|
||
----
|
||
hero = nodeNew()
|
||
nodeSetPosition(hero, 0, 0, 0)
|
||
playerNew(hero, 0.3, 1.0) -- Una capsula di raggio 0.3, alta 1, piedi sul nodo.
|
||
fox = modelInstance(modelLoad(DIR .. "Fox.glb"))
|
||
nodeSetParent(fox, hero)
|
||
|
||
function onInputPressed(switch)
|
||
if switch == SWITCH_BUTTON1 then
|
||
playerJump(hero, 4.5) -- Serve il terreno sotto i piedi.
|
||
end
|
||
end
|
||
|
||
function onOverlayUpdate()
|
||
local vx = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) / 32767 * 3
|
||
local vz = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_Y) / 32767 * 3
|
||
playerMove(hero, vx, vz)
|
||
if vx ~= 0 or vz ~= 0 then
|
||
nodeSetRotation(hero, 0, math.deg(math.atan(-vx, -vz)), 0)
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
I giocatori segnalano `onCollision` quando cominciano a toccare un corpo e
|
||
`onTrigger` quando entrano in un sensore o ne escono, con il loro nodo al posto
|
||
che occuperebbe il nodo di un corpo, quindi una porta, un oggetto da
|
||
raccogliere e una pozza di lava funzionano per un giocatore come per una
|
||
palla. `playerIsOnGround` e `playerGetGround` dicono su che cosa poggia il
|
||
giocatore, per il suono dei passi e per scendere apposta da una piattaforma.
|
||
In un mondo 2D un giocatore è un personaggio da platform: `playerMove(node, vx)`
|
||
corre lungo X, la gravità punta verso il basso dello schermo, e tutto il resto
|
||
è uguale. Le tolleranze del controller scalano con la forma, quindi una
|
||
capsula di 30 pixel in un gioco di sprite si comporta come una di 0,3 metri in
|
||
un gioco 3D.
|
||
|
||
[#_vehicles]
|
||
==== Veicoli
|
||
|
||
Un *veicolo* è un corpo dinamico (il telaio, creato con `bodyNew` come
|
||
qualsiasi corpo, rivolto verso -Z con Y in alto) che Jolt guida tramite delle
|
||
ruote: `vehicleNew(node, VEHICLE_CAR)` per qualsiasi cosa abbia degli assali,
|
||
`VEHICLE_MOTORCYCLE` per due ruote in linea che si inclinano in curva e stanno
|
||
in equilibrio, o `VEHICLE_TANK` per ruote su due cingoli che girano sul posto.
|
||
Le ruote sono nodi: `vehicleAddWheel(node, wheelNode, radius, width,
|
||
suspension)` prende la posizione della ruota da dove si trova il suo nodo
|
||
rispetto al telaio, e da quel momento il motore di gioco mette in posa quel
|
||
nodo a ogni fotogramma (il suo asse X è l'assale, quindi una ruota
|
||
`meshCylinder` vuole un figlio ruotato di 90 gradi). Le ruote trovate in un
|
||
modello con `nodeFind("wheelFL", car)` funzionano allo stesso modo. Per
|
||
impostazione predefinita sterzano le ruote anteriori e sono motrici tutte;
|
||
`vehicleSetWheel` dice altrimenti.
|
||
|
||
A ogni fotogramma lo script passa i comandi del pilota: `vehicleDrive(node, forward,
|
||
right, brake, handBrake)`. Il motore, il cambio automatico e i differenziali
|
||
sono quelli di Jolt, con i valori predefiniti di un'auto da 1500 kg;
|
||
`vehicleSetEngine`, `vehicleSetGears`, `vehicleSetSuspension`,
|
||
`vehicleSetSteering`, `vehicleSetBrakes` e `vehicleSetAntiRoll` li regolano, e
|
||
qualsiasi modifica ricostruisce il veicolo sul posto al passo successivo.
|
||
`vehicleGetSpeed`, `vehicleGetRpm` e `vehicleGetGear` alimentano un cruscotto.
|
||
Un veicolo resta un corpo: salta dalle rampe, sbatte contro i muri, spinge via
|
||
le casse e segnala `onCollision`.
|
||
|
||
[source,lua]
|
||
----
|
||
car = nodeNew()
|
||
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
|
||
nodeSetPosition(car, 0, 1, 0)
|
||
bodyNew(car, BODY_DYNAMIC, SHAPE_BOX, 1.8, 0.5, 4.0)
|
||
bodySetMass(car, 1500)
|
||
vehicleNew(car, VEHICLE_CAR)
|
||
local corners = {
|
||
{ -0.95, -0.2, -1.4 }, { 0.95, -0.2, -1.4 }, -- Anteriore sinistra e destra ...
|
||
{ -0.95, -0.2, 1.4 }, { 0.95, -0.2, 1.4 }, -- ... posteriore sinistra e destra.
|
||
}
|
||
for i, p in ipairs(corners) do
|
||
local wheel = nodeNew()
|
||
nodeSetParent(wheel, car)
|
||
nodeSetPosition(wheel, p[1], p[2], p[3])
|
||
vehicleAddWheel(car, wheel, 0.35, 0.25, 0.4)
|
||
end
|
||
|
||
function onOverlayUpdate()
|
||
local throttle = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_TRIGGER) / 32767
|
||
local brake = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_TRIGGER) / 32767
|
||
local steer = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) / 32767
|
||
vehicleDrive(car, throttle, steer, brake, 0)
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
I carri armati prendono `forward` come acceleratore e `right` come sterzata
|
||
(senza acceleratore ruotano sul posto), e ignorano le regolazioni di sterzo,
|
||
freni e antirollio, perché i loro cingoli frenano e sterzano da soli. La molla
|
||
che inclina la moto è scalata sul telaio, quindi una moto leggera sta in
|
||
equilibrio bene quanto una pesante. I veicoli a ruote hanno bisogno di un mondo
|
||
3D; un'auto 2D vista di lato è un corpo con due ruote incernierate, che le
|
||
chiamate dei giunti costruiscono già.
|
||
|
||
[#_water]
|
||
==== Acqua
|
||
|
||
`bodySetWater(node, density, linearDrag, angularDrag)` riempie d'acqua un corpo
|
||
statico: la faccia superiore del suo parallelepipedo è la superficie, e ogni
|
||
corpo dinamico al suo interno galleggia, affonda o rolla secondo la spinta di
|
||
galleggiamento di Jolt rispetto a quella superficie (conta la parte immersa
|
||
della forma, quindi una cassa inclinata si raddrizza da sola), rallentato dalle
|
||
resistenze. `bodySetCurrent` dà all'acqua un flusso. `density` è un
|
||
moltiplicatore, `1` per l'acqua, e il `bodySetBuoyancy` di ciascun corpo
|
||
galleggiante (predefinito `1.2`, che galleggia dolcemente; sotto `1` affonda)
|
||
lo scala. Il volume è un trigger, quindi `onTrigger` scatta all'entrata e
|
||
all'uscita, per gli spruzzi. L'acqua non viene disegnata: dai al nodo del
|
||
volume un materiale con fusione, o nessun materiale.
|
||
|
||
Un giocatore in acqua nuota: `playerIsSwimming` lo dice, `playerMove(node, vx,
|
||
vy, vz)` allora governa su tre assi, il giocatore affonda lentamente se non
|
||
riceve input verticale (`playerSetSwim` imposta quella velocità e la
|
||
resistenza dell'acqua), e la corrente lo trascina. Una barca è uno scafo
|
||
galleggiante a cui `vehicleNew(node, VEHICLE_BOAT)` dà un'elica e un timone:
|
||
`vehicleSetThrust` dice quanto forte spinge e in quale punto dello scafo,
|
||
`vehicleSetRudder` quanto forte vira, e `vehicleDrive` funziona come per
|
||
un'auto, il tutto solo finché l'elica è sotto una superficie.
|
||
|
||
[source,lua]
|
||
----
|
||
pool = nodeNew()
|
||
nodeSetMesh(pool, meshBox(8, 3, 12), blue) -- Blu con fusione, disegnato come l'acqua.
|
||
nodeSetPosition(pool, 0, -1.5, 0) -- La sua faccia superiore, a Y = 0, è la superficie.
|
||
bodyNew(pool, BODY_STATIC, SHAPE_BOX, 8, 3, 12)
|
||
bodySetWater(pool, 1.0, 0.6, 0.2)
|
||
bodySetCurrent(pool, 0.5, 0, 0) -- Deriva verso +X.
|
||
bodySetBuoyancy(crate, 1.6) -- Galleggia alta.
|
||
bodySetBuoyancy(anchor, 0.3) -- Va a fondo.
|
||
----
|
||
|
||
[#_ragdolls]
|
||
==== Ragdoll
|
||
|
||
`ragdollNew(node)` cerca la skin sotto un'istanza di modello e trasforma lo
|
||
scheletro in una ricetta di ragdoll: ogni giunto che ha un giunto figlio
|
||
diventa una capsula lungo quell'osso, collegata all'osso più vicino sopra di
|
||
essa da un giunto swing-twist con limiti sensati, e i giunti foglia (dita,
|
||
orecchie, la punta di una coda) seguono i loro genitori. Niente si muove fino a
|
||
`ragdollActivate`: allora l'animazione si ferma, i corpi vengono creati dove
|
||
sono le ossa, e da quel momento la fisica guida i nodi dei giunti, così la mesh
|
||
con skin si affloscia, colpisce il pavimento e si ferma insieme al resto del
|
||
mondo.
|
||
|
||
`ragdollApplyImpulse` dà una spinta a un osso tramite il nome del suo giunto.
|
||
`ragdollSetStrength` dà forza a ogni giunto verso la posa in cui il ragdoll è
|
||
stato attivato, il che trasforma una caduta inerte in un barcollare o permette
|
||
a un personaggio a terra di ritirare a sé gli arti; non rimette in piedi un
|
||
corpo, perché niente fa leva sul pavimento. `ragdollDeactivate` restituisce lo
|
||
scheletro all'animazione da dovunque giaccia, e un `animationPlay` con
|
||
dissolvenza lo rialza dolcemente da lì. Regola un osso che sembra sbagliato con
|
||
`ragdollSetJoint` prima dell'attivazione.
|
||
|
||
[source,lua]
|
||
----
|
||
fox = modelInstance(foxModel)
|
||
ragdollNew(fox) -- Ricetta dalla skin; ancora animato.
|
||
function onCollision(a, b, x, y, z, speed)
|
||
if (a == hero or b == hero) and speed > 3 then
|
||
playerSetEnabled(hero, false)
|
||
ragdollActivate(fox) -- Ora è la fisica a guidare lo scheletro.
|
||
ragdollApplyImpulse(fox, "b_Spine02_03", 40, 30, 0)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#_soft_bodies]
|
||
==== Corpi morbidi
|
||
|
||
Stoffa, corde e gelatina sono mesh i cui vertici sono simulati.
|
||
`softNew(node, SOFT_CLOTH)` trasforma la mesh del nodo in stoffa, e i suoi
|
||
vertici (saldati per posizione) in particelle unite da vincoli di stiramento,
|
||
taglio e flessione, nel punto in cui il nodo l'ha messa nel mondo; una
|
||
`meshPlane(width, depth, columns, rows)` suddivisa è il punto di partenza
|
||
abituale. `softNew(node,
|
||
SOFT_BODY)` fa lo stesso per una mesh chiusa e `softSetPressure` la gonfia: un
|
||
palloncino, una gelatina. `softNew(node, SOFT_ROPE, x, y, z, segments,
|
||
radius)` stende una catena di particelle dalla posizione del nodo a quel punto
|
||
e dà al nodo una mesh a tubo attorno a esse (imposta il suo aspetto con
|
||
`nodeSetMaterial`).
|
||
|
||
I corpi morbidi collidono con tutto ciò che è rigido, cadono per gravità e
|
||
vengono disegnati dalla mesh da cui sono nati, che il motore di gioco riscrive
|
||
a ogni fotogramma. `softPin` tiene la particella più vicina dov'è o la lega a
|
||
un nodo (il bordo della bandiera all'asta, la cima della corda alla gru);
|
||
`softSetStiffness`, `softSetMass` e `softSetDamping` regolano il materiale. Un
|
||
nodo la cui mesh è condivisa con altri nodi cambia anche quelli, quindi dai a
|
||
un corpo morbido una mesh tutta sua.
|
||
|
||
[source,lua]
|
||
----
|
||
flag = nodeNew()
|
||
nodeSetMesh(flag, meshPlane(1.6, 1.0, 16, 10), red)
|
||
nodeSetPosition(flag, -4.2, 2.4, 0)
|
||
nodeSetRotation(flag, 90, 0, 0) -- Appesa in verticale.
|
||
softNew(flag, SOFT_CLOTH)
|
||
for _, y in ipairs({ 1.9, 2.15, 2.4, 2.65, 2.9 }) do
|
||
softPin(flag, -5, y, 0) -- Lungo l'asta.
|
||
end
|
||
----
|
||
|
||
Il costo cresce con il numero di particelle: un telo di 20 per 20 sono 441
|
||
particelle e va bene su un Raspberry Pi 4; tieni la stoffa entro qualche
|
||
centinaio di particelle e una corda entro una dozzina di segmenti. Le
|
||
particelle collidono con i corpi rigidi, non con le facce, quindi è una mesh
|
||
abbastanza fitta (particelle più vicine della cosa più piccola che ci cadrà
|
||
sopra) a impedire che una cassa la attraversi. Non c'è autocollisione: la
|
||
stoffa accartocciata può attraversare se stessa. Le mesh con skin non possono
|
||
essere morbide.
|
||
|
||
[#_seeing_the_physics]
|
||
==== Vedere la fisica
|
||
|
||
Quando una cassa attraversa il pavimento o un giunto si piega nel verso
|
||
sbagliato, `physicsSetDebug` disegna ciò che la simulazione contiene davvero,
|
||
come linee sopra la scena a ogni fotogramma:
|
||
|
||
* `DEBUG_SHAPES`: i corpi in wireframe, colorati secondo lo stato (dormiente,
|
||
in movimento, statico), con i giocatori in giallo.
|
||
* `DEBUG_CONSTRAINTS`: i giunti, i giunti dei ragdoll e i vincoli dei veicoli,
|
||
con i loro limiti.
|
||
* `DEBUG_CONTACTS`: una croce rossa dove i corpi si sono incontrati in questo passo.
|
||
* `DEBUG_VELOCITIES`: una linea lungo la velocità di ogni corpo in movimento.
|
||
* `DEBUG_STATIC`: anche le forme dei corpi statici, che altrimenti restano
|
||
spente perché una mesh di livello sono tantissime linee.
|
||
|
||
Sommali, o usa `DEBUG_ALL` per i primi quattro. `DEBUG_NONE` lo spegne, e da
|
||
spento non costa nulla. Uno script disegna le proprie linee allo stesso modo
|
||
con `lineDraw` (un raycast, un percorso, un riquadro di delimitazione), un
|
||
fotogramma alla volta come l'overlay, e `navDraw` mostra una navmesh.
|
||
|
||
[source,lua]
|
||
----
|
||
if debugging then
|
||
physicsSetDebug(DEBUG_SHAPES + DEBUG_CONTACTS)
|
||
end
|
||
|
||
function onOverlayUpdate()
|
||
local hit, hx, hy, hz = physicsRaycast(gunX, gunY, gunZ, aimX, aimY, aimZ, 60)
|
||
if hit then
|
||
lineDraw(gunX, gunY, gunZ, hx, hy, hz, 255, 255, 0)
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#_where_it_runs]
|
||
==== Dove gira
|
||
|
||
La fisica gira sulla CPU e non ha bisogno di una GPU, ma la libreria è
|
||
compilata per SSE4.1 su x86 (dal 2008 in poi) e per NEON sul Pi; una macchina
|
||
che ne è priva riceve un errore dal primo `bodyNew`. La simulazione usa tutti i
|
||
core tranne uno, quindi un Pi a quattro core avanza su tre thread; la traccia
|
||
del programma dice quanti.
|
||
|
||
[#particles]
|
||
=== Particelle
|
||
|
||
Scintille, fumo, fuoco, pioggia, polvere, magia: tutto ciò che è fatto di tante
|
||
piccole cose di breve durata è un lavoro per un *emettitore*. Un emettitore
|
||
contiene una ricetta (quante al secondo, quanto vivono, quanto veloci e in che
|
||
direzione partono, che cosa le attira, come crescono, sbiadiscono e ruotano) e
|
||
una riserva di particelle che gestisce secondo quella ricetta. Lo script
|
||
imposta la ricetta una volta, poi avvia l'emettitore per un flusso costante
|
||
(`emitterStart`), lo fa esplodere per uno sbuffo singolo (`emitterBurst`), o
|
||
entrambe le cose. Il motore di gioco muove ogni particella viva a ogni
|
||
fotogramma, e smette di muoverle mentre il gioco è in pausa.
|
||
|
||
Un'unica API degli emettitori serve l'overlay e la scena 3D; cambiano solo le
|
||
unità e il disegno. Tutto ciò che si dà come minimo e massimo
|
||
(`emitterSetLife`, `emitterSetSpeed`, `emitterSetSize`) viene estratto a caso
|
||
tra i due per ogni particella.
|
||
|
||
[#_2d_emitters]
|
||
==== Emettitori 2D
|
||
|
||
`emitterNew()` senza argomenti crea un emettitore 2D. Vive in coordinate
|
||
dell'overlay, con Y verso il basso, quindi la gravità di una fontana è una Y
|
||
positiva. Spostalo con `emitterSetPosition`, e disegnalo da `onOverlayUpdate`
|
||
con `emitterDraw`. L'overlay stesso è un'immagine che dipinge la CPU, quindi le
|
||
particelle non vi vengono dipinte dentro: le disegna la GPU, o sopra l'intero
|
||
overlay (`PARTICLE_OVER`, il predefinito) o sotto di esso e sopra il video e la
|
||
scena 3D (`PARTICLE_UNDER`, per il fumo dietro il punteggio). Gli emettitori 2D
|
||
girano su ogni macchina su cui gira Singe, compreso il Raspberry Pi 3, e si
|
||
sposano bene con la fisica 2D: leggi la posizione del nodo di un corpo e
|
||
passala a `emitterSetPosition`.
|
||
|
||
[source,lua]
|
||
----
|
||
sparks = emitterNew()
|
||
emitterSetBlend(sparks, PARTICLE_ADD)
|
||
emitterSetLife(sparks, 0.3, 0.7)
|
||
emitterSetSpeed(sparks, 80, 200)
|
||
emitterSetDirection(sparks, 0, -1) -- Verso l'alto dello schermo.
|
||
emitterSetSpread(sparks, 70)
|
||
emitterSetGravity(sparks, 0, 500)
|
||
emitterSetSize(sparks, 5, 1)
|
||
emitterSetColor(sparks, 255, 255, 200, 255, 255, 120, 0, 0)
|
||
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
emitterSetPosition(sparks, grinderX, grinderY)
|
||
emitterDraw(sparks)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#_3d_emitters]
|
||
==== Emettitori 3D
|
||
|
||
`emitterNew(node)` crea un emettitore 3D collegato a un nodo. Le particelle
|
||
nascono nella posizione del nodo nel mondo (sparse su `emitterSetRadius`), in
|
||
unità del mondo, e si disegnano nella scena come billboard rivolti verso la
|
||
telecamera dopo ogni mesh: con test di profondità contro la geometria, senza
|
||
mai scrivere la profondità, non illuminate se non richiesto, senza proiettare
|
||
né ricevere ombre. Per impostazione predefinita una particella resta dove è
|
||
nata una volta lasciato l'emettitore (il fumo continua a fluttuare dove è stato
|
||
prodotto mentre la torcia si sposta); `emitterSetLocal` fa invece seguire il
|
||
nodo all'intera nube. Eliminare il nodo elimina i suoi emettitori.
|
||
|
||
[source,lua]
|
||
----
|
||
flame = emitterNew(torchTip)
|
||
emitterSetBlend(flame, PARTICLE_ADD)
|
||
emitterSetRate(flame, 140)
|
||
emitterSetLife(flame, 0.4, 0.9)
|
||
emitterSetSpeed(flame, 0.6, 1.4)
|
||
emitterSetDirection(flame, 0, 1, 0)
|
||
emitterSetSpread(flame, 15)
|
||
emitterSetSize(flame, 0.35, 0.05, 0.3)
|
||
emitterSetColor(flame, 255, 200, 90, 255, 255, 40, 0, 0)
|
||
emitterStart(flame)
|
||
----
|
||
|
||
[#_the_picture]
|
||
==== L'immagine
|
||
|
||
Una particella è un quad con texture. Senza texture è un disco sfumato fornito
|
||
dal motore di gioco; `emitterSetTexture` le dà invece uno sprite caricato (uno
|
||
sbuffo, una scia di scintilla, un fiocco di neve), e per uno sprite animato
|
||
`emitterSetFrames` sceglie un fotogramma a caso per ogni particella, così un
|
||
solo atlante di sbuffi dà una nube varia. La fusione è normale
|
||
(`PARTICLE_ALPHA`) o additiva (`PARTICLE_ADD`, per tutto ciò che brilla).
|
||
`emitterSetColor` dà il colore e l'alfa alla nascita e alla morte, e la
|
||
particella sfuma dall'uno all'altro nel corso della sua vita; `emitterSetSize`
|
||
fa lo stesso per la sua dimensione, con un terzo valore per la velocità con cui
|
||
ruota.
|
||
|
||
[#_trails_collisions_lit_and_soft_particles]
|
||
==== Scie, collisioni, particelle illuminate e morbide
|
||
|
||
`emitterSetTrail(emitter, length, width)` conserva le ultime posizioni di ogni
|
||
particella e disegna un nastro che le attraversa, sfumando verso la coda:
|
||
scintille, proiettili traccianti, comete, in 2D come in 3D. `emitterSetCollide`
|
||
fa rimbalzare le particelle: su un pavimento orizzontale a un'altezza che
|
||
indichi tu (`COLLIDE_FLOOR`, gratuito), o sul mondo fisico tramite raycast
|
||
(`COLLIDE_SCENE`, solo 3D, con un tetto per fotogramma così che un grande
|
||
emettitore distribuisca il costo su qualche fotogramma).
|
||
|
||
Altre due sono solo per il 3D. `emitterSetLit` ombreggia ogni particella
|
||
secondo le luci e il cielo della scena come una piccola sfera rivolta verso la
|
||
telecamera, così il fumo prende il colore di un braciere e la polvere luccica
|
||
al sole (le ombre non vengono lette). `emitterSetSoftness` fa svanire una
|
||
particella lungo una certa distanza dove incontra la geometria, così il fumo
|
||
non taglia un pavimento né una fontana la sua vasca; costa una passata di
|
||
profondità in più della scena per fotogramma finché un emettitore la usa.
|
||
|
||
[source,lua]
|
||
----
|
||
smoke = emitterNew(brazier)
|
||
emitterSetTexture(smoke, spriteLoad(DIR .. "puff.png"))
|
||
emitterSetRate(smoke, 30)
|
||
emitterSetLife(smoke, 2, 4)
|
||
emitterSetSpeed(smoke, 0.3, 0.6)
|
||
emitterSetDirection(smoke, 0, 1, 0)
|
||
emitterSetSize(smoke, 0.3, 1.2, 0.2)
|
||
emitterSetColor(smoke, 90, 90, 90, 160, 60, 60, 60, 0)
|
||
emitterSetLit(smoke, true) -- Prende il colore del fuoco da sotto.
|
||
emitterSetSoftness(smoke, 0.4) -- Nessun bordo netto dove tocca la conca.
|
||
emitterStart(smoke)
|
||
|
||
embers = emitterNew(brazier)
|
||
emitterSetBlend(embers, PARTICLE_ADD)
|
||
emitterSetTrail(embers, 6, 0.02) -- Una breve scia dietro ogni brace.
|
||
emitterSetCollide(embers, COLLIDE_FLOOR, 0.3, 0.8, 0) -- Rimbalza sul terreno a Y = 0.
|
||
emitterStart(embers)
|
||
----
|
||
|
||
[#_cost]
|
||
==== Costo
|
||
|
||
Le particelle costano poco da simulare e costano quanto coprono sullo schermo.
|
||
Qualche migliaio di particelle piccole non sono niente; è una manciata di
|
||
particelle additive grandi su tutto lo schermo a rallentare un Raspberry Pi 4.
|
||
La riserva ha un tetto fissato da `emitterSetMax` (predefinito 1000); quando è
|
||
piena, non ne nascono altre finché alcune non muoiono.
|
||
|
||
[#authoring]
|
||
=== Descrivere un gioco invece di scriverlo
|
||
|
||
Un gioco può essere scritto come *descrizione* -- una tabella di livelli, entità e
|
||
regole -- e compilato in un normale gioco Singe da Forge, lo strumento di
|
||
creazione. Forge è un progetto separato, con il suo download e il suo libro.
|
||
|
||
|
||
[[migrating]]
|
||
=== Migrazione da Singe 2.10
|
||
|
||
Singe 3.00 ha spostato l'handle dello sprite al primo argomento di `spriteDraw`,
|
||
`spriteLoop`, `spriteQuality`, `spriteRotate`, `spriteRotateAndScale`,
|
||
`spriteScale` e `spriteSetFrame`, così ogni chiamata agli sprite ora corrisponde
|
||
alla famiglia `video*`. Per aggiornare un gioco, sposta in testa l'ultimo
|
||
argomento di ciascuna di quelle chiamate:
|
||
|
||
[source,lua]
|
||
----
|
||
spriteDraw(x, y, cursor) -- 2.10
|
||
spriteDraw(cursor, x, y) -- 3.00
|
||
spriteRotate(angle, cursor) -- 2.10
|
||
spriteRotate(cursor, angle) -- 3.00
|
||
----
|
||
|
||
Un gioco che non puoi modificare può invece scegliere il vecchio ordine. O
|
||
imposti la variabile globale prima di caricare il framework:
|
||
|
||
[source,lua]
|
||
----
|
||
SINGE_LEGACY_SPRITE_ARGS = true
|
||
dofile("Singe/Framework.singe")
|
||
----
|
||
|
||
oppure aggiungi `LEGACY_SPRITE_ARGS = true` alla sua voce in `games.dat`.
|
||
Nient'altro ha cambiato forma, ma alcuni comportamenti sì:
|
||
|
||
* Handle e conteggi vengono restituiti come interi Lua; `videoIsPlaying`
|
||
restituisce un booleano; `mouseSetMode` non restituisce nulla.
|
||
* Gli errori dentro le callback ora terminano il gioco con un traceback invece
|
||
di essere stampati e ignorati, come si sono sempre comportati gli errori
|
||
negli argomenti.
|
||
* I tasti tenuti premuti non si ripetono più in `MODE_NORMAL`.
|
||
* Accanto al gioco non viene scritto nulla. Senza `--datadir` la cartella dei
|
||
dati è `data/<game>/` sotto la cartella dei giochi, mentre la 2.x usava la
|
||
cartella del gioco stesso; salvataggi, impostazioni e punteggi record vanno
|
||
sotto `singeGetDataPath()`.
|
||
* `math.randomseed` accetta un seme frazionario, come faceva con il Lua della
|
||
2.10; viene arrotondato per difetto prima che il generatore di Lua 5.4 lo
|
||
veda. `os.clock` è il tempo reale trascorso dall'avvio del motore di gioco,
|
||
non il tempo di processore.
|
||
* Il disco viene posizionato sul fotogramma 1 dal motore di gioco prima che il
|
||
tuo script parta; `Framework.singe` non fa più un salto quando viene caricato.
|
||
* `onMouseMoved` in modalità `MANY_MOUSE` passa il movimento reale nel terzo e
|
||
nel quarto argomento, e `onControllerMoved` passa l'indice del controller
|
||
(da `0` a `3`) invece dell'ID di istanza di SDL.
|
||
* `mouseEnable` / `mouseDisable` e `singeEnablePauseKey` /
|
||
`singeDisablePauseKey` funzionano ancora, come alias di `mouseSetEnabled` e
|
||
`singeSetPauseKeyEnabled`.
|
||
* Il laserdisc non è più dato per scontato. Una voce di `games.dat` con un
|
||
`VIDEO` è un gioco laserdisc, come prima; una senza gira invece su un canvas.
|
||
Dalla riga di comando passa `--disc` (o `--framefile`), perché un video
|
||
trovato accanto allo script non implica più un disco. Vedi
|
||
<<withoutadisc,Giochi senza disco>>.
|
||
* Il tasto di pausa ora mette in pausa l'intero gioco, non solo i media: lo
|
||
script resta congelato finché il tasto non viene premuto di nuovo, e
|
||
`SWITCH_PAUSE` viene consegnato allo script solo quando il tasto è stato
|
||
disabilitato. Agisce alla pressione del tasto anziché al rilascio. Vedi
|
||
<<pausing,La pausa>>.
|
||
* `fontPrint` conserva le stringhe che disegna, e `overlayClear` ridipinge solo
|
||
ciò che è stato disegnato dall'ultima pulizia, quindi un HUD ristampato a
|
||
ogni fotogramma costa poco. Disegnare il testo con `fontToSprite` per le
|
||
prestazioni è deprecato; funziona ancora, ed è ancora il modo per ottenere un
|
||
testo che puoi misurare, scalare o ruotare. Vedi <<fonttosprite,fontToSprite>>.
|
||
|
||
[#_engine_constants]
|
||
=== Costanti del motore
|
||
|
||
Il motore definisce queste variabili globali prima che parta qualsiasi script,
|
||
quindi sono disponibili sia a `controls.cfg` sia a `Framework.singe`:
|
||
|
||
[cols="2,3",options="header"]
|
||
|===
|
||
| Costante | Significato
|
||
| `SWITCH_UP` ... `SWITCH_RECORD` | Valori passati a `onInputPressed` / `onInputReleased` in `MODE_NORMAL`. `SWITCH_MOUSE_DISCONNECT` lo genera il motore stesso quando un mouse o una pistola ottica viene scollegato, una volta come pressione e una volta come rilascio; nessun tasto o pulsante lo produce. `SWITCH_RECORD` è l'interruttore di registrazione, su cui il motore agisce da sé, come fa con l'interruttore dello screenshot.
|
||
| `FONT_QUALITY_SOLID`, `FONT_QUALITY_SHADED`, `FONT_QUALITY_BLENDED` | Argomenti per `fontQuality`.
|
||
| `MODE_NORMAL`, `MODE_FULL` | Argomenti per `keyboardSetMode`.
|
||
| `MOUSE_SINGLE`, `MOUSE_MANY` (anche `SINGLE_MOUSE`, `MANY_MOUSE`) | Argomenti per `mouseSetMode`.
|
||
| `OVERLAY_NOT_UPDATED`, `OVERLAY_UPDATED` | Valori di ritorno per `onOverlayUpdate`.
|
||
| `RENDER_PIXELATED`, `RENDER_SMOOTH` | Argomenti per `spriteQuality` / `videoQuality`.
|
||
| `SPRITE_ALPHA`, `SPRITE_ADD`, `SPRITE_MULTIPLY` | Fusione per `spriteSetBlend`.
|
||
| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED`, `DISC_EJECTED` | Valori restituiti da `discGetState`.
|
||
| `LIGHT_DIRECTIONAL`, `LIGHT_POINT`, `LIGHT_SPOT` | Argomenti per `lightNew`.
|
||
| `BODY_STATIC`, `BODY_DYNAMIC`, `BODY_KINEMATIC` | Tipi di corpo per `bodyNew`.
|
||
| `SHAPE_BOX`, `SHAPE_SPHERE`, `SHAPE_CAPSULE`, `SHAPE_CYLINDER`, `SHAPE_HULL`, `SHAPE_MESH` | Forme di collisione per `bodyNew`.
|
||
| `JOINT_HINGE`, `JOINT_BALL`, `JOINT_SLIDER` | Tipi di giunto (le chiamate `joint*` li nominano direttamente).
|
||
| `DEBUG_NONE`, `DEBUG_SHAPES`, `DEBUG_CONSTRAINTS`, `DEBUG_CONTACTS`, `DEBUG_VELOCITIES`, `DEBUG_STATIC`, `DEBUG_ALL` | Ciò che disegna `physicsSetDebug`; sommali.
|
||
| `PARTICLE_ALPHA`, `PARTICLE_ADD` | Fusione per `emitterSetBlend`.
|
||
| `PARTICLE_OVER`, `PARTICLE_UNDER` | Dove si disegnano le particelle 2D, per `emitterSetLayer`.
|
||
| `COLLIDE_NONE`, `COLLIDE_FLOOR`, `COLLIDE_SCENE` | Su che cosa rimbalzano le particelle, per `emitterSetCollide`.
|
||
| `BILLBOARD_NONE`, `BILLBOARD_ALL`, `BILLBOARD_Y` | Come un nodo si rivolge alla telecamera, per `nodeSetBillboard`.
|
||
| `LAYER_SCENE`, `LAYER_FRONT` | Se un nodo si disegna sotto l'overlay o davanti a esso, per `nodeSetLayer`.
|
||
| `VEHICLE_CAR`, `VEHICLE_MOTORCYCLE`, `VEHICLE_TANK`, `VEHICLE_BOAT` | Tipi di veicolo per `vehicleNew`.
|
||
| `SOFT_CLOTH`, `SOFT_BODY`, `SOFT_ROPE` | Tipi di corpo morbido per `softNew`.
|
||
| `FILTER_LINEAR`, `FILTER_NEAREST` | Campionamento delle texture per `materialSetFilter`.
|
||
| `TONEMAP_NEUTRAL`, `TONEMAP_ACES`, `TONEMAP_NONE` | Curve tonali per `sceneSetTonemap`.
|
||
| `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, ciò che `soundPlay` restituisce quando nessun canale è libero.
|
||
| `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | La versione del motore, come interi, come stringa (`"v3.00"`) e come il numero che restituisce `singeVersion()`.
|
||
| `SINGE_DEAD_ZONE` | Il `DEAD_ZONE` di `controls.cfg`, in unità grezze dell'asse.
|
||
| `SINGE_TRIGGER_THRESHOLD` | La soglia dei grilletti analogici, nelle stesse unità grezze; `0` quando i grilletti usano `SINGE_DEAD_ZONE`, che è il predefinito.
|
||
| `SINGE_LEGACY_SPRITE_ARGS` | Vero quando il gioco ha chiesto l'ordine degli argomenti degli sprite della 2.10.
|
||
| `SINGE_DISC` | Vero quando il gioco ha un laserdisc; falso quando il mondo è il canvas.
|
||
| `SINGE_GAMEPAD_BASE`, `SINGE_GAMEPAD_STRIDE`, `SINGE_AXIS_STRIDE`, `SINGE_GAMEPAD_BUTTON_OFFSET`, `SINGE_MOUSE_BASE`, `SINGE_MOUSE_STRIDE`, `SINGE_MAX_CONTROLLERS`, `SINGE_MAX_MICE`, `SINGE_CONTROLLER_AXES`, `SINGE_CONTROLLER_BUTTONS` | La disposizione dei codici di input di controller e mouse, e quanti assi e pulsanti ha un controller; `Framework.singe` costruisce da essi le tabelle `GAMEPAD_N` e `MOUSE_N`, e una schermata di servizio li usa per percorrere un controller.
|
||
|
||
| `SINGE_SCALE_MIN`, `SINGE_SCALE_MAX`, `SINGE_SHIFT_MIN`, `SINGE_SHIFT_MAX`, `SINGE_ROTATE_STEP` | Ciò che accettano `vldpSetScale`, `vldpSetShift` e `vldpSetRotate`. Limita i valori a queste costanti anziché a copie tue: uno spostamento fuori intervallo termina lo script.
|
||
|===
|
||
|
||
`Framework.singe` aggiunge le tabelle `SCANCODE` e `MODIFIER` (i valori di
|
||
tasti e modificatori di SDL), da `GAMEPAD_0` a `GAMEPAD_3`, da `MOUSE_0` a
|
||
`MOUSE_3`, gli indici `GAMEPAD_AXIS_*` per `controllerGetAxis`, e `DIR`.
|
||
|
||
[#_included_libraries]
|
||
=== Librerie incluse
|
||
|
||
Oltre alle librerie standard di Lua e all'API di Singe, nei programmi Singe sono
|
||
disponibili anche le seguenti librerie, senza bisogno di installare altro
|
||
software:
|
||
|
||
[cols="1,3",options="header"]
|
||
|===
|
||
| Libreria | Scopo
|
||
| Copas | Rete asincrona
|
||
| LuaFileSystem | Supporto esteso al file system
|
||
| json.lua | Codifica e decodifica JavaScript Object Notation
|
||
| binaryheap | Struttura dati heap binario
|
||
| timerwheel | Pianificazione efficiente dei timer
|
||
| LuaSec | Comunicazione TLS / SSL
|
||
| LuaSocket | TCP e UDP
|
||
| LuaRS232 | Accesso alla porta seriale RS232
|
||
| lsqlite3 | Database SQLite, `require("sqlite3")`
|
||
| md5 | `require("md5")` per `sum`, `hexsum`, `exor`, `crypt` e `decrypt`; la libreria PUC-Rio inclusa in Hypseus, che ne chiama `hexsum` il `sumhexa`
|
||
| LuaBitOp | `require("bit")` per le operazioni bit a bit di Lua 5.1, che la 5.4 ha come operatori
|
||
| lrandom | `require("random")` per un generatore Mersenne Twister, separato da `math.random`
|
||
| Lua CJSON | `require("cjson")` per codifica e decodifica JSON, accanto al modulo `json` qui sopra
|
||
| LPeg | `require("lpeg")` per le parsing expression grammar
|
||
| middleclass | `require("middleclass")` per le classi, se preferisci costruire le entità con quelle anziché con le tabelle
|
||
| lume | `require("lume")` per le piccole cose che servono a ogni gioco: `lerp`, `clamp`, `round`, `shuffle`, `split`, `serialize`, `weightedchoice`
|
||
| inspect | `require("inspect")` per stampare una tabella in una forma che una persona riesce a leggere, per quando il debugger è `debugPrint`
|
||
| bump | `require("bump")` per muovere cose che non devono attraversarsi a vicenda -- dice dove va a finire chi si muove, cosa che <<collide,le chiamate collide>> deliberatamente non fanno
|
||
| MobDebug | `require("mobdebug")`, il debugger con cui parla <<zerobrane,ZeroBrane Studio>>; `--debug` lo avvia, quindi a un gioco non serve nessuna riga sua
|
||
| RmlUi | Il modello a oggetti della GUI stessa, la variabile globale `rmlui` (vedi <<gui,GUI>>)
|
||
|===
|
||
|
||
Il loro uso va oltre lo scopo di questo documento. Le librerie md5, LuaBitOp,
|
||
lrandom, Lua CJSON e LPeg ci sono perché Hypseus le espone, così un gioco scritto per Hypseus trova gli stessi
|
||
moduli con gli stessi nomi; niente in Singe ne ha bisogno.
|
||
|
||
[#audiosync]
|
||
=== Sincronizzazione dell'audio
|
||
|
||
Singe presenta il video rispetto all'orologio dell'audio: un fotogramma viene
|
||
mostrato quando si sente il suono che gli appartiene. Il motore misura il
|
||
proprio percorso audio all'avvio. SDL non può riferire quanto audio è in coda
|
||
tra il mixer e l'altoparlante, quindi Singe svuota il dispositivo una volta,
|
||
prima che si senta qualsiasi cosa, e conta la raffica di buffer che SDL usa per
|
||
riempirlo di nuovo; quella raffica è la profondità della coda, e l'orologio la
|
||
sottrae. Avvia con `--program` per vedere la misura nella traccia
|
||
(`Audio device queue: ...`).
|
||
|
||
Ciò che il motore non può vedere è tutto quello che sta a valle del server
|
||
audio del sistema operativo: un DAC, un ricevitore HDMI, un collegamento
|
||
Bluetooth. Questi aggiungono un ritardo fisso che varia da macchina a macchina.
|
||
Se le azioni sullo schermo precedono visibilmente il loro suono, passa i
|
||
millisecondi mancanti con `--audiodelay=MS`, metti `AUDIO_DELAY
|
||
= MS` in `games.dat`, o lascia che il gioco lo imposti da un menu di servizio
|
||
con <<singesetaudiodelay,singeSetAudioDelay>>. I valori positivi significano
|
||
che l'audio è in ritardo; il video viene trattenuto per allinearsi. Un valore
|
||
negativo gestisce il raro caso di un audio che arriva in anticipo.
|
||
|
||
[#menucalibration]
|
||
==== Calibrare dal menu
|
||
|
||
Il menu incluso ha una schermata di calibrazione per quel ritardo a valle.
|
||
Premi il tasto assegnato a `INPUT_SERVICE` (di base il tasto `9`) sull'elenco
|
||
dei giochi e scegli Audio Delay dagli strumenti di servizio (vedi xref:Manual.adoc#servicetools[Gli strumenti
|
||
di servizio]). La schermata è una pagina del documento del menu: copre il menu
|
||
di nero, mostra il ritardo in prova e la coda del dispositivo misurata dal
|
||
motore, riproduce un clic una volta al secondo e diventa bianca per un
|
||
fotogramma a ogni lampo. Regola con sinistra e destra (10 ms) e con su e giù
|
||
(1 ms) finché il lampo e il clic non avvengono insieme, poi premi il pulsante 1
|
||
per salvare. Start azzera, e il pulsante 2 (o di nuovo il tasto di servizio)
|
||
annulla. Il valore viene salvato in `machine.cfg` nella radice dei dati e
|
||
applicato dal motore a ogni gioco su quella macchina, che sia avviato dal menu
|
||
o dalla riga di comando; si somma a qualsiasi `AUDIO_DELAY` del singolo gioco.
|
||
Ricalibra dopo aver cambiato altoparlanti, cuffie o schermi.
|
||
|
||
La schermata funziona perché il clic e l'audio del disco condividono un solo
|
||
dispositivo mixer e una sola coda: il lampo è programmato dopo il clic a una
|
||
distanza pari alla coda misurata più il valore candidato, quindi quando i due
|
||
coincidono il candidato è esattamente il ritardo che il motore non può vedere.
|
||
Il ritardo dello schermo viene compreso gratis. Un gioco può leggere i valori
|
||
con <<singegetaudiocalibration,singeGetAudioCalibration>> e
|
||
<<singegetaudiolatency,singeGetAudioLatency>>, o offrire una propria schermata
|
||
con <<singesetaudiocalibration,singeSetAudioCalibration>>.
|
||
|
||
Contano anche due proprietà del file video stesso. L'audio AAC porta con sé
|
||
dei campioni di priming dell'encoder che il contenitore deve dire ai decoder di
|
||
saltare; i file multiplexati senza quell'informazione (un MP4 senza edit list,
|
||
un file Matroska senza codec delay) riproducono l'audio circa 20 millisecondi
|
||
in ritardo, cosa che `AUDIO_DELAY` può assorbire. E quando il primo fotogramma
|
||
video ha una marca temporale successiva al primo campione audio, Singe rispetta
|
||
la tempistica del contenitore, così l'audio iniziale non va perso.
|
||
|
||
[#formats]
|
||
=== Formati video, audio e dei contenitori
|
||
|
||
Singe decodifica il video direttamente con le librerie di FFmpeg, usando il decoder hardware
|
||
della piattaforma quando ne offre uno per il codec (VA-API o VDPAU su Linux, D3D11VA su
|
||
Windows, VideoToolbox su macOS, e su una scheda ARM i decoder V4L2 -- quello
|
||
memory-to-memory, che su un Pi 4 copre l'H.264, quello stateless della Request API
|
||
che un Pi 5 usa per l'HEVC, e quello di Rockchip su un RK3588), e altrimenti il
|
||
proprio decoder software; la traccia del programma riporta quale, e `--softwarevideo`
|
||
forza il software. Qualsiasi contenitore
|
||
e codec che
|
||
l'FFmpeg incluso sa demultiplexare e decodificare verrà riprodotto: MP4, MKV, program stream MPEG,
|
||
AVI, AV1 e i classici elementary stream `.m2v` di Daphne con un file audio `.ogg`
|
||
corrispondente accanto. Ogni traccia audio del file è disponibile per
|
||
`discSetAudioTrack` / `videoSetAudioTrack`; tutte le tracce devono condividere lo
|
||
stesso formato dei campioni, numero di canali e frequenza.
|
||
|
||
Un laserdisc conteneva semiquadri interlacciati, e un rip che li ha conservati
|
||
mostra l'effetto pettine su ogni schermo progressivo. `--deinterlace` decide che
|
||
cosa succede: `auto`, il predefinito, applica bwdif ai fotogrammi che il file
|
||
segna come interlacciati e lascia stare tutto il resto, così un disco
|
||
progressivo non paga nulla; `on` è per un file i cui flag sono sbagliati; `off`
|
||
non tocca mai l'immagine. Esce un'immagine per ognuna che entra -- un disco è
|
||
indirizzato per numero di fotogramma, quindi un deinterlacciatore che
|
||
trasformasse ogni fotogramma in due semiquadri sposterebbe ogni fotogramma
|
||
nell'indice.
|
||
|
||
La prima volta che un video viene aperto, Singe lo indicizza e salva l'indice
|
||
accanto agli altri dati del gioco (`<name>-<hash>.index`, con l'hash ricavato
|
||
dal nome completo del video, così due video con lo stesso nome base mantengono indici separati). L'indicizzazione è una passata sul file
|
||
senza decodifica, pochi secondi anche per un disco lungo quanto un film, e si
|
||
ripete se il video cambia. Quando un video viene caricato, Singe
|
||
riporta la spaziatura dei suoi fotogrammi chiave nella traccia del programma
|
||
(`--program`), con un avviso se i fotogrammi chiave distano più di due secondi,
|
||
perché un salto deve decodificare in avanti dal fotogramma chiave precedente. La decodifica avviene su un thread separato,
|
||
quindi un salto lento mostra il fotogramma precedente un po' più a lungo invece
|
||
di bloccare il gioco.
|
||
|
||
I segmenti di un framefile vengono aperti man mano che il disco li raggiunge, non
|
||
tutti insieme: quello in riproduzione, quello successivo, e lo spazio perché un
|
||
salto passi da uno all'altro. Un framefile lungo può avere centinaia di
|
||
segmenti -- un gioco pubblicato ne ha 213 -- e tenere aperto un decoder per
|
||
ognuno di essi costa centinaia di file aperti senza alcun guadagno. Un framefile che nomina
|
||
un file che non c'è lo segnala comunque all'avvio.
|
||
|
||
Per il materiale da laserdisc i vincoli sono la precisione al fotogramma e la
|
||
velocità dei salti, non la compressione. L'H.264 in MP4 o MKV con un intervallo
|
||
breve tra i fotogrammi chiave (uno o due secondi) salta rapidamente e si
|
||
riproduce su ogni piattaforma supportata; intervalli lunghi rendono `discSearch`
|
||
visibilmente lento, perché il decoder deve avanzare dal fotogramma chiave
|
||
precedente. La definizione standard (720x480 o 720x576) è il punto ideale. Le
|
||
sorgenti ad alta definizione funzionano ma costano proporzionalmente più CPU su
|
||
hardware di classe Raspberry Pi, e l'overlay è per impostazione predefinita a
|
||
metà della risoluzione del video, quindi un video sovradimensionato porta poco.
|
||
|
||
Gli effetti sonori e la musica vengono letti prima da SDL_mixer, e da FFmpeg per
|
||
tutto ciò che quello rifiuta, quindi insieme coprono:
|
||
|
||
* WAV, AIFF, AU e VOC per l'audio non compresso. I file WAV brevi non compressi
|
||
danno la latenza più bassa, e sono ciò che dovrebbe essere uno sparo.
|
||
* Ogg Vorbis, Opus, MP3, FLAC e WavPack.
|
||
* AAC (`.m4a`, `.aac`), ALAC, AC-3, WMA, APE, TTA, Speex e AMR -- tutto il resto
|
||
che l'FFmpeg incluso decodifica.
|
||
* Moduli tracker tramite libxmp: MOD, S3M, XM e IT, e una cinquantina di altri,
|
||
tra cui 669, AMF, MED, OKT, PTM, STM e ULT.
|
||
* Chiptune tramite game-music-emu: NSF e NSFE, SPC, VGM, GBS, AY, GYM, HES, KSS
|
||
e SAP. La forma VGZ compressa con gzip non viene letta; decomprimila prima in VGM. Un chiptune che non dice quanto dura suona finché non tace, il che può non accadere mai, quindi viene decodificato man mano che suona invece che subito.
|
||
* MIDI, sintetizzato da un SoundFont. Vedi <<midi,MIDI>>.
|
||
|
||
La riga `Audio:` dell'intestazione della traccia (`--program` o `--trace`) elenca
|
||
i decoder che la build contiene davvero.
|
||
|
||
Le immagini funzionano allo stesso modo: prima SDL_image, poi FFmpeg per il
|
||
resto. Questo copre PNG, JPEG, WebP, GIF, BMP, TGA, PCX, PNM, LBM, QOI, XCF, XPM e
|
||
SVG da SDL_image, e TIFF, OpenEXR, JPEG 2000, DDS, PSD, DPX, AVIF e HEIC da
|
||
FFmpeg. Una GIF o una WEBP animata si carica come animazione di sprite; vedi
|
||
<<spriteload,`spriteLoad`>>, che accetta anche una dimensione per rasterizzare un SVG.
|
||
Radiance `.hdr` e OpenEXR funzionano entrambi come cielo (<<scenesetsky,`sceneSetSky`>>)
|
||
e conservano tutta la loro gamma.
|
||
|
||
I font vengono letti da FreeType: TrueType, OpenType, WOFF, WOFF2, Type 1, BDF e
|
||
PCF, con glifi a colori (sia di tipo bitmap sia vettoriali) e shaping HarfBuzz
|
||
per l'arabo, l'ebraico e le scritture indiane.
|
||
|
||
[#reference]
|
||
== Riferimento dell'API Lua
|
||
|
||
Questo capitolo documenta l'API Lua esposta dal motore Singe -- ogni funzione
|
||
che lo script del tuo gioco può chiamare, e ogni callback che Singe invocherà su
|
||
di te. È rivolto a chi sviluppa nuovi giochi o porta da altri motori giochi
|
||
ricchi di video.
|
||
|
||
[#conventions]
|
||
=== Convenzioni
|
||
|
||
Ogni voce dell'API qui sotto segue la stessa struttura:
|
||
|
||
* *Firma (o firme)* in un blocco di codice, con ogni forma accettata.
|
||
* *Descrizione* di ciò che fa la funzione e di eventuali effetti collaterali.
|
||
* *Parametri* elencati nel testo, o in una tabella quando sono più di due.
|
||
* Riga *Restituisce* che descrive il valore restituito a Lua (omessa quando la funzione non restituisce nulla).
|
||
* *Note*, se ci sono insidie da segnalare.
|
||
* *Disponibile da* -- la versione di Singe in cui la funzione è comparsa per la prima volta.
|
||
* *Vedi anche* -- funzioni correlate che probabilmente userai nello stesso punto del codice.
|
||
* *Esempio* -- un frammento breve e realistico, con la forma del vero codice di un gioco.
|
||
|
||
Una voce che inizia con *Estensione di Hypseus* c'è perché un gioco scritto per
|
||
Hypseus Singe giri qui senza modifiche. Lo stesso vale per una forma degli
|
||
argomenti segnata come di Hypseus su una funzione che per il resto accetta
|
||
quella di Singe.
|
||
|
||
IMPORTANT: *Non scrivere nuovi giochi per l'API di Hypseus.* Quelle voci esistono
|
||
per compatibilità, non come raccomandazione. Diverse di esse rispondono con un
|
||
valore fisso perché la cosa che descrivono in Singe non esiste, alcune sono
|
||
accettate e ignorate, e altre portano scale e ordini degli argomenti ereditati
|
||
da un altro motore -- `videoGetVolume()` conta fino a `64` dove il resto di
|
||
Singe conta fino a `100`, e i nomi `spriteAnim` prendono lo sprite per ultimo
|
||
dove ogni chiamata agli sprite di Singe lo prende per primo. Ogni voce indica
|
||
l'equivalente Singe da usare al suo posto. Il nuovo codice che si attiene
|
||
all'API di Singe è più breve, coerente con se stesso, e non avrà sorprese quando
|
||
una risposta di compatibilità resta costante.
|
||
|
||
Alcune regole valgono per tutta l'API:
|
||
|
||
* *I colori* sono a 8 bit per canale: rosso, verde, blu e opacità sono ciascuno interi da `0` a `255`.
|
||
* *Le coordinate dell'overlay* sono nella risoluzione corrente dell'overlay (impostata da `overlaySetResolution`), non in pixel dello schermo. La risoluzione predefinita dell'overlay è metà della risoluzione del video in ogni dimensione; chiama `overlaySetResolution` per cambiarla.
|
||
* *Gli handle* restituiti dalle funzioni `*Load` (sprite, suoni, font, video) sono interi opachi. Ripassali invariati alle altre funzioni di quello spazio dei nomi. Non farci calcoli.
|
||
* *Gli argomenti non validi terminano lo script.* Se una funzione viene chiamata con il numero sbagliato di argomenti, con tipi sbagliati o con un handle già scaricato, Singe termina lo script in esecuzione con un errore -- non restituisce `nil` né un codice di errore. Convalida l'input dell'utente nel tuo script prima di passarlo all'API.
|
||
* *Si disegna solo da `onOverlayUpdate`.* Le chiamate a qualsiasi funzione `overlay*`, `spriteDraw`, `videoDraw` o `fontPrint` da altre callback non si rifletteranno sullo schermo e possono corrompere l'overlay. È l'unica regola in cui inciampa ogni nuovo sviluppatore Singe.
|
||
|
||
[#animation]
|
||
=== Animazione
|
||
|
||
Queste chiamate guidano le animazioni che un modello porta con sé, per
|
||
istanza. Ognuna accetta il *nodo radice* restituito da `modelInstance`; gli
|
||
altri nodi sollevano un errore, tranne con `animationIsPlaying`, che risponde
|
||
`false`. Le animazioni sono numerate da `1` nell'ordine in cui le elenca
|
||
`modelGetAnimations`, o chiamate con il nome che dà loro il file. Una clip si
|
||
riproduce sul livello base, e fino ad altre quattro possono riprodursi sui
|
||
livelli da `1` a `4` sopra di esso; le chiamate di tempo, pausa, ripresa e salto
|
||
agiscono solo sul livello base. Il tempo è in secondi, avanza con l'orologio di
|
||
al massimo un decimo di secondo per fotogramma, e si ferma mentre il gioco è in
|
||
pausa. Vengono guidati l'animazione dei nodi, lo skinning e
|
||
i pesi dei morph target. Vedi la parte Animazione di
|
||
<<scenes3d,Scene 3D>>.
|
||
|
||
[#animationgettime]
|
||
==== animationGetTime
|
||
|
||
[source,text]
|
||
----
|
||
seconds = animationGetTime(node)
|
||
----
|
||
|
||
Il tempo corrente nella clip del livello base, in riproduzione o in pausa, nei secondi propri della clip (una `speed` di `2` lo fa avanzare due volte più veloce). Una clip in loop torna a `0` alla fine; una che non è in loop si ferma alla sua durata. Se non è stato ancora riprodotto niente dà `0`. Le clip sui livelli superiori non vengono riportate. Un nodo che non è una radice di istanza termina lo script.
|
||
|
||
*Restituisce:* i secondi, come numero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<animationsettime,animationSetTime>>, <<animationisplaying,animationIsPlaying>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Suona il passo una volta ogni volta che il ciclo di camminata supera i suoi due punti di contatto.
|
||
function onOverlayUpdate()
|
||
local t = animationGetTime(hero)
|
||
if (lastStepTime < 0.25 and t >= 0.25) or (lastStepTime < 0.75 and t >= 0.75) then
|
||
soundPlay(footstep)
|
||
end
|
||
lastStepTime = t
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#animationisplaying]
|
||
==== animationIsPlaying
|
||
|
||
[source,text]
|
||
----
|
||
playing = animationIsPlaying(node)
|
||
----
|
||
|
||
Se la clip del livello base sta avanzando su questa istanza. Una clip non in loop smette di riprodursi quando raggiunge la fine, e `animationPause` e `animationStop` rendono entrambe questo valore `false`; una clip in loop resta `true` finché non viene fermata. A differenza delle altre chiamate qui, un nodo che non è una radice di istanza non termina lo script ma risponde `false`.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<animationplay,animationPlay>>, <<animationpause,animationPause>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Torna a riposo quando l'attacco singolo si è esaurito.
|
||
function onOverlayUpdate()
|
||
if attacking and not animationIsPlaying(hero) then
|
||
attacking = false
|
||
animationPlay(hero, "Idle", true, 1, 0.2)
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#animationpause]
|
||
==== animationPause
|
||
|
||
[source,text]
|
||
----
|
||
animationPause(node)
|
||
----
|
||
|
||
Blocca la clip del livello base al suo tempo corrente; i nodi mantengono quella posa finché `animationResume` non la fa continuare o non viene riprodotta un'altra clip. Le clip sui livelli superiori continuano. Mettere in pausa un'istanza che non sta riproducendo niente è innocuo. Un nodo che non è una radice di istanza termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<animationresume,animationResume>>, <<animationsettime,animationSetTime>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una scena d'intermezzo blocca la folla mentre la telecamera si muove.
|
||
function beginCutscene()
|
||
for _, npc in ipairs(crowd) do
|
||
animationPause(npc)
|
||
end
|
||
cameraSet(cutsceneCamera)
|
||
end
|
||
----
|
||
|
||
[#animationplay]
|
||
==== animationPlay
|
||
|
||
[source,text]
|
||
----
|
||
animationPlay(node, nameOrIndex)
|
||
animationPlay(node, nameOrIndex, loop)
|
||
animationPlay(node, nameOrIndex, loop, speed)
|
||
animationPlay(node, nameOrIndex, loop, speed, fade)
|
||
----
|
||
|
||
Avvia dall'inizio una delle clip del modello sul livello base di questa istanza, sostituendo ciò che vi era in riproduzione; la posa si aggiorna subito. Con un `fade` la vecchia clip continua mentre svanisce in quel numero di secondi, e quando non c'era niente in riproduzione la dissolvenza parte dalla posa in cui si trovano ora i nodi (un ragdoll appena disattivato, una posa impostata dallo script), e i nodi che la nuova clip non anima tornano dolcemente alla posa di riposo del modello. Un nodo che non è una radice di istanza, un nome che il modello non ha o un numero fuori dall'intervallo da `1` al numero di clip termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo radice dell'istanza, da `modelInstance`.
|
||
* `nameOrIndex` -- il nome di una clip come lo salva il file, o il suo numero da `1` nell'ordine di `modelGetAnimations`.
|
||
* `loop` -- booleano, predefinito `true`. Con `false` la clip si ferma al suo ultimo fotogramma chiave e `animationIsPlaying` diventa `false`.
|
||
* `speed` -- velocità di riproduzione, predefinita `1`. `2` è due volte più veloce; `0.5` è la metà.
|
||
* `fade` -- secondi di dissolvenza incrociata da ciò che era in riproduzione, predefinito `0` (uno stacco netto).
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<modelgetanimations,modelGetAnimations>>, <<animationplaylayer,animationPlayLayer>>, <<animationstop,animationStop>>, <<animationsettime,animationSetTime>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una guardia che resta a riposo finché non avvista il giocatore, poi corre.
|
||
guardModel = modelLoad(DIR .. "Models/Guard.glb")
|
||
guard = modelInstance(guardModel)
|
||
animationPlay(guard, "Idle")
|
||
|
||
function onSpotted()
|
||
animationPlay(guard, "Run", true, 1.3, 0.4)
|
||
end
|
||
----
|
||
|
||
[#animationplaylayer]
|
||
==== animationPlayLayer
|
||
|
||
[source,text]
|
||
----
|
||
animationPlayLayer(node, layer, nameOrIndex)
|
||
animationPlayLayer(node, layer, nameOrIndex, loop)
|
||
animationPlayLayer(node, layer, nameOrIndex, loop, speed)
|
||
animationPlayLayer(node, layer, nameOrIndex, loop, speed, fade)
|
||
----
|
||
|
||
Riproduce una clip su uno dei quattro livelli sopra il base, fusa sopra ciò che sta sotto secondo il peso del livello (`animationSetLayerWeight`, predefinito `1`) e limitata a una parte dello scheletro quando è impostata una maschera (`animationSetLayerMask`). I livelli più alti si fondono sopra quelli più bassi; ogni livello gestisce la propria clip, il proprio tempo e il proprio loop indipendentemente dal base. `nameOrIndex`, `loop`, `speed` e `fade` significano ciò che significano in `animationPlay`, con una differenza: un `fade` su un livello che era vuoto fa salire il peso del livello da `0` invece di fare una dissolvenza incrociata da una posa. Un livello fuori dall'intervallo da `1` a `4`, un nodo che non è una radice di istanza o una clip che il modello non ha termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo radice dell'istanza.
|
||
* `layer` -- da `1` a `4`.
|
||
* `nameOrIndex` -- il nome o il numero di una clip, come in `animationPlay`.
|
||
* `loop` -- predefinito `true`.
|
||
* `speed` -- predefinito `1`.
|
||
* `fade` -- secondi, predefinito `0`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<animationplay,animationPlay>>, <<animationsetlayermask,animationSetLayerMask>>, <<animationsetlayerweight,animationSetLayerWeight>>, <<animationstop,animationStop>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Cammina con tutto il corpo, si guarda intorno solo con la testa.
|
||
animationPlay(fox, "Walk", true)
|
||
animationPlayLayer(fox, 1, "Survey", true, 1, 0.5)
|
||
animationSetLayerMask(fox, 1, nodeFind("b_Neck_04", fox))
|
||
|
||
-- Più tardi, saluta con un braccio sopra qualunque cosa stiano facendo le gambe.
|
||
function onGreet()
|
||
animationPlayLayer(fox, 2, "Wave", false, 1, 0.2)
|
||
end
|
||
----
|
||
|
||
[#animationresume]
|
||
==== animationResume
|
||
|
||
[source,text]
|
||
----
|
||
animationResume(node)
|
||
----
|
||
|
||
Fa continuare la clip del livello base da dove l'ha bloccata `animationPause`, o da dovunque l'abbia spostata nel frattempo `animationSetTime`. Riprendere una clip già in riproduzione non cambia nulla. Un'istanza senza clip sul livello base (mai riprodotta, o fermata) termina lo script, come un nodo che non è una radice di istanza.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<animationpause,animationPause>>, <<animationsettime,animationSetTime>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function endCutscene()
|
||
for _, npc in ipairs(crowd) do
|
||
animationResume(npc)
|
||
end
|
||
cameraSet(gameCamera)
|
||
end
|
||
----
|
||
|
||
[#animationsetlayermask]
|
||
==== animationSetLayerMask
|
||
|
||
[source,text]
|
||
----
|
||
animationSetLayerMask(node, layer, maskNode)
|
||
animationSetLayerMask(node, layer)
|
||
----
|
||
|
||
Limita un livello a `maskNode` e a tutto ciò che sta sotto di esso, così la clip su quel livello muove solo quella parte del modello e il resto continua a seguire i livelli sottostanti. `maskNode` deve essere uno dei nodi dell'istanza stessa, trovato con `nodeFind(name, node)`; la radice dell'istanza in sé non conta. Omettere `maskNode`, o passare `nil`, toglie il limite. La maschera resta sul livello anche nelle successive chiamate ad `animationPlayLayer`. Un livello fuori dall'intervallo da `1` a `4` termina lo script, e così un nodo che non è una radice di istanza o un `maskNode` che non fa parte di quell'istanza.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo radice dell'istanza.
|
||
* `layer` -- da `1` a `4`.
|
||
* `maskNode` -- un giunto o un nodo dell'istanza; omesso o `nil` per l'intero modello.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<animationplaylayer,animationPlayLayer>>, <<nodefind,nodeFind>>, <<animationsetlayerweight,animationSetLayerWeight>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Punta la parte superiore del corpo verso il bersaglio mentre le gambe continuano a camminare.
|
||
spine = nodeFind("b_Spine02_03", hero)
|
||
animationSetLayerMask(hero, 1, spine)
|
||
animationPlayLayer(hero, 1, "AimForward", true)
|
||
|
||
function onHolster()
|
||
animationStop(hero, 1)
|
||
animationSetLayerMask(hero, 1)
|
||
end
|
||
----
|
||
|
||
[#animationsetlayerweight]
|
||
==== animationSetLayerWeight
|
||
|
||
[source,text]
|
||
----
|
||
animationSetLayerWeight(node, layer, weight)
|
||
----
|
||
|
||
Quanto si vede la clip di un livello sopra ciò che sta sotto, da `0` (per niente) a `1` (del tutto, il predefinito); i valori fuori da quell'intervallo vengono limitati. Il peso appartiene al livello, non alla clip, quindi sopravvive ad `animationPlayLayer` e ad `animationStop` su quel livello. Cambialo un poco a ogni fotogramma per far apparire o svanire un livello a mano, o lascia che lo faccia una volta un `fade` su `animationPlayLayer`. Un livello fuori dall'intervallo da `1` a `4` o un nodo che non è una radice di istanza termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<animationplaylayer,animationPlayLayer>>, <<animationsetlayermask,animationSetLayerMask>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La zoppia peggiora man mano che la salute cala.
|
||
function onHealthChanged(health)
|
||
local limp = 1 - health / maxHealth
|
||
animationSetLayerWeight(hero, 1, limp)
|
||
end
|
||
----
|
||
|
||
[#animationsettime]
|
||
==== animationSetTime
|
||
|
||
[source,text]
|
||
----
|
||
animationSetTime(node, seconds)
|
||
----
|
||
|
||
Fa saltare la clip del livello base a un tempo nei suoi secondi; i valori negativi diventano `0`. La posa si aggiorna subito, che la clip sia in riproduzione o no, così un'istanza in pausa può essere portata a un fotogramma esatto, e una in riproduzione continua dal nuovo tempo. Qualsiasi dissolvenza incrociata in corso sul livello base viene interrotta. Un'istanza senza clip sul livello base termina lo script, come un nodo che non è una radice di istanza.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<animationgettime,animationGetTime>>, <<animationpause,animationPause>>, <<animationresume,animationResume>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una porta la cui clip di apertura segue una leva trascinata dal giocatore.
|
||
animationPlay(door, "Open", false)
|
||
animationPause(door)
|
||
|
||
function onLeverMoved(fraction)
|
||
animationSetTime(door, fraction * doorClipLength)
|
||
end
|
||
----
|
||
|
||
[#animationstop]
|
||
==== animationStop
|
||
|
||
[source,text]
|
||
----
|
||
animationStop(node)
|
||
animationStop(node, layer)
|
||
----
|
||
|
||
Ferma e dimentica ogni clip sull'istanza, o solo la clip su un livello sopra il base (da `1` a `4`); il livello base da solo non può essere fermato senza i livelli sopra di esso. I nodi mantengono la loro ultima posa, da cui un successivo `animationPlay` con un `fade` esce dolcemente. Dopo aver fermato tutto, `animationResume` e `animationSetTime` terminano lo script finché non si riproduce di nuovo qualcosa. I pesi e le maschere dei livelli vengono conservati. Un livello fuori dall'intervallo da `1` a `4` o un nodo che non è una radice di istanza termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<animationplay,animationPlay>>, <<animationplaylayer,animationPlayLayer>>, <<animationpause,animationPause>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Passa il corpo al ragdoll della fisica: blocca la posa, poi lascialo cadere.
|
||
function onKilled()
|
||
animationStop(hero)
|
||
ragdollActivate(hero)
|
||
end
|
||
|
||
-- Il saluto sul livello 2 è finito; la camminata sotto di esso continua.
|
||
function onWaveFinished()
|
||
animationStop(hero, 2)
|
||
end
|
||
----
|
||
|
||
[#bezelref]
|
||
=== Bezel
|
||
|
||
La grafica del cabinato e il tabellone segnapunti da sala giochi che l'accompagna. `mainBezelLoaded`
|
||
chiede della grafica caricata da `--bezel`; la famiglia `scoreBezel` guida il pannello
|
||
dei punteggi, un documento RmlUi che il motore fornisce e che un gioco può
|
||
sostituire con una copia propria, `scoreBezel.rml` accanto al suo script. Ogni chiamata qui è un'estensione di Hypseus Singe,
|
||
mantenuta perché i suoi giochi girino senza modifiche, e ognuna di esse è
|
||
descritta dal lato del giocatore in xref:Manual.adoc#bezels[Bezel].
|
||
|
||
Il pannello parte spento, come in Hypseus, e solo un gioco lo accende. I valori
|
||
che riceve vengono ricordati sia che sia visibile sia che non lo sia, quindi un
|
||
gioco può impostarli prima di abilitare il pannello. Ogni indicatore ha il
|
||
numero di cifre che ha il tabellone segnapunti di Hypseus -- sei per un punteggio, due per
|
||
i crediti, una per il numero di vite -- e un valore vi viene mostrato allineato
|
||
a destra con zeri iniziali; un punteggio di più di sei cifre mostra le ultime
|
||
sei. Anche i valori negativi di Hypseus vengono capiti: `-2` accende un trattino
|
||
in ogni cifra, `-4` una `H` e `-5` una `A`, e `-1`, `-3` e qualsiasi altro
|
||
numero negativo non accendono niente.
|
||
|
||
[#mainbezelloaded]
|
||
==== mainBezelLoaded
|
||
|
||
[source,text]
|
||
----
|
||
loaded, id = mainBezelLoaded()
|
||
----
|
||
|
||
Estensione di Hypseus. Se `--bezel` ha caricato una grafica del cabinato, e
|
||
l'identificativo del file che ha caricato. L'identificativo è l'hash FNV-1a a 64
|
||
bit dei byte del file scritto come stringa decimale, che è ciò che riporta
|
||
Hypseus, quindi un gioco che riconosce un certo bezel dal suo numero riconosce
|
||
qui lo stesso file. Senza grafica caricata le risposte sono `false` e `"0"`.
|
||
|
||
*Restituisce:* un booleano, e l'identificativo come stringa.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<setoverlayontop,setOverlayOnTop>>, xref:Manual.adoc#bezels[Bezel]
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Disegna la cornice di sprite solo quando il giocatore non ha una grafica sua.
|
||
local loaded, id = mainBezelLoaded()
|
||
if not loaded then
|
||
spriteDraw(frameSprite, 0, 0)
|
||
end
|
||
----
|
||
|
||
[#scorebezelclear]
|
||
==== scoreBezelClear
|
||
|
||
[source,text]
|
||
----
|
||
scoreBezelClear()
|
||
----
|
||
|
||
Estensione di Hypseus. Svuota ogni indicatore del pannello dei punteggi: i
|
||
crediti, entrambi i punteggi ed entrambi i numeri di vite non mostrano proprio
|
||
nulla, e restano solo le cifre attenuate dietro di essi. I valori non vengono
|
||
dimenticati, ma il successivo `scoreBezelCredits`,
|
||
`scoreBezelScore` o `scoreBezelLives` riaccende il pannello con ciò che contiene
|
||
in quel momento. Si può chiamare senza rischi con il pannello spento.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<scorebezelenable,scoreBezelEnable>>
|
||
|
||
[#scorebezelcredits]
|
||
==== scoreBezelCredits
|
||
|
||
[source,text]
|
||
----
|
||
scoreBezelCredits(credits)
|
||
----
|
||
|
||
Estensione di Hypseus. Imposta l'indicatore dei crediti, che ha due cifre.
|
||
Riaccende anche un pannello che `scoreBezelClear` ha svuotato.
|
||
|
||
*Parametri:*
|
||
|
||
* `credits` -- intero; vengono mostrate le ultime due cifre.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<scorebezelscore,scoreBezelScore>>
|
||
|
||
[#scorebezelenable]
|
||
==== scoreBezelEnable
|
||
|
||
[source,text]
|
||
----
|
||
scoreBezelEnable(enabled)
|
||
scoreBezelEnable(enabled, type)
|
||
----
|
||
|
||
Estensione di Hypseus. Mostra o nasconde il pannello dei punteggi. Il primo
|
||
`true` lo costruisce dal `scoreBezel.rml` del gioco stesso, accanto al suo script,
|
||
quando c'è, oppure da `Singe/scoreBezel.rml`; nasconderlo e mostrarlo di nuovo
|
||
dopo di allora non costa nulla e ne conserva i valori. Parte nascosto, che è
|
||
anche il modo in cui viene distribuito Hypseus, quindi un gioco che non chiama
|
||
mai questa funzione si comporta esattamente come prima -- e dovrebbe chiedere a
|
||
<<scorebezelgetstate,`scoreBezelGetState`>> prima di dare per scontato di averne uno.
|
||
|
||
Il `type` di Hypseus sceglie tra i suoi cinque backend di tabellone segnapunti. Singe ne ha
|
||
uno, un documento RmlUi, quindi l'argomento viene controllato e ignorato. Una
|
||
macchina senza GPU non ha alcuna GUI; lì il pannello non può essere costruito,
|
||
`scoreBezelGetState` continua a rispondere `false`, e viene stampato un avviso,
|
||
una volta sola. Un primo argomento non booleano termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
* `type` -- intero, accettato e ignorato.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<scorebezelgetstate,scoreBezelGetState>>, xref:Manual.adoc#bezels[Bezel]
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il pannello del motore quando c'è, gli sprite quando non c'è.
|
||
scoreBezelEnable(true, 0)
|
||
if scoreBezelGetState() then
|
||
scoreBezelTwinScoreOn(twoPlayer)
|
||
scoreBezelCredits(credits)
|
||
else
|
||
drawSpriteScoreboard()
|
||
end
|
||
----
|
||
|
||
[#scorebezelgetstate]
|
||
==== scoreBezelGetState
|
||
|
||
[source,text]
|
||
----
|
||
state = scoreBezelGetState()
|
||
----
|
||
|
||
Estensione di Hypseus. Se il pannello dei punteggi è visibile. Ogni gioco
|
||
Hypseus lo chiede prima di usare il pannello e disegna il proprio tabellone
|
||
segnapunti con gli sprite quando la risposta è `false`, che è il ripiego giusto e non un errore.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<scorebezelenable,scoreBezelEnable>>
|
||
|
||
[#scorebezellives]
|
||
==== scoreBezelLives
|
||
|
||
[source,text]
|
||
----
|
||
scoreBezelLives(player, lives)
|
||
----
|
||
|
||
Estensione di Hypseus. Imposta l'indicatore delle vite di un giocatore, che ha
|
||
una cifra. `player` è
|
||
`1` o `2`; qualsiasi altro numero viene ignorato, e così `2` finché
|
||
<<scorebezeltwinscoreon,`scoreBezelTwinScoreOn(true)`>> non ha messo sul
|
||
pannello gli indicatori del secondo giocatore. Riaccende anche un pannello che
|
||
`scoreBezelClear` ha svuotato.
|
||
|
||
*Parametri:*
|
||
|
||
* `player` -- `1` o `2`.
|
||
* `lives` -- intero; viene mostrata l'ultima cifra.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<scorebezelscore,scoreBezelScore>>
|
||
|
||
[#scorebezelscore]
|
||
==== scoreBezelScore
|
||
|
||
[source,text]
|
||
----
|
||
scoreBezelScore(player, score)
|
||
----
|
||
|
||
Estensione di Hypseus. Imposta l'indicatore del punteggio di un giocatore, che
|
||
ha sei cifre. `player` è `1` o `2`, con la stessa regola per `2` di
|
||
`scoreBezelLives`. Riaccende anche un pannello che `scoreBezelClear` ha
|
||
svuotato.
|
||
|
||
*Parametri:*
|
||
|
||
* `player` -- `1` o `2`.
|
||
* `score` -- intero; vengono mostrate le ultime sei cifre.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<scorebezellives,scoreBezelLives>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function addScore(points)
|
||
score = score + points
|
||
if scoreBezelGetState() then
|
||
scoreBezelScore(1, score)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#scorebezeltwinscoreon]
|
||
==== scoreBezelTwinScoreOn
|
||
|
||
[source,text]
|
||
----
|
||
scoreBezelTwinScoreOn(enabled)
|
||
----
|
||
|
||
Estensione di Hypseus. Mette sul pannello gli indicatori del punteggio e delle
|
||
vite del secondo giocatore, o li toglie di nuovo e lascia lo spazio agli altri
|
||
due. Spegnerli svuota anche il pannello, esattamente come in Hypseus, quindi
|
||
falli seguire da un valore se gli indicatori del primo giocatore devono restare
|
||
accesi. Mentre sono spenti,
|
||
`scoreBezelScore(2, ...)` e `scoreBezelLives(2, ...)` non fanno nulla. Un
|
||
argomento non booleano termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<scorebezelscore,scoreBezelScore>>
|
||
|
||
[#body]
|
||
=== Corpo
|
||
|
||
Un corpo dà a un nodo della scena un posto nel mondo fisico. C'è un corpo per nodo, indicato dall'handle del nodo: un corpo statico resta dov'era il nodo quando il corpo è stato creato, un corpo cinematico segue il suo nodo a ogni passo e spinge senza essere spinto, e un corpo dinamico è mosso dalla simulazione e guida il suo nodo. Dimensioni, posizioni e velocità sono in unità del mondo (metri in una scena 3D, pixel dell'overlay in un mondo 2D), le masse in chilogrammi e il tempo in secondi. Ogni chiamata qui termina lo script quando il nodo non ha un corpo; una chiamata che ha senso solo per un tipo di corpo (una forza su un corpo statico, per esempio) viene ignorata sugli altri, tranne `bodySetMass`, che solleva un errore. Vedi <<physics,Fisica>> in Sviluppo di giochi per come si combinano corpi, nodi e fotogramma.
|
||
|
||
[#bodynew]
|
||
==== bodyNew
|
||
|
||
[source,text]
|
||
----
|
||
bodyNew(node, type, shape)
|
||
bodyNew(node, type, shape, a)
|
||
bodyNew(node, type, shape, a, b)
|
||
bodyNew(node, type, shape, a, b, c)
|
||
----
|
||
|
||
Dà al nodo un corpo del tipo indicato, posizionato dove si trova il nodo nel mondo in questo momento (genitori compresi), con una forma di collisione dimensionata da `a`, `b` e `c` in unità del mondo e scalata dalla scala del nodo nel mondo. Una seconda chiamata sullo stesso nodo sostituisce la prima, e un nodo che aveva un giocatore lo perde. La massa di un corpo dinamico deriva dal volume della sua forma alla densità dell'acqua, quindi imposta `bodySetMass` su tutto ciò che deve sembrare leggero. Cambiare in seguito la scala del nodo non ridimensiona il corpo. Termina lo script per un tipo o una forma sconosciuti, quando la fisica non è disponibile su questa macchina, quando il mondo contiene già `4096` corpi, o quando la forma non può essere costruita (una `SHAPE_MESH` dinamica, o un inviluppo senza mesh sotto il nodo).
|
||
|
||
*Parametri:*
|
||
|
||
* `type` -- `BODY_STATIC`, `BODY_DYNAMIC` o `BODY_KINEMATIC`.
|
||
* `shape` -- `SHAPE_BOX` (`a`, `b`, `c` sono larghezza, altezza e profondità), `SHAPE_SPHERE` (`a` è il raggio), `SHAPE_CAPSULE` o `SHAPE_CYLINDER` (`a` è il raggio, `b` l'altezza totale lungo Y, calotte comprese), `SHAPE_HULL` (un inviluppo convesso della mesh del nodo e di tutto ciò che sta sotto, senza misure), o `SHAPE_MESH` (i triangoli esatti, solo per corpi statici e cinematici; una mesh da heightmap diventa un campo di altezze).
|
||
* `a`, `b`, `c` -- le misure di cui la forma ha bisogno; quelle mancanti valgono `0` e ogni misura viene portata ad almeno `0.001`.
|
||
|
||
*Note:* Una sfera è scalata dall'asse di scala più grande del nodo, il raggio di una capsula o di un cilindro dal maggiore tra X e Z. Una capsula più corta del doppio del suo raggio è una sfera con una fettina nel mezzo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodysetmass,bodySetMass>>, <<bodydelete,bodyDelete>>, <<physicssetgravity,physicsSetGravity>>, <<playernew,playerNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una cassa leggera e un modello di papera come inviluppo, lasciati cadere sul pavimento.
|
||
crate = nodeNew()
|
||
nodeSetMesh(crate, meshBox(0.6, 0.6, 0.6), wood)
|
||
nodeSetPosition(crate, -1, 1, 0)
|
||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
|
||
bodySetMass(crate, 5)
|
||
duck = modelInstance(modelLoad(DIR .. "Models/Duck.glb"))
|
||
nodeSetPosition(duck, 3, 2, 0)
|
||
bodyNew(duck, BODY_DYNAMIC, SHAPE_HULL)
|
||
----
|
||
|
||
[#bodyapplyforce]
|
||
==== bodyApplyForce
|
||
|
||
[source,text]
|
||
----
|
||
bodyApplyForce(node, fx, fy, fz)
|
||
bodyApplyForce(node, fx, fy, fz, px, py, pz)
|
||
----
|
||
|
||
Aggiunge una forza, in newton, a un corpo dinamico solo per i passi di questo fotogramma; chiamala a ogni fotogramma per continuare a spingere. Senza un punto la forza agisce attraverso il centro di massa e si limita a spostare il corpo; con un punto del mondo `(px, py, pz)` fa anche ruotare il corpo attorno al suo centro. Una forza accelera di forza diviso massa, quindi commisurala a `bodySetMass`. I corpi statici e cinematici la ignorano.
|
||
|
||
*Parametri:*
|
||
|
||
* `fx`, `fy`, `fz` -- la forza in newton lungo ciascun asse del mondo.
|
||
* `px`, `py`, `pz` -- punto del mondo facoltativo in cui agisce la forza; omettilo per il centro di massa.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodyapplyimpulse,bodyApplyImpulse>>, <<bodysetmass,bodySetMass>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un ventilatore spinge il palloncino attraverso la stanza finché l'interruttore è acceso.
|
||
function onOverlayUpdate()
|
||
if fanOn then
|
||
local bx, by, bz = nodeGetPosition(balloon)
|
||
if bx < fanReach then
|
||
bodyApplyForce(balloon, 2.5, 0.4, 0)
|
||
end
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#bodyapplyimpulse]
|
||
==== bodyApplyImpulse
|
||
|
||
[source,text]
|
||
----
|
||
bodyApplyImpulse(node, ix, iy, iz)
|
||
bodyApplyImpulse(node, ix, iy, iz, px, py, pz)
|
||
----
|
||
|
||
Cambia subito la quantità di moto di un corpo dinamico: un impulso di massa per velocità aggiunge quella velocità. Senza un punto agisce attraverso il centro di massa; con un punto del mondo `(px, py, pz)` fa anche ruotare il corpo, quindi colpire una cassa vicino alla sommità la ribalta. Questa è la chiamata per un colpo, un calcio, un'esplosione o un lancio, mentre `bodyApplyForce` è per una spinta che dura. I corpi statici e cinematici lo ignorano.
|
||
|
||
*Parametri:*
|
||
|
||
* `ix`, `iy`, `iz` -- l'impulso in chilogrammi per metro al secondo lungo ciascun asse del mondo.
|
||
* `px`, `py`, `pz` -- punto del mondo facoltativo in cui agisce l'impulso; omettilo per il centro di massa.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodyapplyforce,bodyApplyForce>>, <<bodysetvelocity,bodySetVelocity>>, <<physicsraycast,physicsRaycast>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fa schizzare verso l'alto ciò che sta sotto il mouse, nel punto in cui è stato colpito.
|
||
local mx, my = mouseGetPosition(0)
|
||
local ox, oy, oz = sceneUnproject(mx, my, 0)
|
||
local fx, fy, fz = sceneUnproject(mx, my, 10)
|
||
local hit, hx, hy, hz = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz)
|
||
if hit and hit ~= floor then
|
||
bodyApplyImpulse(hit, 0, 3, 0, hx, hy, hz)
|
||
end
|
||
----
|
||
|
||
[#bodydelete]
|
||
==== bodyDelete
|
||
|
||
[source,text]
|
||
----
|
||
bodyDelete(node)
|
||
----
|
||
|
||
Toglie il corpo dal mondo; il nodo e la sua mesh restano dove sono. Ogni giunto collegato al corpo, e qualsiasi veicolo costruito su di esso, se ne va con lui. Eliminare il nodo toglie anche il suo corpo, quindi questa chiamata serve solo per tenere un nodo senza il suo corpo. Termina lo script quando il nodo non ha un corpo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodynew,bodyNew>>, <<bodysetenabled,bodySetEnabled>>, <<jointdelete,jointDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una moneta raccolta smette di essere solida e sale fluttuando come un semplice nodo.
|
||
function onTrigger(trigger, other, entered)
|
||
if trigger == coin and other == hero and entered then
|
||
bodyDelete(coin)
|
||
collected = collected + 1
|
||
coinRising = true
|
||
end
|
||
end
|
||
----
|
||
|
||
[#bodygetangularvelocity]
|
||
==== bodyGetAngularVelocity
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z = bodyGetAngularVelocity(node)
|
||
----
|
||
|
||
Quanto velocemente il corpo ruota attorno a ciascun asse del mondo, in radianti al secondo, all'ultimo passo. Un corpo statico riporta zero. Utile per l'altezza del suono di una ruota o per un contagiri.
|
||
|
||
*Restituisce:* tre numeri, radianti al secondo attorno a X, Y e Z.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodysetangularvelocity,bodySetAngularVelocity>>, <<bodygetvelocity,bodyGetVelocity>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La ruota della roulette si è fermata quando gira appena.
|
||
function onOverlayUpdate()
|
||
local _, spin = bodyGetAngularVelocity(wheel)
|
||
if spinning and math.abs(spin) < 0.05 then
|
||
spinning = false
|
||
announceWinner(pocketUnder(ball))
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#bodygetvelocity]
|
||
==== bodyGetVelocity
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z = bodyGetVelocity(node)
|
||
----
|
||
|
||
Quanto velocemente si muove il corpo lungo ciascun asse del mondo, in unità del mondo al secondo, all'ultimo passo. Un corpo statico riporta zero; un corpo cinematico riporta la velocità implicata dal movimento del suo nodo.
|
||
|
||
*Restituisce:* tre numeri, unità al secondo lungo X, Y e Z.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodysetvelocity,bodySetVelocity>>, <<bodygetangularvelocity,bodyGetAngularVelocity>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Riproduce un sibilo quando la palla si muove davvero.
|
||
function onOverlayUpdate()
|
||
local vx, vy, vz = bodyGetVelocity(ball)
|
||
local speed = math.sqrt(vx * vx + vy * vy + vz * vz)
|
||
if speed > 8 and not whooshing then
|
||
soundPlay(whoosh)
|
||
whooshing = true
|
||
elseif speed < 2 then
|
||
whooshing = false
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#bodyisresting]
|
||
==== bodyIsResting
|
||
|
||
[source,text]
|
||
----
|
||
resting = bodyIsResting(node)
|
||
----
|
||
|
||
Se la simulazione ha messo a dormire il corpo perché ha smesso di muoversi. I corpi statici e quelli tolti dal mondo con `bodySetEnabled` contano come a riposo. Un corpo a riposo si risveglia da solo quando qualcosa lo tocca o quando gli si applica una velocità, una forza o un impulso.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodysetenabled,bodySetEnabled>>, <<bodysetvelocity,bodySetVelocity>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il turno finisce quando tutti i birilli si sono assestati.
|
||
function onOverlayUpdate()
|
||
local settled = true
|
||
for _, pin in ipairs(pins) do
|
||
settled = settled and bodyIsResting(pin)
|
||
end
|
||
if settled then
|
||
scoreFrame()
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#bodysetangularvelocity]
|
||
==== bodySetAngularVelocity
|
||
|
||
[source,text]
|
||
----
|
||
bodySetAngularVelocity(node, x, y, z)
|
||
----
|
||
|
||
Imposta la rotazione attorno a ciascun asse del mondo, in radianti al secondo, e risveglia il corpo. I corpi statici la ignorano. La rotazione di un corpo cinematico viene sovrascritta dal suo nodo al passo successivo, quindi la chiamata dura solo per i corpi dinamici.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodygetangularvelocity,bodyGetAngularVelocity>>, <<bodysetvelocity,bodySetVelocity>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Lancia la moneta: verso l'alto, facendola girare su se stessa.
|
||
function tossCoin()
|
||
nodeSetPosition(coin, 0, 1.2, 0)
|
||
bodySetVelocity(coin, 0, 4, 0)
|
||
bodySetAngularVelocity(coin, 25, 0, 0)
|
||
tossed = true
|
||
end
|
||
----
|
||
|
||
[#bodysetbounce]
|
||
==== bodySetBounce
|
||
|
||
[source,text]
|
||
----
|
||
bodySetBounce(node, bounce)
|
||
----
|
||
|
||
Imposta il coefficiente di restituzione del corpo, da `0` (si ferma di colpo) a `1` (rimbalza indietro con tutto lo slancio con cui è arrivato); i valori fuori da quell'intervallo vengono limitati. Il predefinito è `0.1`. I valori dei due corpi che si toccano si combinano, quindi una palla vivace su un pavimento sordo rimbalza comunque.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodysetfriction,bodySetFriction>>, <<bodynew,bodyNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una palla di gomma lasciata cadere nella scena.
|
||
ball = nodeNew()
|
||
nodeSetMesh(ball, meshSphere(0.3, 32), blue)
|
||
nodeSetPosition(ball, 0, 3, 0)
|
||
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.3)
|
||
bodySetMass(ball, 0.5)
|
||
bodySetBounce(ball, 0.7)
|
||
----
|
||
|
||
[#bodysetbuoyancy]
|
||
==== bodySetBuoyancy
|
||
|
||
[source,text]
|
||
----
|
||
bodySetBuoyancy(node, factor)
|
||
----
|
||
|
||
Come galleggia il corpo quando si trova dentro un volume d'acqua: `1` è neutro, di più galleggia più in alto, di meno affonda, e `0` è un sasso. Il predefinito è `1.2`, che galleggia dolcemente. Il fattore moltiplica la densità dell'acqua stessa data da `bodySetWater`, e i valori negativi vengono limitati a `0`. Solo i corpi dinamici vengono mossi dall'acqua, ma il valore può essere impostato su qualsiasi corpo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodysetwater,bodySetWater>>, <<bodysetcurrent,bodySetCurrent>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una cassa che galleggia alta, un'ancora che va a fondo, una palla che ondeggia.
|
||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8)
|
||
bodySetMass(crate, 20)
|
||
bodySetBuoyancy(crate, 1.6)
|
||
bodyNew(anchor, BODY_DYNAMIC, SHAPE_BOX, 0.5, 0.5, 0.5)
|
||
bodySetMass(anchor, 200)
|
||
bodySetBuoyancy(anchor, 0.3)
|
||
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.4)
|
||
bodySetBuoyancy(ball, 2.0)
|
||
----
|
||
|
||
[#bodysetcurrent]
|
||
==== bodySetCurrent
|
||
|
||
[source,text]
|
||
----
|
||
bodySetCurrent(node, x, y, z)
|
||
----
|
||
|
||
Imposta il flusso dentro un volume d'acqua, in unità del mondo al secondo lungo ciascun asse del mondo; i corpi dinamici che vi galleggiano derivano con il flusso, e un giocatore che nuota ne viene trascinato. Il predefinito è nessun flusso. Impostalo sul nodo su cui è stato chiamato `bodySetWater`; su qualsiasi altro corpo viene conservato e non fa nulla.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodysetwater,bodySetWater>>, <<bodysetbuoyancy,bodySetBuoyancy>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un fiume che trascina a valle qualsiasi cosa ci cada dentro.
|
||
river = nodeNew()
|
||
nodeSetMesh(river, meshBox(6, 3, 12), water)
|
||
nodeSetPosition(river, 10, -1.5, 0)
|
||
bodyNew(river, BODY_STATIC, SHAPE_BOX, 6, 3, 12)
|
||
bodySetWater(river, 1.0, 0.8, 0.2)
|
||
bodySetCurrent(river, 0, 0, 2.5)
|
||
----
|
||
|
||
[#bodysetenabled]
|
||
==== bodySetEnabled
|
||
|
||
[source,text]
|
||
----
|
||
bodySetEnabled(node, enabled)
|
||
----
|
||
|
||
Con `false` toglie il corpo dal mondo: smette di collidere, smette di muoversi, non guida né segue più il suo nodo, e conta come a riposo. `true` lo rimette dove si trova, sveglio. Il corpo conserva forma, massa e impostazioni, quindi per qualcosa che va e viene costa meno che eliminarlo e ricrearlo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodyisresting,bodyIsResting>>, <<bodydelete,bodyDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il ponte levatoio è solido solo quando è abbassato.
|
||
function lowerBridge()
|
||
animationPlay(bridge, "Lower", false)
|
||
bodySetEnabled(bridgeDeck, true)
|
||
end
|
||
|
||
function raiseBridge()
|
||
animationPlay(bridge, "Raise", false)
|
||
bodySetEnabled(bridgeDeck, false)
|
||
end
|
||
----
|
||
|
||
[#bodysetfriction]
|
||
==== bodySetFriction
|
||
|
||
[source,text]
|
||
----
|
||
bodySetFriction(node, friction)
|
||
----
|
||
|
||
Imposta l'attrito della superficie: `0` è ghiaccio, `0.5` (il predefinito) è normale, valori più alti fanno più presa. I valori negativi vengono limitati a `0`. I valori dei due corpi che si toccano si combinano, quindi un pavimento scivoloso ha bisogno di un valore basso sul pavimento, non su tutto ciò che ci cammina sopra.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodysetbounce,bodySetBounce>>, <<bodynew,bodyNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Uno stagno ghiacciato in mezzo al livello.
|
||
pond = nodeNew()
|
||
nodeSetMesh(pond, meshBox(6, 0.1, 6), ice)
|
||
nodeSetPosition(pond, 8, -1.95, 0)
|
||
bodyNew(pond, BODY_STATIC, SHAPE_BOX, 6, 0.1, 6)
|
||
bodySetFriction(pond, 0.02)
|
||
----
|
||
|
||
[#bodysetmass]
|
||
==== bodySetMass
|
||
|
||
[source,text]
|
||
----
|
||
bodySetMass(node, kilograms)
|
||
----
|
||
|
||
Riscala la massa di un corpo dinamico e, con essa, la sua inerzia. Fallo per tutto ciò che deve sembrare leggero o pesante: il predefinito è il volume della forma alla densità dell'acqua, quindi un parallelepipedo grande come una porta pesa circa 140 chilogrammi e una spintarella non lo smuove. Forze e impulsi scalano con la massa, quindi imposta la massa prima di regolarli. Termina lo script quando il corpo non è dinamico o la massa non è positiva.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodynew,bodyNew>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<playersetpush,playerSetPush>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una cassa che l'eroe può spingere e un'incudine che nessuno può smuovere.
|
||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
|
||
bodySetMass(crate, 5)
|
||
bodyNew(anvil, BODY_DYNAMIC, SHAPE_BOX, 0.5, 0.3, 0.3)
|
||
bodySetMass(anvil, 200)
|
||
----
|
||
|
||
[#bodysettrigger]
|
||
==== bodySetTrigger
|
||
|
||
[source,text]
|
||
----
|
||
bodySetTrigger(node, trigger)
|
||
----
|
||
|
||
Con `true` rende il corpo un trigger: non spinge niente e non ferma niente, ma `onTrigger` segnala ogni corpo o giocatore che vi entra e ne esce. `false` lo rende di nuovo solido: ciò che c'era dentro viene dimenticato senza un'uscita, mentre un trigger in cui si trova il corpo stesso continua a segnalarlo. Un trigger è di solito un parallelepipedo o una sfera statici collocati su una porta, un traguardo o una zona di pericolo. I raggi di `physicsRaycast` colpiscono i trigger come qualsiasi altro corpo.
|
||
|
||
*Note:* Un giocatore viene segnalato dentro un trigger quando i loro riquadri di delimitazione si sovrappongono, quindi per i giocatori un trigger rotondo è un trigger quadrato.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<ontrigger,onTrigger>>, <<bodynew,bodyNew>>, <<bodysetwater,bodySetWater>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un traguardo di traverso sulla pista.
|
||
line = nodeNew()
|
||
nodeSetPosition(line, 0, 0, -20)
|
||
bodyNew(line, BODY_STATIC, SHAPE_BOX, 6, 3, 0.5)
|
||
bodySetTrigger(line, true)
|
||
|
||
function onTrigger(trigger, other, entered)
|
||
if trigger == line and other == car and entered then
|
||
lapDone()
|
||
end
|
||
end
|
||
----
|
||
|
||
[#bodysetvelocity]
|
||
==== bodySetVelocity
|
||
|
||
[source,text]
|
||
----
|
||
bodySetVelocity(node, x, y, z)
|
||
----
|
||
|
||
Imposta la velocità del corpo lungo ciascun asse del mondo, in unità del mondo al secondo, e lo risveglia. Sostituisce qualsiasi velocità avesse il corpo, mentre `bodyApplyImpulse` vi si aggiunge. I corpi statici la ignorano, e la velocità di un corpo cinematico viene sovrascritta dal movimento del suo nodo al passo successivo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodygetvelocity,bodyGetVelocity>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<bodysetangularvelocity,bodySetAngularVelocity>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Serve la palla verso il lato che ha perso l'ultimo punto.
|
||
function serve()
|
||
nodeSetPosition(ball, 0, 0.5, 0)
|
||
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.15)
|
||
bodySetMass(ball, 0.1)
|
||
local dir = (lastWinner == 1) and -1 or 1
|
||
bodySetVelocity(ball, 4 * dir, 2, (math.random() - 0.5) * 2)
|
||
end
|
||
----
|
||
|
||
[#bodysetwater]
|
||
==== bodySetWater
|
||
|
||
[source,text]
|
||
----
|
||
bodySetWater(node, density)
|
||
bodySetWater(node, density, linearDrag)
|
||
bodySetWater(node, density, linearDrag, angularDrag)
|
||
----
|
||
|
||
Riempie d'acqua un corpo statico e lo rende un trigger: la faccia superiore della sua forma è la superficie, e ogni corpo dinamico al suo interno galleggia, affonda o rolla per galleggiamento rispetto a quella superficie (conta la parte immersa della forma, quindi una cassa inclinata si raddrizza da sola), rallentato dalle resistenze. Un giocatore con il punto centrale sotto la superficie nuota. Poiché il volume è un trigger, `onTrigger` scatta all'entrata e all'uscita. L'acqua non viene disegnata: dai al nodo un materiale con fusione, o nessun materiale. Termina lo script quando il corpo non è statico.
|
||
|
||
*Parametri:*
|
||
|
||
* `density` -- un moltiplicatore, `1` per l'acqua; il `bodySetBuoyancy` di ciascun corpo lo scala. I valori negativi vengono limitati a `0`.
|
||
* `linearDrag` -- quanto in fretta l'acqua rallenta il movimento al suo interno; predefinito `0.5`.
|
||
* `angularDrag` -- quanto in fretta rallenta la rotazione; predefinito `0.1`.
|
||
|
||
*Note:* I giocatori e le barche vedono al massimo otto volumi d'acqua; quelli in più fanno comunque galleggiare i corpi, ma non ci si può nuotare.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodysetbuoyancy,bodySetBuoyancy>>, <<bodysetcurrent,bodySetCurrent>>, <<bodysettrigger,bodySetTrigger>>, <<playerisswimming,playerIsSwimming>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una piscina ricavata nel ponte; la sua faccia superiore, a Y = 0, è la superficie.
|
||
pool = nodeNew()
|
||
nodeSetMesh(pool, meshBox(8, 3, 12), water)
|
||
nodeSetPosition(pool, 0, -1.5, 0)
|
||
bodyNew(pool, BODY_STATIC, SHAPE_BOX, 8, 3, 12)
|
||
bodySetWater(pool, 1.0, 0.6, 0.2)
|
||
|
||
function onTrigger(trigger, other, entered)
|
||
if trigger == pool and entered then
|
||
soundPlay(splash)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#camera]
|
||
=== Telecamera
|
||
|
||
La telecamera è un nodo come gli altri: posizionala con `nodeSetPosition`, puntala con `nodeLookAt`, rendila figlia di un oggetto in movimento per una visuale d'inseguimento, e designala con `cameraSet`. Guarda lungo il proprio -Z. Senza un nodo telecamera la scena è vista da `(0, 0, 5)` verso l'origine. La proiezione si imposta a parte e vale per qualunque nodo sia la telecamera: prospettica con un campo visivo verticale di 60 gradi da `0.1` a `1000` unità per impostazione predefinita, oppure ortografica con `cameraSetOrthographic`. Le viste renderizzate (`viewNew`) condividono la proiezione ma guardano dai propri nodi. Vedi <<scenes3d,Scene 3D>>.
|
||
|
||
[#cameraset]
|
||
==== cameraSet
|
||
|
||
[source,text]
|
||
----
|
||
cameraSet(node)
|
||
cameraSet(-1)
|
||
----
|
||
|
||
Rende un nodo la telecamera: la scena viene disegnata dalla sua posizione nel mondo guardando lungo il suo -Z, quindi contano il movimento e la rotazione dei suoi genitori. Va bene qualsiasi nodo, anche uno che porta una mesh o una luce, e cambiare telecamera è una sola chiamata. `-1` ripristina la vista predefinita da `(0, 0, 5)` verso l'origine. Anche eliminare il nodo telecamera fa tornare alla vista predefinita. Termina lo script per un handle di nodo non valido.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodelookat,nodeLookAt>>, <<camerasetperspective,cameraSetPerspective>>, <<viewsetcamera,viewSetCamera>>, <<nodenew,nodeNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una telecamera d'inseguimento dietro l'auto del giocatore, e una fissa per il replay.
|
||
chaseCamera = nodeNew(car)
|
||
nodeSetPosition(chaseCamera, 0, 2, 6)
|
||
nodeLookAt(chaseCamera, nodeGetWorldPosition(car))
|
||
cameraSet(chaseCamera)
|
||
|
||
function onReplayStart()
|
||
replayCamera = nodeNew()
|
||
nodeSetPosition(replayCamera, 30, 12, 30)
|
||
nodeLookAt(replayCamera, 0, 0, 0)
|
||
cameraSet(replayCamera)
|
||
end
|
||
----
|
||
|
||
[#camerasetorthographic]
|
||
==== cameraSetOrthographic
|
||
|
||
[source,text]
|
||
----
|
||
cameraSetOrthographic(height, near, far)
|
||
----
|
||
|
||
Passa a una proiezione parallela che mostra `height` unità del mondo dall'alto in basso, con la larghezza che segue le proporzioni del livello, da `near` a `far` davanti alla telecamera. La distanza non rimpicciolisce le cose, il che si addice a diagrammi, viste isometriche e di lato, e scene simili a un HUD. L'impostazione vale per il nodo telecamera corrente e per quelli successivi finché non viene chiamata `cameraSetPerspective`. L'ombra di una luce direzionale usa una sola mappa sotto una telecamera ortografica, qualunque cosa dica `sceneSetShadowCascades`.
|
||
|
||
*Parametri:*
|
||
|
||
* `height` -- l'altezza visibile, in unità del mondo.
|
||
* `near` -- la distanza più vicina disegnata, in unità del mondo davanti alla telecamera.
|
||
* `far` -- la distanza più lontana disegnata.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<camerasetperspective,cameraSetPerspective>>, <<cameraset,cameraSet>>, <<scenesetshadowcascades,sceneSetShadowCascades>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una vista isometrica della plancia: alta 12 unità, dall'alto e di lato.
|
||
boardCamera = nodeNew()
|
||
nodeSetPosition(boardCamera, 20, 20, 20)
|
||
nodeLookAt(boardCamera, 0, 0, 0)
|
||
cameraSet(boardCamera)
|
||
cameraSetOrthographic(12, 1, 100)
|
||
----
|
||
|
||
[#camerasetperspective]
|
||
==== cameraSetPerspective
|
||
|
||
[source,text]
|
||
----
|
||
cameraSetPerspective(fov, near, far)
|
||
----
|
||
|
||
Passa alla consueta proiezione prospettica, quella predefinita. `fov` è il campo visivo verticale in gradi (predefinito `60`); `near` e `far` (predefiniti `0.1` e `1000`) delimitano ciò che viene disegnato, in unità del mondo davanti alla telecamera. La precisione della profondità si distribuisce tra i due, quindi tieni `near` grande quanto la scena consente e `far` non più grande del necessario: da `0.05` a `100` per una stanza, da `0.1` a `400` per un paesaggio. L'impostazione vale per il nodo telecamera corrente e per quelli successivi, e per le viste renderizzate.
|
||
|
||
*Parametri:*
|
||
|
||
* `fov` -- campo visivo verticale in gradi; un ampio `70` dà un senso di velocità, uno stretto `40` sembra un teleobiettivo.
|
||
* `near` -- la distanza più vicina disegnata.
|
||
* `far` -- la distanza più lontana disegnata.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<camerasetorthographic,cameraSetOrthographic>>, <<cameraset,cameraSet>>, <<sceneproject,sceneProject>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un obiettivo più stretto per la vista dall'abitacolo, quello predefinito per l'esterno.
|
||
function setView(inside)
|
||
if inside then
|
||
cameraSet(cockpitCamera)
|
||
cameraSetPerspective(45, 0.05, 300)
|
||
else
|
||
cameraSet(chaseCamera)
|
||
cameraSetPerspective(60, 0.1, 400)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#collide]
|
||
=== Collisioni
|
||
|
||
Se due cose piatte si toccano. Jolt risponde a questa domanda in tre dimensioni
|
||
per i corpi (vedi <<body,Corpo>>); queste funzioni rispondono per l'overlay, che
|
||
è dove vivono le hitbox di una pistola ottica e tutto quanto di un gioco 2D.
|
||
|
||
Sono semplici funzioni: niente handle, niente stato, niente da creare o liberare
|
||
e niente da aggiornare a ogni fotogramma. Le coordinate sono quelle in cui stai
|
||
già disegnando -- di solito i pixel dell'overlay. Un rettangolo è un vertice e
|
||
una dimensione, così come lo accetta ogni chiamata di disegno del motore.
|
||
Toccarsi esattamente su un bordo conta come toccarsi, e un rettangolo senza
|
||
larghezza o altezza non tocca proprio niente.
|
||
|
||
Nessuna di queste funzioni sposta qualcosa. Quando vuoi sapere non solo *se* due
|
||
cose si sono incontrate ma *dove va a finire ciò che si muove* -- scivolando
|
||
lungo un muro invece di fermarsi di colpo contro di esso -- usa
|
||
`require("bump")` e lascia fare a lui; è incluso proprio per questo.
|
||
|
||
[#collidecircles]
|
||
==== collideCircles
|
||
|
||
[source,text]
|
||
----
|
||
touching = collideCircles(x1, y1, radius1, x2, y2, radius2)
|
||
----
|
||
|
||
Se due cerchi si sovrappongono. Costa meno di quanto sembri: niente calcola una radice quadrata per rispondere sì o no.
|
||
|
||
*Parametri:*
|
||
|
||
* `x1`, `y1`, `radius1` -- numeri; il primo cerchio.
|
||
* `x2`, `y2`, `radius2` -- numeri; il secondo.
|
||
|
||
*Restituisce:* `true` o `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<colliderectcircle,collideRectCircle>>
|
||
|
||
[#collidepointcircle]
|
||
==== collidePointCircle
|
||
|
||
[source,text]
|
||
----
|
||
inside = collidePointCircle(pointX, pointY, x, y, radius)
|
||
----
|
||
|
||
Se un punto cade dentro un cerchio. Il bordo conta come dentro.
|
||
|
||
*Parametri:*
|
||
|
||
* `pointX`, `pointY` -- numeri; il punto.
|
||
* `x`, `y`, `radius` -- numeri; il cerchio.
|
||
|
||
*Restituisce:* `true` o `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#collidepointpolygon]
|
||
==== collidePointPolygon
|
||
|
||
[source,text]
|
||
----
|
||
inside = collidePointPolygon(pointX, pointY, points)
|
||
----
|
||
|
||
Se un punto cade dentro un poligono di qualsiasi forma, compresi quelli concavi. La tabella è un elenco piatto di `x`, `y`, `x`, `y` -- la forma che una hitbox ha già nella maggior parte dei giochi -- e il verso in cui sono elencati i vertici non conta. Con meno di tre vertici non contiene nulla. Al massimo 256 vertici.
|
||
|
||
*Parametri:*
|
||
|
||
* `pointX`, `pointY` -- numeri; il punto.
|
||
* `points` -- tabella; un numero pari di numeri, x e y alternati.
|
||
|
||
*Restituisce:* `true` o `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il colpo ha centrato il drago, il cui contorno non è un rettangolo?
|
||
local dragon = { 120,40, 200,60, 210,140, 150,180, 100,120 }
|
||
|
||
function onInputPressed(key)
|
||
if key == SWITCH_BUTTON1 then
|
||
local x, y = mouseGetPosition(0)
|
||
if collidePointPolygon(x, y, dragon) then
|
||
hit()
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
[#collidepointrect]
|
||
==== collidePointRect
|
||
|
||
[source,text]
|
||
----
|
||
inside = collidePointRect(pointX, pointY, x, y, width, height)
|
||
----
|
||
|
||
Se un punto cade dentro un rettangolo. Il bordo conta come dentro.
|
||
|
||
*Parametri:*
|
||
|
||
* `pointX`, `pointY` -- numeri; il punto.
|
||
* `x`, `y`, `width`, `height` -- numeri; il rettangolo.
|
||
|
||
*Restituisce:* `true` o `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una hitbox grande quanto lo sprite a cui appartiene.
|
||
local x, y = mouseGetPosition(0)
|
||
if collidePointRect(x, y, enemyX, enemyY, spriteGetWidth(enemy), spriteGetHeight(enemy)) then
|
||
kill(enemy)
|
||
end
|
||
----
|
||
|
||
[#colliderectcircle]
|
||
==== collideRectCircle
|
||
|
||
[source,text]
|
||
----
|
||
touching = collideRectCircle(x, y, width, height, circleX, circleY, radius)
|
||
----
|
||
|
||
Se un rettangolo e un cerchio si sovrappongono, trovando il punto del rettangolo più vicino al centro del cerchio.
|
||
|
||
*Parametri:*
|
||
|
||
* `x`, `y`, `width`, `height` -- numeri; il rettangolo.
|
||
* `circleX`, `circleY`, `radius` -- numeri; il cerchio.
|
||
|
||
*Restituisce:* `true` o `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#colliderects]
|
||
==== collideRects
|
||
|
||
[source,text]
|
||
----
|
||
touching = collideRects(x1, y1, width1, height1, x2, y2, width2, height2)
|
||
----
|
||
|
||
Se due rettangoli si sovrappongono. Quella che userai più spesso.
|
||
|
||
*Parametri:*
|
||
|
||
* `x1`, `y1`, `width1`, `height1` -- numeri; il primo rettangolo.
|
||
* `x2`, `y2`, `width2`, `height2` -- numeri; il secondo.
|
||
|
||
*Restituisce:* `true` o `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<collidepointrect,collidePointRect>>
|
||
|
||
[#collidesegments]
|
||
==== collideSegments
|
||
|
||
[source,text]
|
||
----
|
||
crossing = collideSegments(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
|
||
----
|
||
|
||
Se due segmenti si incrociano: un colpo lungo un percorso contro un muro, o il passo che ciò che si muove vuole fare contro il bordo che non deve superare. Segmenti che giacciono l'uno lungo l'altro rispondono `false`, perché allora «dove si incrociano» non ha un'unica risposta.
|
||
|
||
*Parametri:*
|
||
|
||
* `ax1`, `ay1`, `ax2`, `ay2` -- numeri; gli estremi del primo segmento.
|
||
* `bx1`, `by1`, `bx2`, `by2` -- numeri; quelli del secondo.
|
||
|
||
*Restituisce:* `true` o `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#color]
|
||
=== Colore
|
||
|
||
I colori di primo piano e di sfondo sono valori globali del motore e non argomenti di ogni chiamata: li imposti con `colorForeground` e `colorBackground`, e ogni chiamata di disegno successiva li legge finché non cambiano. Il colore di primo piano è usato dalle primitive di forma `overlay*` e dal testo TrueType di `fontPrint` e `fontToSprite`; il colore di sfondo è usato da `overlayClear` e dal testo `FONT_QUALITY_SHADED`. `overlayPrint` non usa né l'uno né l'altro. I canali sono interi da `0` a `255`, limitati a quell'intervallo, ed entrambi i colori accettano un alfa facoltativo. Vedi le sezioni <<overlay,Overlay>> e <<font,Font>> per le chiamate che li usano.
|
||
|
||
[#colorbackground]
|
||
==== colorBackground
|
||
|
||
[source,text]
|
||
----
|
||
colorBackground(r, g, b)
|
||
colorBackground(r, g, b, a)
|
||
----
|
||
|
||
Imposta il colore di sfondo: quello con cui `overlayClear` riempie l'overlay, e quello che il testo `FONT_QUALITY_SHADED` di `fontPrint` e `fontToSprite` dipinge dietro i suoi glifi. All'avvio è nero trasparente, e l'alfa vale `0` quando viene omesso, quindi `colorBackground(0, 0, 0)` seguito da `overlayClear()` rende di nuovo invisibile l'overlay. Un alfa parziale sul nero attenua il video; `255` lo nasconde. I valori dei canali fuori intervallo vengono limitati, non rifiutati.
|
||
|
||
*Parametri:*
|
||
|
||
* `r`, `g`, `b` -- rosso, verde e blu, da `0` a `255`.
|
||
* `a` -- opacità, da `0` (trasparente) a `255` (opaco); predefinito `0`.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<colorforeground,colorForeground>>, <<overlayclear,overlayClear>>, <<fontquality,fontQuality>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Nero pieno dietro la schermata di calibrazione, trasparente durante il gioco.
|
||
function onOverlayUpdate()
|
||
if calibrating then
|
||
colorBackground(0, 0, 0, 255)
|
||
else
|
||
colorBackground(0, 0, 0, 0)
|
||
end
|
||
overlayClear()
|
||
drawHud()
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#colorforeground]
|
||
==== colorForeground
|
||
|
||
[source,text]
|
||
----
|
||
colorForeground(r, g, b)
|
||
colorForeground(r, g, b, a)
|
||
----
|
||
|
||
Imposta il colore di primo piano usato da `overlayBox`, `overlayCircle`, `overlayEllipse`, `overlayLine` e `overlayPlot`, e il colore del testo per `fontPrint` e `fontToSprite`. All'avvio è bianco opaco, e l'alfa vale `255` quando viene omesso, l'opposto di `colorBackground`. Gli sprite e `overlayPrint` sono disegnati con i propri pixel e non vengono tinti. I valori dei canali fuori intervallo vengono limitati, non rifiutati.
|
||
|
||
*Parametri:*
|
||
|
||
* `r`, `g`, `b` -- rosso, verde e blu, da `0` a `255`.
|
||
* `a` -- opacità, da `0` (trasparente) a `255` (opaco); predefinito `255`.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<colorbackground,colorBackground>>, <<overlaybox,overlayBox>>, <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Punteggio in bianco, avviso dell'ultima vita in rosso, entrambi con lo stesso font.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
fontSelect(hudFont)
|
||
colorForeground(255, 255, 255)
|
||
fontPrint(8, 8, "Score: " .. score)
|
||
if lives == 1 then
|
||
colorForeground(255, 32, 32)
|
||
fontPrint(8, 32, "LAST LIFE")
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#commandlineref]
|
||
=== Riga di comando
|
||
|
||
Tre chiamate riportano numeri che l'utente ha scelto all'avvio di Singe.
|
||
Esistono perché un gioco si possa regolare dal launcher invece che modificando
|
||
il gioco. Tutte e tre sono estensioni di Hypseus Singe, e Singe da sé non fa
|
||
nulla con i valori: è il gioco a decidere che cosa significano. Le opzioni sono
|
||
descritte in xref:Manual.adoc#commandlineoptions[Opzioni da riga di comando], e
|
||
si possono impostare anche in un file delle impostazioni (vedi
|
||
xref:Manual.adoc#settings[Il file delle impostazioni]).
|
||
|
||
Nessuna delle tre cambia durante un'esecuzione, quindi leggi ciascuna una volta
|
||
sola e conservala.
|
||
|
||
[#getfvalue]
|
||
==== getFValue
|
||
|
||
[source,text]
|
||
----
|
||
value = getFValue()
|
||
----
|
||
|
||
Estensione di Hypseus. Restituisce il numero dato come `--fvalue`, o `0` quando
|
||
l'opzione non è stata usata. È un canale di un solo numero da chi ha avviato
|
||
Singe al gioco, pensato per un valore che un front-end vuole variare da un avvio
|
||
all'altro -- una difficoltà, un numero di crediti iniziali, un indice di menu.
|
||
Il motore si limita a trasportarlo.
|
||
|
||
Il valore va da `0` a `100000` e conserva tre decimali, quanti ne conserva
|
||
Hypseus, così un gioco che confronta il numero vede lo stesso che vedeva lì.
|
||
|
||
*Restituisce:* numero.
|
||
|
||
*Disponibile da:* 3.00 (Hypseus)
|
||
*Vedi anche:* <<ratiogetx,ratioGetX>>, <<ratiogety,ratioGetY>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il launcher passa --fvalue 2 per il cabinato difficile.
|
||
local difficulty = getFValue()
|
||
if difficulty == 0 then
|
||
difficulty = 1
|
||
end
|
||
----
|
||
|
||
[#ratiogetx]
|
||
==== ratioGetX
|
||
|
||
[source,text]
|
||
----
|
||
factor = ratioGetX()
|
||
----
|
||
|
||
Estensione di Hypseus. Restituisce il numero dato come `--xratio`, o `0` quando
|
||
l'opzione non è stata usata. I giochi per pistola ottica scritti per Hypseus lo
|
||
leggono e ci stirano le proprie coordinate della pistola, per un cabinato il cui
|
||
schermo non ha la forma per cui il video è stato masterizzato -- `1.33` adatta
|
||
un pannello 16:9 a un filmato 4:3. Singe non lo applica a niente; i conti li fa
|
||
il gioco, esattamente come in Hypseus, ed è per questo che ogni gioco che lo
|
||
legge tratta `0` e qualsiasi valore sotto `1` come `1`.
|
||
|
||
Il valore va da `0` a `100` e conserva due decimali, come li conserva Hypseus.
|
||
|
||
*Restituisce:* numero.
|
||
|
||
*Disponibile da:* 3.00 (Hypseus)
|
||
*Vedi anche:* <<ratiogety,ratioGetY>>, <<getfvalue,getFValue>>, <<mousegetposition,mouseGetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La forma delle coordinate della pistola che vuole questo cabinato.
|
||
local ratioX = ratioGetX()
|
||
if ratioX <= 1 then
|
||
ratioX = 1
|
||
end
|
||
local offsetX = (ratioX - 1) * (overlayGetWidth() / 2)
|
||
----
|
||
|
||
[#ratiogety]
|
||
==== ratioGetY
|
||
|
||
[source,text]
|
||
----
|
||
factor = ratioGetY()
|
||
----
|
||
|
||
Estensione di Hypseus. La metà verticale della coppia: il numero dato come
|
||
`--yratio`, o `0` quando l'opzione non è stata usata. Vedi <<ratiogetx,ratioGetX>>.
|
||
|
||
*Restituisce:* numero.
|
||
|
||
*Disponibile da:* 3.00 (Hypseus)
|
||
*Vedi anche:* <<ratiogetx,ratioGetX>>, <<getfvalue,getFValue>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
local ratioY = ratioGetY()
|
||
if ratioY <= 1 then
|
||
ratioY = 1
|
||
end
|
||
----
|
||
|
||
[#controller]
|
||
=== Controller
|
||
|
||
Singe gestisce fino a quattro controller, indici da `0` a `3`, aperti automaticamente all'avvio e ogni volta che se ne collega o se ne scollega uno; non esiste `controllerLoad`. Gli assi analogici vengono segnalati tramite `onControllerMoved` (vedi <<enginecallbacks,Callback del motore>>) e interrogati con `controllerGetAxis`, nell'intervallo grezzo di SDL da `-32768` a `32767`. I pulsanti digitali, e ciascuna direzione di ciascun asse, sono interruttori come i tasti: arrivano a `onInputPressed` e `onInputReleased` quando `controls.cfg` assegna un codice delle tabelle da `GAMEPAD_0` a `GAMEPAD_3` (per esempio `INPUT_ACTION_1 = { GAMEPAD_0.BUTTON_A }`), e `controllerGetButton` interroga lo stato attuale di un pulsante. `SINGE_DEAD_ZONE`, dalla voce `DEAD_ZONE` di `controls.cfg`, è la soglia che il motore stesso usa prima che la direzione di un asse conti come un interruttore premuto; considera rumore il movimento di un asse al di sotto di essa. A un asse alla volta si può dare una soglia diversa con `DEAD_ZONES`, e ai grilletti analogici una soglia tutta loro con `TRIGGER_THRESHOLD` (che si rilegge come `SINGE_TRIGGER_THRESHOLD`); vedi xref:Manual.adoc#deadzones[Zone morte e grilletti]. Un dispositivo che SDL non riconosce come controller viene aperto comunque come tale, tramite un'assegnazione che Singe scrive apposta, così un encoder arcade o una cloche compare in questi slot come qualsiasi altro controller -- vedi xref:Manual.adoc#devices[Ogni dispositivo è un controller].
|
||
|
||
[#controllerdorumble]
|
||
==== controllerDoRumble
|
||
|
||
[source,text]
|
||
----
|
||
controllerDoRumble(strength, duration)
|
||
controllerDoRumble(controller, strength, duration)
|
||
----
|
||
|
||
Estensione di Hypseus. Fa vibrare un controller con entrambi i motori a una di quattro intensità per una di quattro durate. La forma a due argomenti si rivolge al controller `0`. Uno slot senza controller non fa nulla (lì Hypseus si interrompe, quindi i suoi giochi si proteggono con `controllerIsValid`); un'intensità o una durata non compresa tra `1` e `4`, o un indice non compreso tra `0` e `3`, termina lo script. Un controller che non supporta la vibrazione ignora la chiamata.
|
||
|
||
*Parametri:*
|
||
|
||
* `controller` -- indice del controller, da `0` a `3`.
|
||
* `strength` -- da `1` a `4`, da un quarto alla piena intensità.
|
||
* `duration` -- da `1` a `4`, cioè 150, 300, 600 o 1200 ms.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<controllerisvalid,controllerIsValid>>, <<controllergetbutton,controllerGetButton>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una breve scossa a ogni colpo, una lunga a piena intensità quando il giocatore muore.
|
||
function onHit()
|
||
if controllerIsValid(0) then
|
||
controllerDoRumble(0, 2, 1)
|
||
end
|
||
end
|
||
|
||
function onPlayerDied()
|
||
if controllerIsValid(0) then
|
||
controllerDoRumble(0, 4, 4)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#controllergetaxis]
|
||
==== controllerGetAxis
|
||
|
||
[source,text]
|
||
----
|
||
value = controllerGetAxis(controller, axis)
|
||
----
|
||
|
||
Restituisce la posizione di un asse analogico come il motore l'ha vista l'ultima volta. Il valore viene aggiornato da ogni evento dell'asse, compreso il movimento dentro la zona morta, ed è `0` per un controller non collegato o per un asse che non si è mai mosso. Le levette vanno da `-32768` a `32767`; i grilletti stanno a riposo su `0` e arrivano a `32767` quando sono premuti fino in fondo. Un indice non compreso tra `0` e `3`, o un asse non compreso tra `0` e `5`, termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `controller` -- indice del controller, da `0` a `3`.
|
||
* `axis` -- `GAMEPAD_AXIS_LEFT_X` (`0`), `GAMEPAD_AXIS_LEFT_Y` (`1`), `GAMEPAD_AXIS_RIGHT_X` (`2`), `GAMEPAD_AXIS_RIGHT_Y` (`3`), `GAMEPAD_AXIS_LEFT_TRIGGER` (`4`) o `GAMEPAD_AXIS_RIGHT_TRIGGER` (`5`).
|
||
|
||
*Restituisce:* intero, da `-32768` a `32767`.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<controllergetbutton,controllerGetButton>>, <<oncontrollermoved,onControllerMoved>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Sterza con la levetta sinistra, accelera con il grilletto destro.
|
||
function onOverlayUpdate()
|
||
local steer = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X)
|
||
local throttle = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_TRIGGER)
|
||
if math.abs(steer) > SINGE_DEAD_ZONE then
|
||
heading = heading + (steer / 32768) * turnRate
|
||
end
|
||
speed = speed + (throttle / 32767) * acceleration
|
||
drawHud()
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#controllergetbutton]
|
||
==== controllerGetButton
|
||
|
||
[source,text]
|
||
----
|
||
pressed = controllerGetButton(controller, button)
|
||
----
|
||
|
||
Restituisce se un pulsante è tenuto premuto in questo momento, letto direttamente dal controller anziché dalla coda degli eventi. Usalo per «tieni premuto per caricare» e per casi simili in cui `onInputPressed` e `onInputReleased` sono scomodi. Uno slot senza controller restituisce `false`. Il codice del pulsante deve venire dalla tabella `GAMEPAD_N` dello stesso indice di controller; un codice della tabella di un altro controller, o il codice di un asse, cade fuori dall'intervallo dei pulsanti e termina lo script, come un indice non compreso tra `0` e `3`.
|
||
|
||
*Parametri:*
|
||
|
||
* `controller` -- indice del controller, da `0` a `3`.
|
||
* `button` -- un valore `BUTTON_*` o `DPAD_*` della tabella corrispondente, come `GAMEPAD_0.BUTTON_A.value`.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<controllergetaxis,controllerGetAxis>>, <<keyboardisdown,keyboardIsDown>>, <<oninputpressedoninputreleased,onInputPressed>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Carica mentre A è premuto, spara al rilascio.
|
||
function onOverlayUpdate()
|
||
if controllerGetButton(0, GAMEPAD_0.BUTTON_A.value) then
|
||
charge = math.min(charge + 1, chargeMax)
|
||
elseif charge > 0 then
|
||
fireShot(charge)
|
||
charge = 0
|
||
end
|
||
drawChargeBar(charge)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#controllergetname]
|
||
==== controllerGetName
|
||
|
||
[source,text]
|
||
----
|
||
name = controllerGetName(controller)
|
||
----
|
||
|
||
Il nome del dispositivo in uno slot dei controller, come lo riporta SDL, o `nil` quando lo slot è vuoto. Un dispositivo che SDL non ha riconosciuto riporta il proprio nome tramite l'assegnazione che Singe ha scritto apposta, così una schermata di servizio può elencare per nome un encoder arcade con la stessa facilità di un controller di marca. Un indice non compreso tra `0` e `3` termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `controller` -- indice del controller, da `0` a `3`.
|
||
|
||
*Restituisce:* stringa, o `nil`.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<controllerisvalid,controllerIsValid>>, <<mousegetname,mouseGetName>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una schermata di servizio che elenca ciò che è collegato.
|
||
for slot = 0, 3 do
|
||
if controllerIsValid(slot) then
|
||
fontPrint(20, 20 + slot * 18, string.format("Pad %d: %s", slot + 1, controllerGetName(slot)))
|
||
else
|
||
fontPrint(20, 20 + slot * 18, string.format("Pad %d: not connected", slot + 1))
|
||
end
|
||
end
|
||
----
|
||
|
||
[#controllergetpadding]
|
||
==== controllerGetPadding
|
||
|
||
[source,text]
|
||
----
|
||
padding = controllerGetPadding()
|
||
----
|
||
|
||
Estensione di Hypseus, sempre `0` in Singe. Hypseus aggiunge questo scostamento al codice di un interruttore arrivato da un controller, così un gioco può capire quale dispositivo ha premuto cosa e recuperare comunque l'interruttore sottraendolo. Singe consegna l'input dei controller come normali interruttori assegnati, senza alcun segno del genere, quindi risponde `0`: un gioco che controlla l'intervallo spostato prende l'altro ramo e legge l'interruttore così com'è.
|
||
|
||
*Restituisce:* un intero, sempre `0`.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<controllersetpadding,controllerSetPadding>>
|
||
|
||
[#controllerhowmany]
|
||
==== controllerHowMany
|
||
|
||
[source,text]
|
||
----
|
||
count = controllerHowMany()
|
||
----
|
||
|
||
Estensione di Hypseus. Quanti controller sono aperti, dei quattro slot che Singe ha. Dato che ogni joystick che Singe riesce ad aprire diventa un controller (vedi xref:Manual.adoc#devices[Ogni dispositivo è un controller]), anche un encoder arcade o uno spinner viene contato qui.
|
||
|
||
Il conteggio cambia man mano che i dispositivi vengono collegati e scollegati, quindi leggilo quando ti serve invece che una volta sola all'avvio. Non dice nulla su quali slot siano occupati: un dispositivo può lasciare vuoto lo slot 0 mentre lo slot 1 è in uso, quindi <<controllerisvalid,`controllerIsValid`>> resta il modo per chiedere di uno slot in particolare.
|
||
|
||
*Parametri:* nessuno.
|
||
|
||
*Restituisce:* numero, da `0` a `4`.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<controllerisvalid,controllerIsValid>>, <<controllergetname,controllerGetName>>, <<mousehowmany,mouseHowMany>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Offri la partita a due giocatori solo quando il giocatore due ha qualcosa da impugnare.
|
||
local twoPlayer = controllerHowMany() >= 2
|
||
----
|
||
|
||
[#controllerisvalid]
|
||
==== controllerIsValid
|
||
|
||
[source,text]
|
||
----
|
||
valid = controllerIsValid(controller)
|
||
----
|
||
|
||
Estensione di Hypseus. Restituisce se in questo momento c'è un controller aperto nello slot; i controller vanno e vengono con il collegamento a caldo, quindi chiedi prima di ogni `controllerDoRumble`. Un indice non compreso tra `0` e `3` termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `controller` -- indice del controller, da `0` a `3`.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<controllerdorumble,controllerDoRumble>>, <<controllergetaxis,controllerGetAxis>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Mostra i suggerimenti per il controller solo quando c'è un controller a cui riferirli.
|
||
function drawAttract()
|
||
if controllerIsValid(0) then
|
||
fontPrint(HINT_X, HINT_Y, "Press A to start")
|
||
else
|
||
fontPrint(HINT_X, HINT_Y, "Press 1 to start")
|
||
end
|
||
end
|
||
----
|
||
|
||
[#controllersetpadding]
|
||
==== controllerSetPadding
|
||
|
||
[source,text]
|
||
----
|
||
controllerSetPadding(boolean)
|
||
----
|
||
|
||
Estensione di Hypseus, accettata e ignorata. Vedi <<controllergetpadding,controllerGetPadding>> per sapere perché lo scostamento che richiede non esiste in Singe. Attivarlo senza marcare gli eventi sarebbe peggio che lasciarlo spento, perché il gioco sottrarrebbe allora uno scostamento da codici di interruttore che non ne hanno mai avuto uno.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<controllergetpadding,controllerGetPadding>>
|
||
|
||
[#debug]
|
||
=== Debug
|
||
|
||
Due chiamate per vedere che cosa sta facendo uno script: `debugPrint` scrive del testo, e `lineDraw` disegna una linea sopra la scena 3D per un fotogramma (`physicsSetDebug` e `navDraw` disegnano tramite le stesse linee). L'output stampato va alla console da cui è stato avviato Singe, quindi è visibile durante lo sviluppo e innocuo in un cabinato, dove `--noconsole` lo zittisce. Quando Singe gira con `--trace`, ogni chiamata dell'API, compresa questa, viene anche scritta con i suoi argomenti in `trace.txt` nella cartella dei dati, ed è questo il modo di averne una registrazione permanente. Le opzioni da riga di comando sono elencate nel capitolo sull'avvio di Singe. Quando stampare non basta, <<zerobrane,ZeroBrane Studio>> ferma il gioco a un breakpoint, mostra che cosa c'è nelle sue variabili e lo fa avanzare una riga alla volta.
|
||
|
||
[[traceheader]]
|
||
==== L'intestazione della traccia
|
||
|
||
Qualunque fra `--program` e `--trace` l'abbia aperto, `trace.txt` comincia con
|
||
un blocco che descrive la macchina e l'esecuzione, racchiuso tra due righe
|
||
orizzontali, così da poterlo incollare intero in una segnalazione di bug:
|
||
|
||
----
|
||
----------------------------------------------------------------
|
||
Singe: v3.00
|
||
Command: ./Singe -w -p -d data --screen 2 MyGame
|
||
OS: Linux 6.8.0-124-generic (x86_64)
|
||
CPU: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz, 8 cores, 20177 MB
|
||
Renderer: gpu, GPU driver vulkan
|
||
Decoder: hardware vaapi or vdpau where the codec allows, software otherwise
|
||
SDL: built 3.4.16, linked 3.4.16
|
||
Settings: haptic=0 screen=2 volume_vldp=80
|
||
Game: MyGame/MyGame.singe
|
||
----------------------------------------------------------------
|
||
----
|
||
|
||
`Renderer` è il renderer effettivamente creato e il backend GPU che ha scelto,
|
||
oppure una nota che dice che non c'è un dispositivo GPU e che il 3D non è
|
||
disponibile. `Decoder` è ciò che verrà tentato per il video su questa macchina;
|
||
quale decoder abbia effettivamente ottenuto un certo video viene tracciato
|
||
all'apertura di quel video, qualche riga più in basso, dato che un codec può non
|
||
offrire alcuna via hardware. `Settings` elenca ciò che ha fornito il file delle
|
||
impostazioni, `none` quando non ce n'era. Ogni gioco di una coda ha il proprio
|
||
file e la propria intestazione.
|
||
|
||
[#debugprint]
|
||
==== debugPrint
|
||
|
||
[source,text]
|
||
----
|
||
debugPrint(message)
|
||
----
|
||
|
||
Stampa una riga sulla console (standard output). Non viene stampato nulla quando Singe è stato avviato con `--noconsole`; con `--trace` la chiamata e il suo messaggio finiscono anche in `trace.txt`. L'argomento deve essere una stringa o un numero; qualsiasi altra cosa termina lo script, quindi racchiudi i valori in `tostring` o usa `string.format`. Costa abbastanza poco da poterla lasciare nel codice che distribuisci, ma non è limitata, quindi una stampa dentro `onOverlayUpdate` viene eseguita a ogni fotogramma.
|
||
|
||
*Parametri:*
|
||
|
||
* `message` -- il testo da stampare; un numero viene convertito.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<singegetticks,singeGetTicks>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Registra perché il colpo è andato a vuoto, con il fotogramma in cui è successo.
|
||
function resolveShot(x, y)
|
||
local hit = targetAt(x, y)
|
||
if not hit then
|
||
debugPrint(string.format("miss at %d,%d on frame %d", x, y, discGetFrame()))
|
||
soundPlay(missClip)
|
||
return
|
||
end
|
||
score = score + hit.points
|
||
end
|
||
----
|
||
|
||
[#linedraw]
|
||
==== lineDraw
|
||
|
||
[source,text]
|
||
----
|
||
lineDraw(x0, y0, z0, x1, y1, z1)
|
||
lineDraw(x0, y0, z0, x1, y1, z1, r, g, b)
|
||
----
|
||
|
||
Disegna una linea retta tra due punti nello spazio del mondo, sopra la scena, solo per il fotogramma corrente, bianca a meno che non si indichi un colore. Chiamala a ogni fotogramma per tenere una linea sullo schermo, come con l'overlay. La linea è sottoposta al test di profondità rispetto alla scena ma non scrive la profondità, quindi si nasconde dietro i muri come qualsiasi altra cosa e non copre mai niente. Non viene disegnato nulla mentre la scena è disattivata. Serve a vedere che cosa sta facendo uno script (un raycast, un percorso, un bounding box); `physicsSetDebug` e `navDraw` disegnano tramite le stesse linee.
|
||
|
||
*Parametri:*
|
||
|
||
* `x0, y0, z0` -- il punto di partenza, in unità del mondo.
|
||
* `x1, y1, z1` -- il punto di arrivo, in unità del mondo.
|
||
* `r, g, b` -- il colore della linea, da `0` a `255` per canale, limitato a quell'intervallo; bianco quando omesso.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<physicssetdebug,physicsSetDebug>>, <<navdraw,navDraw>>, <<physicsraycast,physicsRaycast>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Mostra dove mira la torretta e se il colpo andrebbe a segno.
|
||
function onOverlayUpdate()
|
||
local tx, ty, tz = nodeGetWorldPosition(turret)
|
||
local hit, hx, hy, hz = physicsRaycast(tx, ty, tz, aimX, aimY, aimZ, 40)
|
||
if hit then
|
||
lineDraw(tx, ty, tz, hx, hy, hz, 255, 255, 0)
|
||
else
|
||
lineDraw(tx, ty, tz, tx + aimX * 40, ty + aimY * 40, tz + aimZ * 40, 120, 120, 120)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#disc]
|
||
=== Disco
|
||
|
||
Il *disco* è il video laserdisc principale, quello che Singe era nato per emulare. Un gioco ha al massimo un disco, aperto all'avvio dal `VIDEO` indicato in `games.dat` o da `--disc` / `--framefile` sulla riga di comando; un gioco che non ne indica nessuno gira senza disco (vedi <<withoutadisc,Giochi senza disco>>). Tutte le funzioni `disc*` agiscono su questo unico disco implicito e non accettano un handle. I numeri dei fotogrammi sono posizioni a base zero nel file video (o nel framefile combinato) e sono il riferimento temporale di ogni decisione di diramazione in un gioco laserdisc. Senza disco le funzioni di interrogazione restituiscono `0`, `discGetState` riporta `DISC_EJECTED`, e le funzioni di trasporto scrivono una riga nella traccia e non fanno nulla. Le clip in più sovrapposte al disco sono compito della famiglia <<video,Video>>.
|
||
|
||
[#discaudio]
|
||
==== discAudio
|
||
|
||
[source,text]
|
||
----
|
||
discAudio(channel, enabled)
|
||
----
|
||
|
||
Accende o spegne un canale della colonna sonora del disco. Il canale viene portato al volume laserdisc configurato (`--volume_vldp`, predefinito `100`) quando `enabled` è `true` e a `0` quando è `false`; l'altro canale resta com'è. Molti titoli originali avevano la narrazione o una traccia dati su un solo canale, e questa è la chiamata che la zittisce. Un canale diverso da `1` o `2` termina lo script. Senza disco la chiamata non fa nulla.
|
||
|
||
*Parametri:*
|
||
|
||
* `channel` -- `1` per il canale sinistro, `2` per il destro.
|
||
* `enabled` -- booleano. `true` riporta il canale al volume laserdisc configurato, `false` lo zittisce.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discsetaudiotrack,discSetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<videosetvolume,videoSetVolume>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il canale destro di questo disco porta la traccia dati della macchina arcade.
|
||
function startGame()
|
||
discAudio(1, true)
|
||
discAudio(2, false)
|
||
discSkipToFrame(SCENE_FIRST_FRAME)
|
||
end
|
||
----
|
||
|
||
[#discaudiosuffix]
|
||
==== discAudioSuffix
|
||
|
||
[source,text]
|
||
----
|
||
opened = discAudioSuffix(suffix)
|
||
----
|
||
|
||
Estensione di Hypseus. Passa il disco a un file audio separato che sta accanto al suo video: `<base><suffix>.ogg`, dove la base è il nome del file video senza estensione, così `movie.m2v` con `"-es"` riproduce `movie-es.ogg`. Il file viene aperto al fotogramma corrente e la riproduzione si riallinea, come fa `discSetAudioTrack`, mantenendo il volume, lo stato di riproduzione e l'indice della traccia quando il nuovo file ha abbastanza tracce. Con un framefile cambiano tutti i segmenti. Un suffisso vuoto ripristina l'audio predefinito: il `.ogg` accanto a un `.m2v`, o l'audio dentro qualsiasi altro contenitore. Restituisce `true` quando il file si è aperto; quando manca, l'audio precedente continua a suonare, non viene memorizzato nulla e torna `false` (in quel caso Hypseus lascia il silenzio). Il suffisso viene dimenticato quando lo script si ricarica. Senza disco la chiamata restituisce `false`. Usala per un menu delle lingue le cui tracce vengono distribuite come file separati; le tracce dentro un unico file sono compito di `discSetAudioTrack`.
|
||
|
||
*Parametri:*
|
||
|
||
* `suffix` -- stringa aggiunta al nome base del video, come `"-es"`; `""` per l'audio predefinito.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<discsetaudiotrack,discSetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discaudio,discAudio>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- I file delle lingue vengono distribuiti come movie.ogg, movie-es.ogg e movie-fr.ogg.
|
||
languages = { { "English", "" }, { "Spanish", "-es" }, { "French", "-fr" } }
|
||
|
||
function applyLanguage(index)
|
||
if not discAudioSuffix(languages[index][2]) then
|
||
discAudioSuffix("")
|
||
index = 1
|
||
end
|
||
settings.language = index
|
||
end
|
||
----
|
||
|
||
[#discchangespeed]
|
||
==== discChangeSpeed
|
||
|
||
[source,text]
|
||
----
|
||
discChangeSpeed(...)
|
||
----
|
||
|
||
Non fa nulla. I veri lettori laserdisc potevano andare a velocità diverse da 1x; il lettore software di Singe no, quindi la chiamata accetta qualsiasi argomento, scrive una riga nella traccia e ritorna. È mantenuta perché gli script anteriori alla 2.00 si carichino senza modifiche.
|
||
|
||
*Note:*
|
||
|
||
WARNING: *Non implementata.* Mantenuta solo per compatibilità con le versioni precedenti. Non c'è un sostituto; la velocità di riproduzione è fissata alla frequenza dei fotogrammi del file video.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discplay,discPlay>>, <<discskiptoframe,discSkipToFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- I vecchi script a volte racchiudevano un replay al rallentatore tra due cambi di velocità.
|
||
-- Oggi le chiamate sono innocue, ma il replay va a velocità normale.
|
||
discChangeSpeed(0.5)
|
||
discSkipToFrame(REPLAY_FIRST_FRAME)
|
||
discChangeSpeed(1)
|
||
----
|
||
|
||
[#discgetaudiotrack]
|
||
==== discGetAudioTrack
|
||
|
||
[source,text]
|
||
----
|
||
track = discGetAudioTrack()
|
||
----
|
||
|
||
Restituisce l'indice a base zero della traccia audio che il disco sta riproducendo. All'avvio il motore seleziona la traccia indicata da `--audio` o dalla voce `AUDIO_TRACK` di `games.dat` (predefinita `0`) quando il disco ha abbastanza tracce. Restituisce `0` senza disco.
|
||
|
||
*Restituisce:* indice intero della traccia.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<discgetaudiotracks,discGetAudioTracks>>, <<discsetaudiotrack,discSetAudioTrack>>, <<discgetlanguage,discGetLanguage>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Passa al doppiaggio successivo ogni volta che il giocatore preme il tasto della lingua.
|
||
function onInputPressed(what)
|
||
if what == SWITCH_BUTTON2 then
|
||
local next = (discGetAudioTrack() + 1) % discGetAudioTracks()
|
||
discSetAudioTrack(next)
|
||
languageLabel = discGetLanguageDescription(discGetLanguage(next))
|
||
end
|
||
end
|
||
----
|
||
|
||
[#discgetaudiotracks]
|
||
==== discGetAudioTracks
|
||
|
||
[source,text]
|
||
----
|
||
count = discGetAudioTracks()
|
||
----
|
||
|
||
Restituisce quanti flussi audio contiene il file video del disco. Più flussi sono di solito doppiaggi in lingue diverse. Un video muto riporta `0`, e così un gioco senza disco, quindi proteggi i cicli e l'aritmetica in modulo dallo zero.
|
||
|
||
*Restituisce:* conteggio intero, `0` o più.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<discgetaudiotrack,discGetAudioTrack>>, <<discgetlanguage,discGetLanguage>>, <<discsetaudiotrack,discSetAudioTrack>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Costruisci un menu di scelta della lingua dalle tracce audio del disco.
|
||
languages = {}
|
||
for track = 0, discGetAudioTracks() - 1 do
|
||
local code = discGetLanguage(track)
|
||
local name = discGetLanguageDescription(code)
|
||
table.insert(languages, { track = track, label = name })
|
||
end
|
||
----
|
||
|
||
[#discgetsubtitlelanguage]
|
||
==== discGetSubtitleLanguage
|
||
|
||
[source,text]
|
||
----
|
||
language = discGetSubtitleLanguage(track)
|
||
----
|
||
|
||
La lingua con cui è etichettata una traccia di sottotitoli dentro il contenitore stesso del disco, come il codice ISO 639 che riporta il file (`eng`, `fra`, `spa`). Una traccia che il file non etichetta risponde con una stringa vuota, che non è un errore -- moltissimi file non etichettano niente.
|
||
|
||
*Parametri:*
|
||
|
||
* `track` -- numero; da `0` a `discGetSubtitleTracks() - 1`.
|
||
|
||
*Restituisce:* una stringa.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<discgetsubtitletracks,discGetSubtitleTracks>>, <<srtloadtrack,srtLoadTrack>>
|
||
|
||
[#discgetsubtitletracks]
|
||
==== discGetSubtitleTracks
|
||
|
||
[source,text]
|
||
----
|
||
count = discGetSubtitleTracks()
|
||
----
|
||
|
||
Quante tracce di sottotitoli contiene il contenitore stesso del disco. `0` senza disco, e `0` per un disco i cui sottotitoli vengono distribuiti come `.srt` separato -- che è la disposizione abituale, ed è a questo che serve <<srtload,`srtLoad`>>.
|
||
|
||
Le tracce che contengono immagini anziché parole (quelle dei DVD e dei Blu-ray, VobSub e PGS) vengono contate qui perché la numerazione corrisponda a quella che mostra qualsiasi altro lettore, ma <<srtloadtrack,`srtLoadTrack`>> non può leggerle: non c'è niente da leggere, solo bitmap delle parole.
|
||
|
||
*Parametri:*
|
||
|
||
* Nessuno.
|
||
|
||
*Restituisce:* un numero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<srtloadtrack,srtLoadTrack>>, <<discgetsubtitlelanguage,discGetSubtitleLanguage>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Offri tutte le lingue che il disco stesso contiene.
|
||
for track = 0, discGetSubtitleTracks() - 1 do
|
||
languages[#languages + 1] = { track = track, code = discGetSubtitleLanguage(track) }
|
||
end
|
||
----
|
||
|
||
[#discgetframe]
|
||
==== discGetFrame
|
||
|
||
[source,text]
|
||
----
|
||
frame = discGetFrame()
|
||
----
|
||
|
||
Restituisce il fotogramma che il disco sta mostrando. Con un framefile il numero è la posizione nella sequenza combinata, quindi i confini tra i segmenti sono invisibili allo script. Restituisce `0` mentre il disco è fermo e senza disco. Interrogala da `onOverlayUpdate` per reagire quando il video raggiunge un punto di decisione.
|
||
|
||
*Restituisce:* numero intero del fotogramma.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discsearch,discSearch>>, <<discskiptoframe,discSkipToFrame>>, <<discgetstate,discGetState>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ripeti in ciclo la sequenza attract e mostra un'indicazione nella finestra in cui abbassarsi.
|
||
function onOverlayUpdate()
|
||
local frame = discGetFrame()
|
||
if frame >= ATTRACT_LAST_FRAME then
|
||
discSkipToFrame(ATTRACT_FIRST_FRAME)
|
||
elseif frame >= DUCK_FIRST_FRAME and frame <= DUCK_LAST_FRAME then
|
||
drawPrompt("DUCK")
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#discgetframecount]
|
||
==== discGetFrameCount
|
||
|
||
[source,text]
|
||
----
|
||
frames = discGetFrameCount()
|
||
----
|
||
|
||
Il numero di fotogrammi sul disco, che è uno in più del fotogramma più alto che
|
||
<<discsearch,discSearch>> troverà. Un framefile risponde per l'intero disco
|
||
anziché per il segmento in riproduzione, quindi il conteggio non cambia man mano
|
||
che i segmenti vanno e vengono. Senza disco risponde `0`.
|
||
|
||
*Restituisce:* numero intero di fotogrammi, `0` senza disco.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<discgetframe,discGetFrame>>, <<discsearch,discSearch>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Salta a metà del disco, ovunque si trovi.
|
||
discSearch(math.floor(discGetFrameCount() / 2))
|
||
----
|
||
|
||
[#discgetheight]
|
||
==== discGetHeight
|
||
|
||
[source,text]
|
||
----
|
||
height = discGetHeight()
|
||
----
|
||
|
||
Restituisce l'altezza del video del disco in pixel. Senza disco restituisce l'altezza del canvas (`CANVAS_Y` in `games.dat` o `--canvas`, predefinita `480`), quindi è sempre la dimensione dello sfondo su cui l'overlay viene stirato. Abbinala a `discGetWidth` per dare all'overlay la stessa risoluzione del video.
|
||
|
||
*Restituisce:* altezza intera in pixel.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<discgetwidth,discGetWidth>>, <<overlaysetresolution,overlaySetResolution>>, <<vldpgetheight,vldpGetHeight>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Disegna alla risoluzione nativa del disco, così i controlli sui pixel e il disegno condividono una sola griglia.
|
||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||
crosshairLimitX = discGetWidth() - 1
|
||
crosshairLimitY = discGetHeight() - 1
|
||
----
|
||
|
||
[#discgetlanguage]
|
||
==== discGetLanguage
|
||
|
||
[source,text]
|
||
----
|
||
code = discGetLanguage(track)
|
||
----
|
||
|
||
Restituisce l'etichetta della lingua memorizzata nel file video per una delle tracce audio del disco. L'etichetta è un codice ISO 639 di tre lettere come `"eng"` o `"jpn"`. Le tracce senza un'etichetta di tre lettere restituiscono `"unk"`, e così ogni traccia quando non c'è un disco. Con un disco, una traccia non compresa tra `0` e `discGetAudioTracks() - 1` termina lo script.
|
||
|
||
*Restituisce:* stringa con il codice della lingua.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<videogetlanguagedescription,videoGetLanguageDescription>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discsetaudiotrack,discSetAudioTrack>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Preferisci il doppiaggio giapponese quando il disco ne contiene uno.
|
||
for track = 0, discGetAudioTracks() - 1 do
|
||
if discGetLanguage(track) == "jpn" then
|
||
discSetAudioTrack(track)
|
||
break
|
||
end
|
||
end
|
||
----
|
||
|
||
[#discgetstate]
|
||
==== discGetState
|
||
|
||
[source,text]
|
||
----
|
||
state = discGetState()
|
||
----
|
||
|
||
Restituisce lo stato di trasporto del disco come una delle costanti `DISC_*`. Un gioco senza disco riporta `DISC_EJECTED`; dopo `discStop` il disco riporta `DISC_STOPPED` fino al successivo `discPlay`, `discSearch` o `discSkipToFrame`; altrimenti lo stato è `DISC_PLAYING` mentre il video avanza e `DISC_PAUSED` quando non avanza. Non esiste uno stato di ricerca a parte; un disco che sta eseguendo un salto riporta `DISC_PAUSED` o `DISC_PLAYING` a seconda di ciò che è stato richiesto.
|
||
|
||
*Restituisce:* una delle costanti seguenti, definite dal motore e disponibili a ogni script.
|
||
|
||
[cols="1,1,1",options="header"]
|
||
|===
|
||
| Costante | Valore | Significato
|
||
| `DISC_STOPPED` | `2` | Fermo (è stato chiamato `discStop`)
|
||
| `DISC_PLAYING` | `3` | In riproduzione
|
||
| `DISC_PAUSED` | `4` | In pausa
|
||
| `DISC_EJECTED` | `5` | Il gioco non ha un disco (vedi <<withoutadisc,Giochi senza disco>>)
|
||
|===
|
||
|
||
*Disponibile da:* 1.x (RDG)
|
||
*Vedi anche:* <<discplay,discPlay>>, <<discpause,discPause>>, <<discstop,discStop>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il tasto di pausa non deve far ripartire un fermo immagine chiesto dallo script della storia.
|
||
function onInputPressed(what)
|
||
if what == SWITCH_PAUSE then
|
||
if discGetState() == DISC_PAUSED and not storyPaused then
|
||
discPlay()
|
||
elseif discGetState() == DISC_PLAYING then
|
||
discPause()
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
[#discgetwidth]
|
||
==== discGetWidth
|
||
|
||
[source,text]
|
||
----
|
||
width = discGetWidth()
|
||
----
|
||
|
||
Restituisce la larghezza del video del disco in pixel. Senza disco restituisce la larghezza del canvas (`CANVAS_X` in `games.dat` o `--canvas`, predefinita `720`). Insieme a `discGetHeight` questa è la dimensione su cui viene scalato l'overlay, ed è per questo che il menu fornito con Singe le passa entrambe a `overlaySetResolution`.
|
||
|
||
*Restituisce:* larghezza intera in pixel.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<discgetheight,discGetHeight>>, <<overlaysetresolution,overlaySetResolution>>, <<vldpgetwidth,vldpGetWidth>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Centra lo sprite del titolo su qualunque sia la larghezza del disco.
|
||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||
titleSprite = spriteLoad("graphics/title.png")
|
||
titleX = (discGetWidth() - spriteGetWidth(titleSprite)) / 2
|
||
----
|
||
|
||
[#discpause]
|
||
==== discPause
|
||
|
||
[source,text]
|
||
----
|
||
discPause()
|
||
----
|
||
|
||
Mette in pausa la riproduzione sul fotogramma corrente, che resta sullo schermo. L'audio del disco va in pausa insieme al video. La chiamata viene ignorata mentre il disco è fermo e non fa nulla senza disco. Riprendi con `discPlay`.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discplay,discPlay>>, <<discstop,discStop>>, <<singesetpauseflag,singeSetPauseFlag>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Tieni l'ultimo fotogramma della scena mentre il giocatore sceglie una porta.
|
||
function onOverlayUpdate()
|
||
if discGetFrame() >= SCENE_LAST_FRAME and not choosing then
|
||
discPause()
|
||
choosing = true
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#discpauseatframe]
|
||
==== discPauseAtFrame
|
||
|
||
[source,text]
|
||
----
|
||
discPauseAtFrame(frame)
|
||
----
|
||
|
||
Salta a `frame`, lo mostra e si mette in pausa. È la stessa funzione di `discSearch` sotto un secondo nome; le due sono intercambiabili sotto ogni aspetto, comprese le regole di limitazione descritte lì. Preferisci `discSearch` nel codice nuovo e tieni `discPauseAtFrame` solo quando porti vecchi script.
|
||
|
||
*Note:*
|
||
|
||
WARNING: *Alias storico.* Usa <<discsearch,discSearch>> nel codice nuovo.
|
||
|
||
*Disponibile da:* 1.18
|
||
*Vedi anche:* <<discsearch,discSearch>>, <<discplay,discPlay>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Codice 1.x portato: fermati sul primo fotogramma della scena finché il giocatore non è pronto.
|
||
function onInputPressed(what)
|
||
if what == SWITCH_START1 then
|
||
discPauseAtFrame(SCENE_FIRST_FRAME)
|
||
waitingForPlayer = true
|
||
end
|
||
end
|
||
----
|
||
|
||
[#discplay]
|
||
==== discPlay
|
||
|
||
[source,text]
|
||
----
|
||
discPlay()
|
||
----
|
||
|
||
Avvia o riprende la riproduzione dal fotogramma corrente e annulla lo stato di fermo impostato da `discStop`. L'audio riprende con il video, e l'orologio della riproduzione riparte dal fotogramma corrente, così una lunga pausa non provoca uno scatto in avanti. Senza disco non fa nulla, a parte annullare l'indicatore di fermo.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discpause,discPause>>, <<discstop,discStop>>, <<discskiptoframe,discSkipToFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Avvio standard della modalità attract: porta il disco sul primo fotogramma, poi via.
|
||
function attractBegin()
|
||
discSearch(ATTRACT_FIRST_FRAME)
|
||
overlayClear()
|
||
discPlay()
|
||
attractRunning = true
|
||
end
|
||
----
|
||
|
||
[#discsearch]
|
||
==== discSearch
|
||
|
||
[source,text]
|
||
----
|
||
discSearch(frame)
|
||
----
|
||
|
||
Salta al fotogramma `frame`, lo mostra e si mette in pausa su di esso, annullando lo stato di fermo se il disco era fermo. Il disco tiene il fotogramma fino a `discPlay`, `discSkipToFrame` o a un'altra chiamata di trasporto. Un fotogramma negativo viene limitato al primo fotogramma e uno oltre la fine all'ultimo; con un framefile viene selezionato il segmento che contiene il fotogramma e la posizione viene limitata al suo interno. Senza disco non fa nulla.
|
||
|
||
*Parametri:*
|
||
|
||
* `frame` -- numero del fotogramma di destinazione, a base zero.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discskiptoframe,discSkipToFrame>>, <<discplay,discPlay>>, <<discgetframe,discGetFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il giocatore ha scelto il corridoio di sinistra: prepara la scena e aspetta il via.
|
||
function goLeft()
|
||
discSearch(CORRIDOR_LEFT_FIRST_FRAME)
|
||
soundPlay(SOUND_DOOR)
|
||
waitingForPlayer = true
|
||
end
|
||
----
|
||
|
||
[#discsearchblanking]
|
||
==== discSearchBlanking
|
||
|
||
[source,text]
|
||
----
|
||
discSearchBlanking(...)
|
||
----
|
||
|
||
Non fa nulla. Sui lettori laserdisc sceglieva se lo schermo si oscurasse mentre il lettore cercava un fotogramma; il lettore software di Singe salta senza oscurare, quindi la chiamata accetta qualsiasi argomento, scrive una riga nella traccia e ritorna.
|
||
|
||
*Note:*
|
||
|
||
WARNING: *Non implementata.* Mantenuta solo per compatibilità con le versioni precedenti. Non c'è un sostituto.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discsearch,discSearch>>, <<discskipblanking,discSkipBlanking>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Gli script anteriori alla 2.00 spesso impostavano le modalità di oscuramento una volta all'avvio. Innocuo.
|
||
discSearchBlanking(false)
|
||
discSkipBlanking(false)
|
||
discSearch(TITLE_FRAME)
|
||
----
|
||
|
||
[#discsetaudiotrack]
|
||
==== discSetAudioTrack
|
||
|
||
[source,text]
|
||
----
|
||
discSetAudioTrack(track)
|
||
----
|
||
|
||
Passa il disco a un'altra traccia audio. L'audio in coda della vecchia traccia viene scartato e la riproduzione si riallinea, così il cambio si sente quasi subito. Con un disco, una traccia non compresa tra `0` e `discGetAudioTracks() - 1` termina lo script; senza disco la chiamata non fa nulla. Usala per realizzare un menu delle lingue o per rispettare una preferenza salvata tra una sessione e l'altra.
|
||
|
||
*Parametri:*
|
||
|
||
* `track` -- indice della traccia audio, a base zero.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<discgetaudiotrack,discGetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discgetlanguage,discGetLanguage>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Applica la lingua che il giocatore ha scelto nella schermata delle opzioni.
|
||
function applyLanguage(choice)
|
||
if choice >= 0 and choice < discGetAudioTracks() then
|
||
discSetAudioTrack(choice)
|
||
settings.language = choice
|
||
saveSettings()
|
||
end
|
||
end
|
||
----
|
||
|
||
[#discsetfps]
|
||
==== discSetFPS
|
||
|
||
[source,text]
|
||
----
|
||
discSetFPS(...)
|
||
----
|
||
|
||
Non fa nulla. La frequenza dei fotogrammi viene dal file video, quindi la chiamata accetta qualsiasi argomento, scrive una riga nella traccia e ritorna. Niente nel `Framework.singe` distribuito la chiama; lo fanno solo gli script anteriori alla 2.00.
|
||
|
||
*Note:*
|
||
|
||
WARNING: *Non implementata.* Mantenuta solo per compatibilità con le versioni precedenti. La frequenza dei fotogrammi viene letta dal file video e non si può impostare da uno script.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discgetframe,discGetFrame>>, <<discskiptoframe,discSkipToFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Uno script 1.x portato. La chiamata viene ignorata; i fotogrammi al secondo sono una proprietà del file.
|
||
discSetFPS(29.97)
|
||
discSearch(TITLE_FRAME)
|
||
discPlay()
|
||
----
|
||
|
||
[#discskipbackward]
|
||
==== discSkipBackward
|
||
|
||
[source,text]
|
||
----
|
||
discSkipBackward(frames)
|
||
----
|
||
|
||
Salta indietro di `frames` fotogrammi dal fotogramma corrente senza cambiare lo stato di riproduzione o di pausa: un disco in riproduzione continua a riprodurre dalla nuova posizione, un disco in pausa resta in pausa lì. La chiamata viene ignorata mentre il disco è fermo e senza disco. Il delta non viene convalidato; un valore negativo salta in avanti, e un delta maggiore del fotogramma corrente viene limitato al fotogramma `0`. Con un framefile il delta si applica al numero di fotogramma che riporta `discGetFrame`, quindi un salto può passare in un segmento precedente.
|
||
|
||
*Parametri:*
|
||
|
||
* `frames` -- numero di fotogrammi da sottrarre al fotogramma corrente.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discskipforward,discSkipForward>>, <<discstepbackward,discStepBackward>>, <<discgetframe,discGetFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ripeti l'ultimo secondo (30 fotogrammi) quando il giocatore manca un'indicazione.
|
||
function missedPrompt()
|
||
lives = lives - 1
|
||
discSkipBackward(30)
|
||
promptShown = false
|
||
end
|
||
----
|
||
|
||
[#discskipblanking]
|
||
==== discSkipBlanking
|
||
|
||
[source,text]
|
||
----
|
||
discSkipBlanking(...)
|
||
----
|
||
|
||
Non fa nulla. Un tempo sceglieva se lo schermo si oscurasse durante un salto sui lettori laserdisc; il lettore software di Singe non oscura mai, quindi la chiamata accetta qualsiasi argomento, scrive una riga nella traccia e ritorna.
|
||
|
||
*Note:*
|
||
|
||
WARNING: *Non implementata.* Mantenuta solo per compatibilità con le versioni precedenti. Non c'è un sostituto.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discsearchblanking,discSearchBlanking>>, <<discskiptoframe,discSkipToFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Gli script anteriori alla 2.00 spesso impostavano le modalità di oscuramento una volta all'avvio. Innocuo.
|
||
discSearchBlanking(false)
|
||
discSkipBlanking(false)
|
||
discSearch(TITLE_FRAME)
|
||
----
|
||
|
||
[#discskipforward]
|
||
==== discSkipForward
|
||
|
||
[source,text]
|
||
----
|
||
discSkipForward(frames)
|
||
----
|
||
|
||
Salta in avanti di `frames` fotogrammi dal fotogramma corrente senza cambiare lo stato di riproduzione o di pausa. Speculare a `discSkipBackward`: viene ignorata mentre il disco è fermo e senza disco, il delta non viene convalidato, e una destinazione oltre l'ultimo fotogramma viene limitata all'ultimo fotogramma. Con un framefile il delta si applica al numero di fotogramma che riporta `discGetFrame`, quindi un salto può passare in un segmento successivo.
|
||
|
||
*Parametri:*
|
||
|
||
* `frames` -- numero di fotogrammi da aggiungere al fotogramma corrente.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discskipbackward,discSkipBackward>>, <<discstepforward,discStepForward>>, <<discgetframe,discGetFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Lascia che chi ha già giocato salti l'introduzione di due secondi della scena.
|
||
function onInputPressed(what)
|
||
if what == SWITCH_BUTTON1 and inSceneIntro then
|
||
discSkipForward(60)
|
||
inSceneIntro = false
|
||
end
|
||
end
|
||
----
|
||
|
||
[#discskiptoframe]
|
||
==== discSkipToFrame
|
||
|
||
[source,text]
|
||
----
|
||
discSkipToFrame(frame)
|
||
----
|
||
|
||
Salta a `frame` e inizia a riprodurre da lì qualunque sia lo stato del disco, annullando lo stato di fermo se necessario. Al contrario, `discSearch` salta e si mette in pausa. Un fotogramma prima dell'inizio o oltre la fine viene limitato al primo o all'ultimo fotogramma; con un framefile viene selezionato il segmento che contiene il fotogramma e la posizione viene limitata al suo interno. Senza disco non fa nulla, a parte annullare l'indicatore di fermo.
|
||
|
||
*Parametri:*
|
||
|
||
* `frame` -- numero del fotogramma di destinazione, a base zero.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discsearch,discSearch>>, <<discplay,discPlay>>, <<discgetframe,discGetFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Salta l'introduzione quando si gioca di nuovo.
|
||
function startGame()
|
||
if hasPlayedBefore then
|
||
discSkipToFrame(GAMEPLAY_FIRST_FRAME)
|
||
else
|
||
discSkipToFrame(INTRO_FIRST_FRAME)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#discstepbackward]
|
||
==== discStepBackward
|
||
|
||
[source,text]
|
||
----
|
||
discStepBackward()
|
||
----
|
||
|
||
Porta il disco indietro di esattamente un fotogramma e si mette in pausa su di esso, qualunque fosse lo stato di riproduzione precedente. Un passo indietro dal fotogramma `0` resta sul fotogramma `0`. Viene ignorata mentre il disco è fermo, come `discSkipBackward`. Pensata per il debug al fotogramma esatto e per le schermate di servizio. Senza disco non fa nulla.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discstepforward,discStepForward>>, <<discskipbackward,discSkipBackward>>, <<discgetframe,discGetFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Schermata di servizio: sinistra e destra spostano il disco di un fotogramma alla volta.
|
||
function onInputPressed(what)
|
||
if what == SWITCH_LEFT then
|
||
discStepBackward()
|
||
elseif what == SWITCH_RIGHT then
|
||
discStepForward()
|
||
end
|
||
end
|
||
----
|
||
|
||
[#discstepforward]
|
||
==== discStepForward
|
||
|
||
[source,text]
|
||
----
|
||
discStepForward()
|
||
----
|
||
|
||
Porta il disco avanti di esattamente un fotogramma e si mette in pausa su di esso, qualunque fosse lo stato di riproduzione precedente. Un passo avanti dall'ultimo fotogramma resta sull'ultimo fotogramma. Come `discStepBackward`, viene ignorata mentre il disco è fermo. Senza disco non fa nulla.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discstepbackward,discStepBackward>>, <<discskipforward,discSkipForward>>, <<discgetframe,discGetFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Cercafotogrammi: scorri una scena un fotogramma alla volta e stampa il numero del fotogramma.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
overlayPrint(8, 8, "Frame " .. discGetFrame())
|
||
if stepping then
|
||
discStepForward()
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#discstop]
|
||
==== discStop
|
||
|
||
[source,text]
|
||
----
|
||
discStop()
|
||
----
|
||
|
||
Ferma il disco. La riproduzione va in pausa, il video viene sostituito dalla classica schermata blu, `discGetState` riporta `DISC_STOPPED` e `discGetFrame` restituisce `0` finché il disco non viene riavviato. Mentre il disco è fermo, `discPause`, `discSkipForward`, `discSkipBackward`, `discStepForward` e `discStepBackward` vengono ignorate. Una qualsiasi fra `discPlay`, `discSearch` e `discSkipToFrame` annulla lo stato di fermo. Un secondo `discStop` viene ignorato.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discplay,discPlay>>, <<discpause,discPause>>, <<discgetstate,discGetState>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Game over: passa alla schermata blu dietro la tabella dei punteggi.
|
||
function gameOver()
|
||
discStop()
|
||
soundPlay(SOUND_GAME_OVER)
|
||
showScores = true
|
||
end
|
||
----
|
||
|
||
[#emitter]
|
||
=== Emettitore
|
||
|
||
Gli emettitori generano, muovono e disegnano particelle; vedi <<particles,Particelle>>.
|
||
Gli handle sono interi. Un emettitore 2D vive nelle coordinate dell'overlay, con
|
||
la Y verso il basso, e disegna quando `emitterDraw` lo chiede da
|
||
`onOverlayUpdate`; un emettitore 3D sta su un nodo, vive in unità del mondo e si
|
||
disegna da sé nella scena. Le chiamate della ricetta si possono fare in
|
||
qualsiasi ordine, prima o dopo che l'emettitore parta, e hanno effetto sulle
|
||
particelle che nascono da quel momento (alcune, indicate sotto, agiscono anche
|
||
su quelle vive). Tutto ciò che è dato come minimo e massimo viene estratto a
|
||
caso tra i due per ogni particella. Le particelle avanzano con l'orologio, al
|
||
massimo di un decimo di secondo per fotogramma, e restano ferme mentre il gioco
|
||
è in pausa.
|
||
|
||
[#emitternew]
|
||
==== emitterNew
|
||
|
||
[source,text]
|
||
----
|
||
emitter = emitterNew()
|
||
emitter = emitterNew(node)
|
||
----
|
||
|
||
Crea un emettitore. Senza argomenti è un emettitore 2D nella posizione dell'overlay `(0, 0)`, che si sposta con `emitterSetPosition`; con un nodo è un emettitore 3D le cui particelle nascono nella posizione nel mondo di quel nodo, e ha bisogno della scena 3D, quindi su una macchina che non ce l'ha la chiamata termina lo script (gli emettitori 2D funzionano ovunque). Un nuovo emettitore non emette un flusso; chiama `emitterStart` o `emitterBurst`. Eliminare il nodo elimina i suoi emettitori. Valori predefiniti: 50 particelle al secondo, che vivono da uno a due secondi, apertura di 30 gradi, bianco che sfuma nel trasparente, fusione normale, niente gravità, resistenza, rotazione, raggio, texture, scia o collisione, una riserva di 1000; un emettitore 2D le lancia verso l'alto dello schermo a 50-100 unità dell'overlay al secondo, rimpicciolendole da 16 a 4, uno 3D verso l'alto lungo l'asse Y a 1-2 unità del mondo al secondo, rimpicciolendole da 0,2 a 0,05.
|
||
|
||
*Restituisce:* L'handle dell'emettitore.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emitterdelete,emitterDelete>>, <<emitterstart,emitterStart>>, <<emitterburst,emitterBurst>>, <<emitterdraw,emitterDraw>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Gas di scarico dal nodo del motore della nave; la ricetta si imposta prima che parta il flusso.
|
||
exhaust = emitterNew(nodeFind("Engine", ship))
|
||
emitterSetBlend(exhaust, PARTICLE_ADD)
|
||
emitterSetRate(exhaust, 120)
|
||
emitterSetLife(exhaust, 0.3, 0.6)
|
||
emitterSetDirection(exhaust, 0, 0, 1)
|
||
emitterSetSpeed(exhaust, 3, 5)
|
||
emitterSetSize(exhaust, 0.3, 0.05)
|
||
emitterSetColor(exhaust, 120, 180, 255, 255, 30, 60, 255, 0)
|
||
emitterStart(exhaust)
|
||
----
|
||
|
||
[#emitterdelete]
|
||
==== emitterDelete
|
||
|
||
[source,text]
|
||
----
|
||
emitterDelete(emitter)
|
||
----
|
||
|
||
Rimuove all'istante l'emettitore, le sue particelle vive e la sua copia della texture; niente si dissolve. Usa invece `emitterStop` per lasciare che le ultime particelle finiscano. Un emettitore 3D viene rimosso da sé anche quando viene eliminato il suo nodo. Un handle che non è un emettitore vivo termina lo script, quindi dopo azzera la variabile.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emitternew,emitterNew>>, <<emitterstop,emitterStop>>, <<emitterisactive,emitterIsActive>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- L'esplosione è finita: libera il suo emettitore quando l'ultima scintilla si è spenta.
|
||
function onOverlayUpdate()
|
||
if explosion ~= nil and not emitterIsActive(explosion) then
|
||
emitterDelete(explosion)
|
||
explosion = nil
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#emittersettexture]
|
||
==== emitterSetTexture
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetTexture(emitter, sprite)
|
||
emitterSetTexture(emitter)
|
||
----
|
||
|
||
L'immagine che porta ogni particella: uno sprite caricato, o nessuna (un solo argomento, o `nil`) per il disco sfumato incorporato. L'emettitore tiene una propria copia dei pixel dello sprite, compreso ogni fotogramma di uno sprite animato, quindi dopo lo sprite si può scaricare. Impostare la texture riporta l'intervallo di fotogrammi al fotogramma `0` e sposta ogni particella viva al fotogramma `0`, quindi chiama `emitterSetFrames` dopo, non prima. La fusione e la tinta si applicano all'immagine come al disco.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetframes,emitterSetFrames>>, <<spriteload,spriteLoad>>, <<emittersetblend,emitterSetBlend>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Neve: un foglio con sei forme di fiocco, una scelta a caso per ogni particella.
|
||
flakes = spriteLoadFrames(6, DIR .. "Sprites/flakes.png")
|
||
snow = emitterNew()
|
||
emitterSetTexture(snow, flakes)
|
||
emitterSetFrames(snow, 0, 5)
|
||
emitterSetDirection(snow, 0, 1)
|
||
emitterSetSpeed(snow, 20, 40)
|
||
emitterSetLife(snow, 6, 9)
|
||
emitterSetSize(snow, 6, 6, 0.4)
|
||
emitterStart(snow)
|
||
----
|
||
|
||
[#emittersetframes]
|
||
==== emitterSetFrames
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetFrames(emitter, first, last)
|
||
----
|
||
|
||
Con uno sprite animato come texture, ogni nuova particella prende un fotogramma a caso da `first` a `last` e lo tiene per tutta la vita; i fotogrammi si contano da `0`. I valori vengono limitati ai fotogrammi che ha la texture, e `last` viene alzato a `first` quando è più piccolo. Predefinito: solo il fotogramma `0`, che è anche ciò a cui riporta `emitterSetTexture`. Con il disco incorporato o con uno sprite di un solo fotogramma l'intervallo non ha effetto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersettexture,emitterSetTexture>>, <<spriteload,spriteLoad>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il foglio degli sbuffi ha quattro sbuffi morbidi, poi quattro frammenti di detriti più duri.
|
||
emitterSetTexture(smoke, puffSheet)
|
||
emitterSetFrames(smoke, 0, 3)
|
||
emitterSetTexture(debris, puffSheet)
|
||
emitterSetFrames(debris, 4, 7)
|
||
----
|
||
|
||
[#emittersetblend]
|
||
==== emitterSetBlend
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetBlend(emitter, mode)
|
||
----
|
||
|
||
Come le particelle si combinano con ciò che sta dietro di loro. `PARTICLE_ALPHA` (predefinito) fonde normalmente, per fumo, polvere, pioggia e detriti; `PARTICLE_ADD` aggiunge luce, per fuoco, scintille, bagliori e magia, e non scurisce mai. Ha effetto subito sulle particelle vive. Qualsiasi cosa diversa da queste due costanti termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetcolor,emitterSetColor>>, <<emittersettexture,emitterSetTexture>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Le scintille brillano; il fumo sopra di loro no.
|
||
emitterSetBlend(sparks, PARTICLE_ADD)
|
||
emitterSetColor(sparks, 255, 220, 120, 255, 255, 80, 20, 0)
|
||
emitterSetBlend(smoke, PARTICLE_ALPHA)
|
||
emitterSetColor(smoke, 160, 160, 170, 120, 120, 120, 130, 0)
|
||
----
|
||
|
||
[#emittersetlayer]
|
||
==== emitterSetLayer
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetLayer(emitter, layer)
|
||
----
|
||
|
||
Solo 2D: dove `emitterDraw` mette le particelle. `PARTICLE_OVER` (predefinito) disegna sopra tutto ciò che c'è sull'overlay; `PARTICLE_UNDER` disegna sotto l'overlay e sopra il video e la scena 3D, per il fumo dietro il punteggio. Su un emettitore 3D l'impostazione viene accettata e ignorata. Qualsiasi cosa diversa da queste due costanti termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emitterdraw,emitterDraw>>, <<emittersetposition,emitterSetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La pioggia cade dietro l'HUD, le scintille dei colpi davanti.
|
||
rain = emitterNew()
|
||
emitterSetLayer(rain, PARTICLE_UNDER)
|
||
hits = emitterNew()
|
||
emitterSetLayer(hits, PARTICLE_OVER)
|
||
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
emitterDraw(rain)
|
||
drawHud()
|
||
emitterDraw(hits)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#emittersetrate]
|
||
==== emitterSetRate
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetRate(emitter, perSecond)
|
||
----
|
||
|
||
Quante particelle al secondo crea un emettitore avviato; predefinito `50`, i valori negativi diventano `0`. Le frazioni si accumulano, quindi `0.5` ne crea una ogni due secondi. Cambialo durante il flusso per regolare una fiamma o un propulsore. Una riserva piena (`emitterSetMax`) non ne crea altre finché alcune non muoiono, qualunque sia la frequenza.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emitterstart,emitterStart>>, <<emitterburst,emitterBurst>>, <<emittersetmax,emitterSetMax>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il pennacchio del propulsore segue l'acceleratore.
|
||
function onControllerMoved(axis, value, which)
|
||
if axis == GAMEPAD_AXIS_RIGHT_TRIGGER then
|
||
throttle = value / 32767
|
||
emitterSetRate(plume, 30 + 300 * throttle)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#emittersetlife]
|
||
==== emitterSetLife
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetLife(emitter, minSeconds, maxSeconds)
|
||
----
|
||
|
||
Quanto vive ogni nuova particella, estratto a caso tra i due valori; predefinito da `1` a `2`. La sua dimensione e il suo colore vanno dal valore iniziale a quello finale in quel tempo, quindi la vita detta il ritmo di ogni dissolvenza. Il minimo viene tenuto a `0.001` o più e il massimo viene alzato al minimo quando è più piccolo. Le particelle vive mantengono la vita con cui sono nate.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetsize,emitterSetSize>>, <<emittersetcolor,emitterSetColor>>, <<emittersetrate,emitterSetRate>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Vampa di bocca: una raffica che sparisce in un decimo di secondo.
|
||
flash = emitterNew(muzzle)
|
||
emitterSetBlend(flash, PARTICLE_ADD)
|
||
emitterSetLife(flash, 0.05, 0.1)
|
||
emitterSetSpeed(flash, 2, 6)
|
||
emitterSetSize(flash, 0.15, 0.4)
|
||
|
||
function onFire()
|
||
emitterBurst(flash, 12)
|
||
end
|
||
----
|
||
|
||
[#emittersetsoftness]
|
||
==== emitterSetSoftness
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetSoftness(emitter, distance)
|
||
----
|
||
|
||
Solo 3D: dissolve una particella nell'arco di `distance` unità del mondo dove incontra la geometria dietro di essa, così uno sbuffo di fumo non disegna una linea netta dove attraversa un muro e una fontana affonda nella sua vasca invece di tagliarla. `0` (il predefinito) la disattiva; i valori negativi diventano `0`. Finché un qualsiasi emettitore la usa, la profondità della scena viene resa una volta in più per fotogramma, quindi su un Raspberry Pi va usata con parsimonia. Un emettitore 2D memorizza il valore e lo ignora.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetlit,emitterSetLit>>, <<emittersetblend,emitterSetBlend>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il fumo del braciere si ammorbidisce contro il palo accanto a cui sale.
|
||
smoke = emitterNew(brazierTop)
|
||
emitterSetRate(smoke, 25)
|
||
emitterSetLife(smoke, 1.5, 2.5)
|
||
emitterSetSize(smoke, 0.2, 0.9, 0.2)
|
||
emitterSetColor(smoke, 160, 160, 170, 120, 120, 120, 130, 0)
|
||
emitterSetLit(smoke, true)
|
||
emitterSetSoftness(smoke, 0.3)
|
||
emitterStart(smoke)
|
||
----
|
||
|
||
[#emittersetspeed]
|
||
==== emitterSetSpeed
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetSpeed(emitter, min, max)
|
||
----
|
||
|
||
Velocità iniziale di ogni nuova particella, estratta a caso tra i due valori, in unità dell'overlay al secondo (2D) o unità del mondo al secondo (3D); predefinita da `50` a `100` in 2D e da `1` a `2` in 3D. Il massimo viene alzato al minimo quando è più piccolo. La velocità stabilisce quanto lontano arriva una particella prima che la resistenza, la gravità e la sua vita prendano il sopravvento.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetdirection,emitterSetDirection>>, <<emittersetdrag,emitterSetDrag>>, <<emittersetgravity,emitterSetGravity>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una fontana lancia le gocce forte e dritte; la polvere si muove appena.
|
||
emitterSetSpeed(fountain, 4.0, 5.0)
|
||
emitterSetSpread(fountain, 8)
|
||
emitterSetSpeed(dust, 0.05, 0.2)
|
||
emitterSetSpread(dust, 180)
|
||
----
|
||
|
||
[#emittersetdirection]
|
||
==== emitterSetDirection
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetDirection(emitter, x, y)
|
||
emitterSetDirection(emitter, x, y, z)
|
||
----
|
||
|
||
La direzione media in cui partono le nuove particelle; conta solo la direzione, non la lunghezza. Predefinita in 2D `(0, -1)`, verso l'alto dello schermo dato che la Y dell'overlay cresce verso il basso; in 3D `(0, 1, 0)`, verso l'alto lungo l'asse Y del mondo. Un emettitore 2D ignora `z`; un emettitore 3D a cui si danno due valori riceve `z = 0`. Un vettore nullo viene trattato come `(0, -1, 0)`. Per un emettitore 3D la direzione è negli assi del mondo, non in quelli del nodo, quindi ruotala tu quando il nodo ruota.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetspread,emitterSetSpread>>, <<emittersetspeed,emitterSetSpeed>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- I gas di scarico escono dietro la nave da qualunque parte sia rivolta.
|
||
function onOverlayUpdate()
|
||
local heading = math.rad(shipYaw)
|
||
emitterSetDirection(exhaust, -math.sin(heading), 0, -math.cos(heading))
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#emittersetspread]
|
||
==== emitterSetSpread
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetSpread(emitter, degrees)
|
||
----
|
||
|
||
Metà dell'angolo del cono (3D) o del ventaglio (2D) attorno alla direzione in cui vengono lanciate le nuove particelle, da `0` a `180`; `0` è una linea retta, `180` è ogni direzione. Predefinito `30`; i valori fuori intervallo vengono limitati. In 3D le direzioni sono distribuite uniformemente sulla calotta del cono, così un cono ampio non si addensa verso il suo asse.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetdirection,emitterSetDirection>>, <<emittersetradius,emitterSetRadius>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- I fuochi d'artificio vanno ovunque; la fiamma della torcia resta in una colonna stretta.
|
||
emitterSetSpread(fireworks, 180)
|
||
emitterSetSpread(flame, 15)
|
||
----
|
||
|
||
[#emittersetgravity]
|
||
==== emitterSetGravity
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetGravity(emitter, x, y)
|
||
emitterSetGravity(emitter, x, y, z)
|
||
----
|
||
|
||
Un'accelerazione costante su ogni particella, in unità dell'overlay (2D) o unità del mondo (3D) al secondo quadrato: `(0, 500)` tira le scintille 2D verso il basso dello schermo, `(0, -20)` fa salire il fumo 2D, `(0, -9.8, 0)` è la Terra nella scena. Predefinita: nessuna. Si applica alle particelle vive dal fotogramma successivo. Le particelle non sono corpi fisici; dai loro `emitterSetCollide` per farle rimbalzare su un pavimento o sulla scena.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetdrag,emitterSetDrag>>, <<emittersetcollide,emitterSetCollide>>, <<emittersetspeed,emitterSetSpeed>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Le scintille cadono, il fumo sale, entrambi dalla stessa smerigliatrice.
|
||
emitterSetGravity(sparks, 0, 500)
|
||
emitterSetGravity(smoke, 0, -20)
|
||
|
||
-- Sott'acqua, le bolle puntano verso la superficie.
|
||
emitterSetGravity(bubbles, 0, 3, 0)
|
||
----
|
||
|
||
[#emittersetdrag]
|
||
==== emitterSetDrag
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetDrag(emitter, perSecond)
|
||
----
|
||
|
||
Quanto in fretta le particelle perdono la loro velocità: a ogni fotogramma la velocità viene moltiplicata per `1 - perSecond * dt`, mai sotto `0`, quindi `1` toglie gran parte della velocità entro un secondo e `0` (il predefinito) la conserva per sempre. I valori negativi diventano `0`. Fumo, braci e fuochi d'artificio rallentano; scintille e pioggia no. Si applica alle particelle vive dal fotogramma successivo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetgravity,emitterSetGravity>>, <<emittersetspeed,emitterSetSpeed>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una bomba pirotecnica: esce veloce, poi resta sospesa e ricade.
|
||
fireworks = emitterNew()
|
||
emitterSetBlend(fireworks, PARTICLE_ADD)
|
||
emitterSetSpeed(fireworks, 120, 320)
|
||
emitterSetSpread(fireworks, 180)
|
||
emitterSetGravity(fireworks, 0, 220)
|
||
emitterSetDrag(fireworks, 0.8)
|
||
emitterSetLife(fireworks, 0.8, 1.6)
|
||
----
|
||
|
||
[#emittersetsize]
|
||
==== emitterSetSize
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetSize(emitter, start, finish)
|
||
emitterSetSize(emitter, start, finish, variation)
|
||
----
|
||
|
||
La dimensione alla nascita e alla morte, in unità dell'overlay (2D) o unità del mondo (3D), che cambia in modo uniforme nel corso della vita della particella; predefinita da `16` a `4` in 2D e da `0.2` a `0.05` in 3D. `variation` (da `0` a `1`, predefinito `0`) scala entrambe le dimensioni di ogni nuova particella per un unico fattore casuale tra `1 - variation` e `1 + variation`, così uno sbuffo è un po' più grande o più piccolo per tutta la vita. Le dimensioni negative diventano `0` e `variation` viene limitato. Una particella con texture viene disegnata come un quadrato di quella dimensione; un'immagine più larga viene schiacciata.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetlife,emitterSetLife>>, <<emittersetcolor,emitterSetColor>>, <<emittersetspin,emitterSetSpin>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il fumo cresce mentre si dirada; le scintille si rimpiccioliscono fino a sparire.
|
||
emitterSetSize(smoke, 12, 48, 0.2)
|
||
emitterSetSize(sparks, 5, 1)
|
||
----
|
||
|
||
[#emittersetcollide]
|
||
==== emitterSetCollide
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetCollide(emitter, mode)
|
||
emitterSetCollide(emitter, mode, bounce)
|
||
emitterSetCollide(emitter, mode, bounce, friction)
|
||
emitterSetCollide(emitter, mode, bounce, friction, floor)
|
||
----
|
||
|
||
Su che cosa rimbalzano le particelle. `COLLIDE_NONE` (predefinito) è niente; `COLLIDE_FLOOR` è un piano orizzontale all'altezza `floor`, sotto il quale (Y maggiore in 2D, Y minore in 3D) una particella viene rimessa sul piano e riflessa; `COLLIDE_SCENE` è il mondo fisico, solo per gli emettitori 3D, tramite un raycast per particella lungo il suo moto a ogni fotogramma, con un tetto di `200` raycast per emettitore per fotogramma che riprendono da dove si erano fermati, così un emettitore grande distribuisce il costo su qualche fotogramma. Su un emettitore 2D `COLLIDE_SCENE` viene accettato e non fa nulla. Una modalità diversa da queste tre termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `mode` -- `COLLIDE_NONE`, `COLLIDE_FLOOR` o `COLLIDE_SCENE`.
|
||
* `bounce` -- quanta velocità sopravvive a un urto, da `0` a `1`, predefinito `0.5`; limitato.
|
||
* `friction` -- quanta velocità di scivolamento si perde a ogni urto, da `0` a `1`, predefinito `0.2`; limitato.
|
||
* `floor` -- altezza del piano del pavimento, in unità del mondo per un emettitore 3D o in unità dell'overlay per uno 2D, predefinita `0`. Usato solo da `COLLIDE_FLOOR`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetgravity,emitterSetGravity>>, <<emittersettrail,emitterSetTrail>>, <<emittersetsoftness,emitterSetSoftness>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Le scintille rimbalzano sul pavimento dell'officina (riga 440 dell'overlay) e lasciano strisce.
|
||
sparks = emitterNew()
|
||
emitterSetBlend(sparks, PARTICLE_ADD)
|
||
emitterSetGravity(sparks, 0, 500)
|
||
emitterSetCollide(sparks, COLLIDE_FLOOR, 0.4, 0.2, 440)
|
||
emitterSetTrail(sparks, 6, 3)
|
||
|
||
-- Le braci nella scena rimbalzano su qualunque cosa contenga il mondo fisico.
|
||
emitterSetCollide(embers, COLLIDE_SCENE, 0.3, 0.5)
|
||
----
|
||
|
||
[#emittersetcolor]
|
||
==== emitterSetColor
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetColor(emitter, r, g, b, a, r2, g2, b2, a2)
|
||
----
|
||
|
||
Colore e opacità alla nascita (`r, g, b, a`) e alla morte (`r2, g2, b2, a2`), ciascuno da `0` a `255`, che cambiano in modo uniforme nel corso della vita della particella; i valori fuori intervallo vengono limitati. Predefinito: da bianco e opaco a bianco e trasparente. La tinta moltiplica la texture o il disco incorporato, quindi un'immagine bianca prende esattamente il colore. Una dissolvenza in entrata e in uscita richiede due emettitori o una texture che contenga già quella forma; una ricetta esegue una sola rampa lineare.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetblend,emitterSetBlend>>, <<emittersetlife,emitterSetLife>>, <<emittersetsize,emitterSetSize>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fiamma: giallo acceso alla base, che si spegne in un rosso cupo e sparisce.
|
||
emitterSetColor(flame, 255, 200, 90, 255, 255, 40, 0, 0)
|
||
-- I granelli di polvere compaiono dal nulla e restano sospesi; è la loro vita a farli finire.
|
||
emitterSetColor(dust, 255, 240, 200, 0, 255, 240, 200, 160)
|
||
----
|
||
|
||
[#emittersetspin]
|
||
==== emitterSetSpin
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetSpin(emitter, min, max)
|
||
----
|
||
|
||
Velocità di rotazione di ogni nuova particella in gradi al secondo, estratta a caso tra i due valori; i valori negativi ruotano nell'altro verso, e un intervallo a cavallo di `0` dà entrambi. Ogni particella parte da un angolo casuale, che ruoti o no. Predefinita: nessuna. Il massimo viene alzato al minimo quando è più piccolo. La rotazione si vede su una particella con texture o su una scia; il disco incorporato è rotondo e appare uguale a qualsiasi angolo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersettexture,emitterSetTexture>>, <<emittersetsize,emitterSetSize>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Le foglie ruzzolano; le braci girano appena.
|
||
emitterSetTexture(leaves, leafSprite)
|
||
emitterSetSpin(leaves, -180, 180)
|
||
emitterSetSpin(embers, -20, 20)
|
||
----
|
||
|
||
[#emittersetradius]
|
||
==== emitterSetRadius
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetRadius(emitter, radius)
|
||
----
|
||
|
||
Le nuove particelle nascono in un punto qualsiasi entro questa distanza dall'origine dell'emettitore, distribuite uniformemente su un disco (2D) o una sfera (3D), in unità dell'overlay o del mondo. Predefinito `0`, un punto; i valori negativi diventano `0`. Un raggio riempie un volume, la testa di una torcia o una chiazza di polvere nell'aria, dove un punto darebbe un ago.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetspread,emitterSetSpread>>, <<emittersetposition,emitterSetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Polvere in una sfera d'aria grande quanto una stanza, quasi tutta già lì quando il livello comincia.
|
||
air = nodeNew()
|
||
nodeSetPosition(air, 0, 2, 0)
|
||
dust = emitterNew(air)
|
||
emitterSetRadius(dust, 2.5)
|
||
emitterSetSpread(dust, 180)
|
||
emitterSetSpeed(dust, 0.05, 0.2)
|
||
emitterSetLife(dust, 4, 6)
|
||
emitterStart(dust)
|
||
emitterBurst(dust, 120)
|
||
----
|
||
|
||
[#emittersetlit]
|
||
==== emitterSetLit
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetLit(emitter, lit)
|
||
----
|
||
|
||
Solo 3D: ombreggia ogni particella con le luci e il cielo della scena come se fosse una piccola sfera rivolta alla telecamera, così il fumo prende il colore di un braciere e la polvere scintilla al sole. Disattivata in modo predefinito; le ombre non vengono considerate. Con `PARTICLE_ADD` l'illuminazione viene comunque applicata alla tinta prima che venga aggiunta. Un emettitore 2D memorizza il valore e lo ignora.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetsoftness,emitterSetSoftness>>, <<emittersetblend,emitterSetBlend>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il fumo sopra ogni braciere prende la luce del suo fuoco.
|
||
for _, brazier in ipairs(braziers) do
|
||
local smoke = emitterNew(brazier.top)
|
||
emitterSetRate(smoke, 12)
|
||
emitterSetLife(smoke, 2.5, 4)
|
||
emitterSetColor(smoke, 90, 80, 75, 90, 50, 45, 45, 0)
|
||
emitterSetLit(smoke, true)
|
||
emitterStart(smoke)
|
||
end
|
||
----
|
||
|
||
[#emittersetlocal]
|
||
==== emitterSetLocal
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetLocal(emitter, local)
|
||
----
|
||
|
||
Se le particelle vive seguono l'emettitore. `false` (predefinito) lascia ognuna dove è nata, così una torcia portata attraverso una stanza si lascia dietro il fumo; `true` tiene l'intera nube relativa all'origine dell'emettitore, così il pennacchio di un propulsore si muove con la nave. Cambiarlo sposta le particelle vive da un sistema di riferimento all'altro senza scatti. Su un emettitore 3D l'origine è la posizione nel mondo del nodo; su uno 2D è il punto di `emitterSetPosition`, quindi una nube 2D locale scivola anche con quello. Le scie seguono la stessa regola.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetposition,emitterSetPosition>>, <<emittersetradius,emitterSetRadius>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un'aura magica che resta avvolta attorno alla maga mentre cammina.
|
||
aura = emitterNew(nodeFind("Chest", wizard))
|
||
emitterSetLocal(aura, true)
|
||
emitterSetRadius(aura, 0.6)
|
||
emitterSetSpeed(aura, 0.05, 0.15)
|
||
emitterSetSpread(aura, 180)
|
||
emitterSetBlend(aura, PARTICLE_ADD)
|
||
emitterStart(aura)
|
||
----
|
||
|
||
[#emittersetmax]
|
||
==== emitterSetMax
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetMax(emitter, count)
|
||
----
|
||
|
||
La riserva: quante particelle possono essere vive contemporaneamente, predefinito `1000`. Una riserva piena non ne crea altre, né dal flusso né da una raffica, finché alcune non muoiono. Cambiare il numero ricostruisce la riserva e uccide ogni particella viva, quindi impostalo prima di avviare. Dimensiona la riserva come frequenza per vita più lunga, più la raffica più grande. Un numero inferiore a `1` termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetrate,emitterSetRate>>, <<emitterburst,emitterBurst>>, <<emittergetcount,emitterGetCount>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- 300 gocce al secondo che vivono fino a 1.6 secondi richiedono circa 480; lascia margine.
|
||
fountain = emitterNew(spout)
|
||
emitterSetRate(fountain, 300)
|
||
emitterSetLife(fountain, 1.0, 1.6)
|
||
emitterSetMax(fountain, 600)
|
||
emitterStart(fountain)
|
||
----
|
||
|
||
[#emittersetposition]
|
||
==== emitterSetPosition
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetPosition(emitter, x, y)
|
||
----
|
||
|
||
Solo 2D: dove nascono le nuove particelle, in coordinate dell'overlay; predefinito `(0, 0)`. Impostalo a ogni fotogramma per seguire uno sprite o un corpo fisico 2D. Le particelle vive restano dove sono a meno che `emitterSetLocal` non sia attivo. Un emettitore 3D segue il suo nodo e qui termina lo script; sposta invece il nodo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emitterdraw,emitterDraw>>, <<emittersetlocal,emitterSetLocal>>, <<emittersetradius,emitterSetRadius>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Le scintille volano da dovunque si trovi lo sprite della smerigliatrice in questo fotogramma.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
spriteDraw(grinder, grinderX, grinderY)
|
||
emitterSetPosition(sparks, grinderX + 24, grinderY + 8)
|
||
emitterDraw(sparks)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#emittersettrail]
|
||
==== emitterSetTrail
|
||
|
||
[source,text]
|
||
----
|
||
emitterSetTrail(emitter, length, width)
|
||
----
|
||
|
||
Un nastro dietro ogni particella attraverso le sue ultime `length` posizioni, una registrata per fotogramma (da `2` a `16`; `0` o `1` per nessuno, il predefinito), largo `width` in unità del mondo o dell'overlay, in 2D come in 3D. Il nastro porta la tinta corrente della particella, sfuma nel trasparente verso la coda ed è texturizzato con la colonna centrale dell'immagine della particella, così il bordo sfumato del disco diventa quello del nastro. I valori vengono limitati all'intervallo. Cambiare la lunghezza ricostruisce la memoria delle scie, quindi le particelle vive ricominciano da capo i loro nastri. Scintille, braci, proiettili traccianti, comete.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetcollide,emitterSetCollide>>, <<emittersetsize,emitterSetSize>>, <<emittersetlocal,emitterSetLocal>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Proiettili traccianti: minuscoli, veloci, e tutti scia.
|
||
tracers = emitterNew(gunMuzzle)
|
||
emitterSetBlend(tracers, PARTICLE_ADD)
|
||
emitterSetSpeed(tracers, 40, 50)
|
||
emitterSetSpread(tracers, 2)
|
||
emitterSetSize(tracers, 0.04, 0.04)
|
||
emitterSetLife(tracers, 0.8, 1.0)
|
||
emitterSetTrail(tracers, 8, 0.03)
|
||
----
|
||
|
||
[#emitterstart]
|
||
==== emitterStart
|
||
|
||
[source,text]
|
||
----
|
||
emitterStart(emitter)
|
||
----
|
||
|
||
Avvia il flusso costante alla frequenza impostata; da quel momento l'emettitore crea particelle a ogni fotogramma fino a `emitterStop`. Avviare un emettitore che sta già emettendo il flusso non cambia nulla. Il flusso non ha bisogno di una chiamata di disegno per funzionare; un emettitore 2D che emette il flusso ma non viene disegnato consuma comunque la sua riserva.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emitterstop,emitterStop>>, <<emittersetrate,emitterSetRate>>, <<emitterburst,emitterBurst>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il motore si accende quando il giocatore dà gas e si spegne quando lo toglie.
|
||
function onThrottle(open)
|
||
if open then
|
||
emitterStart(exhaust)
|
||
else
|
||
emitterStop(exhaust)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#emitterstop]
|
||
==== emitterStop
|
||
|
||
[source,text]
|
||
----
|
||
emitterStop(emitter)
|
||
----
|
||
|
||
Termina il flusso; le particelle vive finiscono la loro vita e qualsiasi frazione di particella ancora dovuta viene scartata. `emitterBurst` funziona ancora su un emettitore fermo, e `emitterIsActive` resta `true` finché non muore l'ultima particella. Usa `emitterClear` per rimuovere anche le particelle all'istante.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emitterclear,emitterClear>>, <<emitterstart,emitterStart>>, <<emitterisactive,emitterIsActive>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La torcia si spegne: ferma la fiamma, lascia che l'ultimo fumo si disperda.
|
||
function onTorchDoused()
|
||
emitterStop(flame)
|
||
emitterStop(smoke)
|
||
soundPlay(hiss)
|
||
end
|
||
----
|
||
|
||
[#emitterburst]
|
||
==== emitterBurst
|
||
|
||
[source,text]
|
||
----
|
||
emitterBurst(emitter, count)
|
||
----
|
||
|
||
Crea `count` particelle in una volta dalla ricetta corrente, con o senza flusso: un'esplosione, uno spruzzo, uno sbuffo. Ne vengono create solo quante ne entrano nella riserva; lo spazio lo stabilisce `emitterSetMax`. Un emettitore 2D le emette nel suo punto di `emitterSetPosition`, uno 3D sul suo nodo, sparse entro `emitterSetRadius`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emitterstart,emitterStart>>, <<emittersetmax,emitterSetMax>>, <<emittersetradius,emitterSetRadius>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fuochi d'artificio: una bomba esplode da qualche parte sopra il disco più o meno ogni secondo.
|
||
function onOverlayUpdate()
|
||
if frames % 60 == 0 then
|
||
emitterSetPosition(fireworks, 120 + (frames * 7) % (width - 240), 80 + (frames * 13) % 160)
|
||
emitterBurst(fireworks, 260)
|
||
end
|
||
overlayClear()
|
||
emitterDraw(fireworks)
|
||
frames = frames + 1
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#emitterclear]
|
||
==== emitterClear
|
||
|
||
[source,text]
|
||
----
|
||
emitterClear(emitter)
|
||
----
|
||
|
||
Uccide subito ogni particella viva e dimentica qualsiasi frazione di particella ancora dovuta. Il flusso non viene fermato: un emettitore avviato continua a creare nuove particelle dal fotogramma successivo, quindi chiama anche `emitterStop` per svuotarlo definitivamente. Usala quando una scena cambia e le vecchie scintille non devono restare.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emitterstop,emitterStop>>, <<emitterdelete,emitterDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Si ricomincia il livello: gli emettitori restano, le vecchie particelle se ne vanno.
|
||
function resetLevel()
|
||
for _, emitter in ipairs(levelEmitters) do
|
||
emitterStop(emitter)
|
||
emitterClear(emitter)
|
||
end
|
||
placePlayer()
|
||
end
|
||
----
|
||
|
||
[#emitterdraw]
|
||
==== emitterDraw
|
||
|
||
[source,text]
|
||
----
|
||
emitterDraw(emitter)
|
||
----
|
||
|
||
Solo 2D, da `onOverlayUpdate`: mette in coda le particelle dell'emettitore da disegnare in questo fotogramma, sopra o sotto l'overlay secondo `emitterSetLayer`, in coordinate dell'overlay mappate sulla finestra. Gli emettitori sullo stesso livello disegnano nell'ordine in cui sono stati messi in coda. La coda si svuota a ogni fotogramma, quindi la chiamata va fatta in ogni fotogramma in cui le particelle devono vedersi; non disegnare un emettitore non lo ferma. In un fotogramma disegnano fino a `64` emettitori per livello. Un emettitore 3D si disegna da sé nella scena e qui termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetlayer,emitterSetLayer>>, <<emittersetposition,emitterSetPosition>>, <<onoverlayupdate,onOverlayUpdate>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
emitterSetPosition(smoke, chimneyX, chimneyY)
|
||
emitterDraw(smoke)
|
||
emitterDraw(sparks)
|
||
fontPrint(20, 20, "Score " .. score)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#emittergetcount]
|
||
==== emitterGetCount
|
||
|
||
[source,text]
|
||
----
|
||
count = emitterGetCount(emitter)
|
||
----
|
||
|
||
Quante particelle dell'emettitore sono vive in questo fotogramma, da `0` alla dimensione della riserva. Utile per regolare una riserva, per un budget a schermo o per sapere quando una raffica si è esaurita.
|
||
|
||
*Restituisce:* Il numero di particelle vive, un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emittersetmax,emitterSetMax>>, <<emitterisactive,emitterIsActive>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Overlay di sviluppo: quanto di ogni riserva è in uso.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
fontPrint(20, 20, string.format("live %d / %d", emitterGetCount(fireworks), fireworksPool))
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#emitterisactive]
|
||
==== emitterIsActive
|
||
|
||
[source,text]
|
||
----
|
||
active = emitterIsActive(emitter)
|
||
----
|
||
|
||
Se l'emettitore sta emettendo il flusso o ha ancora particelle vive: `true` da `emitterStart` o `emitterBurst` finché il flusso non è fermato e l'ultima particella non è morta, `false` quando l'esplosione è finita. Un emettitore che emette il flusso e la cui riserva risulta vuota è comunque attivo.
|
||
|
||
*Restituisce:* Booleano.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<emitterstop,emitterStop>>, <<emittergetcount,emitterGetCount>>, <<emitterdelete,emitterDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Le esplosioni singole vivono in un elenco e vengono liberate quando si esauriscono.
|
||
function onOverlayUpdate()
|
||
for i = #explosions, 1, -1 do
|
||
if not emitterIsActive(explosions[i]) then
|
||
emitterDelete(explosions[i])
|
||
table.remove(explosions, i)
|
||
end
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#font]
|
||
=== Font
|
||
|
||
Singe ha due renderer di testo. Il font da console incorporato viene disegnato da <<overlayprint,overlayPrint>> e non ha bisogno di risorse; la famiglia `font*` carica file TrueType a una dimensione in punti fissa e li rende o direttamente sull'overlay con `fontPrint` o in uno sprite riutilizzabile con `fontToSprite`. Gli handle dei font sono interi restituiti da `fontLoad`, e il motore tiene un unico font selezionato a livello globale, che `fontLoad` imposta sul font appena caricato e `fontSelect` cambia. Il testo viene reso nel colore di primo piano corrente con la qualità scelta da `fontQuality`, e le posizioni del testo sono coordinate dell'overlay. Rilascia i font con `fontUnload`; `Singe/FreeSansBold.ttf` viene distribuito con il motore ed è sempre disponibile.
|
||
|
||
[#fontload]
|
||
==== fontLoad
|
||
|
||
[source,text]
|
||
----
|
||
result = fontLoad(filename, pointSize)
|
||
----
|
||
|
||
Apre un font TrueType a una dimensione in punti e lo rende il font selezionato. La dimensione è fissa per tutta la vita dell'handle, quindi carica lo stesso file una volta per ogni dimensione che ti serve. Il nome passa per il file system virtuale come qualsiasi altra risorsa; anteponi `DIR` per i file distribuiti con il tuo gioco, o usa `Singe/FreeSansBold.ttf` per il carattere incluso. Un file che non si riesce ad aprire termina lo script con `Unable to open` e il nome.
|
||
|
||
*Parametri:*
|
||
|
||
* `filename` -- percorso di un file `.ttf`.
|
||
* `pointSize` -- dimensione in punti, intera.
|
||
|
||
*Restituisce:* handle intero del font.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<fontselect,fontSelect>>, <<fontunload,fontUnload>>, <<fontquality,fontQuality>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Due dimensioni del carattere incluso: grande per il titolo, piccola per l'HUD.
|
||
fontQuality(FONT_QUALITY_BLENDED)
|
||
titleFont = fontLoad("Singe/FreeSansBold.ttf", 32)
|
||
hudFont = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||
|
||
fontSelect(titleFont)
|
||
titleSprite = fontToSprite("GAME OVER")
|
||
fontSelect(hudFont)
|
||
----
|
||
|
||
[#fontprint]
|
||
==== fontPrint
|
||
|
||
[source,text]
|
||
----
|
||
fontPrint(x, y, text)
|
||
----
|
||
|
||
Rende una stringa con il font selezionato, la qualità corrente e il colore di primo piano corrente (più il colore di sfondo per `FONT_QUALITY_SHADED`), poi la copia sull'overlay con l'angolo in alto a sinistra in `(x, y)`. Chiamala da `onOverlayUpdate`, a ogni fotogramma se serve: il motore conserva ogni stringa che rende, indicizzata per font, qualità, colori e testo, e una chiamata successiva che chiede la stessa stringa si limita a copiarla. Le stringhe non stampate per circa un secondo vengono dimenticate, e le stringhe di un font se ne vanno quando lo si scarica, quindi il testo che cambia non costa più di quanto sia sempre costato e quello che non cambia è quasi gratis. Un'intera pagina di punteggi record ristampata a ogni fotogramma va benissimo. Senza un font selezionato lo script termina con un errore.
|
||
|
||
*Parametri:*
|
||
|
||
* `x`, `y` -- angolo in alto a sinistra in coordinate dell'overlay; le frazioni vengono troncate.
|
||
* `text` -- la stringa da rendere.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<fonttosprite,fontToSprite>>, <<fontselect,fontSelect>>, <<colorforeground,colorForeground>>, <<overlayprint,overlayPrint>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Lettura dal vivo della calibrazione dell'audio; il testo cambia a ogni fotogramma, quindi rendilo direttamente.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
colorForeground(255, 255, 255, 255)
|
||
fontSelect(hudFont)
|
||
fontPrint(MARGIN_X, MARGIN_Y, "AUDIO DELAY CALIBRATION")
|
||
fontPrint(MARGIN_X, MARGIN_Y + 30, "Delay: " .. calibrationDelay .. " ms")
|
||
fontPrint(MARGIN_X, MARGIN_Y + 60, "Left / Right: 10 ms Up / Down: 1 ms")
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#fontquality]
|
||
==== fontQuality
|
||
|
||
[source,text]
|
||
----
|
||
fontQuality(mode)
|
||
----
|
||
|
||
Sceglie come `fontPrint` e `fontToSprite` rasterizzano il testo da quel momento in poi. L'impostazione è globale, non per font, e parte da `FONT_QUALITY_SOLID`. Gli sprite già creati da `fontToSprite` mantengono la qualità con cui sono stati resi. Un valore diverso dalle tre costanti termina lo script con un errore.
|
||
|
||
*Parametri:*
|
||
|
||
* `mode` -- `FONT_QUALITY_SOLID` (`1`): il più veloce, bordi a un bit, niente anti-aliasing. `FONT_QUALITY_SHADED` (`2`): glifi con anti-aliasing su un rettangolo pieno del colore di sfondo. `FONT_QUALITY_BLENDED` (`3`): glifi con anti-aliasing e alfa per pixel che si fondono su qualsiasi cosa; la scelta abituale.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>, <<colorbackground,colorBackground>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Testo con fusione sopra il video, ma un'etichetta su riquadro pieno per la lettura di debug.
|
||
fontQuality(FONT_QUALITY_BLENDED)
|
||
titleSprite = fontToSprite("HIGH SCORES")
|
||
|
||
colorBackground(0, 0, 0, 255)
|
||
fontQuality(FONT_QUALITY_SHADED)
|
||
debugLabel = fontToSprite("FRAME")
|
||
fontQuality(FONT_QUALITY_BLENDED)
|
||
----
|
||
|
||
[#fontselect]
|
||
==== fontSelect
|
||
|
||
[source,text]
|
||
----
|
||
fontSelect(id)
|
||
----
|
||
|
||
Rende un font caricato quello usato da `fontPrint` e `fontToSprite`. La selezione resta fino al successivo `fontSelect` o `fontLoad`, dato che anche il caricamento seleziona. Si può cambiare tutte le volte che serve, anche più volte dentro un solo `onOverlayUpdate`. Un handle sconosciuto o scaricato (o nessun numero) lascia selezionato il font corrente e stampa un avviso, dato che Hypseus Singe lo lascia passare.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<fontload,fontLoad>>, <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Titolo nel carattere grande, punteggio in quello piccolo, a ogni fotogramma.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
fontSelect(titleFont)
|
||
fontPrint(TITLE_X, 40, "HIGH SCORES")
|
||
fontSelect(hudFont)
|
||
fontPrint(8, 8, "Score: " .. score)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#fonttosprite]
|
||
==== fontToSprite
|
||
|
||
[source,text]
|
||
----
|
||
result = fontToSprite(text)
|
||
----
|
||
|
||
Rende una stringa con il font, la qualità e i colori selezionati in un nuovo sprite e ne restituisce l'handle. Da quel momento lo sprite è indipendente dal font, quindi sopravvive a `fontUnload`, e funziona con ogni chiamata `sprite*`: disegnalo, misuralo, scalalo o ruotalo, e liberalo con `spriteUnload`. Per una stringa che cambia, rendi un nuovo sprite e scarica quello vecchio. Senza un font selezionato lo script termina con un errore.
|
||
|
||
NOTE: Rendere il testo in uno sprite per le prestazioni è deprecato. Ora `fontPrint` conserva ciò che rende, quindi stampare la stessa stringa a ogni fotogramma costa più o meno quanto disegnare uno sprite ricavato da essa, senza lo sprite da gestire. Usa `fontToSprite` quando il testo deve essere uno sprite: per scalarlo, ruotarlo, capovolgerlo o misurarlo con `spriteGetWidth` e `spriteGetHeight`.
|
||
|
||
*Restituisce:* handle intero dello sprite.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<fontprint,fontPrint>>, <<spritedraw,spriteDraw>>, <<spritegetwidth,spriteGetWidth>>, <<spriteunload,spriteUnload>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un titolo che entra ruotando e crescendo: testo che deve essere uno sprite, per essere girato e scalato.
|
||
titleSprite = fontToSprite("GAME OVER")
|
||
titleFrames = 0
|
||
|
||
function onOverlayUpdate()
|
||
titleFrames = titleFrames + 1
|
||
spriteRotate(titleSprite, (titleFrames * 12) % 360)
|
||
spriteScale(titleSprite, math.min(titleFrames / 30, 1))
|
||
spriteDraw(titleSprite, overlayGetWidth() / 2, overlayGetHeight() / 2, true)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#fontunload]
|
||
==== fontUnload
|
||
|
||
[source,text]
|
||
----
|
||
fontUnload(id)
|
||
----
|
||
|
||
Chiude un font e invalida il suo handle. Gli sprite creati da esso con `fontToSprite` non ne risentono. Se il font scaricato era quello selezionato, dopo non c'è nessun font selezionato e il successivo `fontPrint` o `fontToSprite` termina lo script con un errore finché `fontSelect` o `fontLoad` non ne sceglie un altro.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<fontload,fontLoad>>, <<fontselect,fontSelect>>, <<onshutdown,onShutdown>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Rilascia i font e gli sprite di testo che hanno prodotto.
|
||
function onShutdown()
|
||
spriteUnload(titleSprite)
|
||
for _, line in ipairs(descriptionLines) do
|
||
spriteUnload(line)
|
||
end
|
||
fontUnload(titleFont)
|
||
fontUnload(hudFont)
|
||
end
|
||
----
|
||
|
||
[#guiref]
|
||
=== Gui
|
||
|
||
Una GUI è un albero di documenti RmlUi di dimensione fissa, reso nella propria texture a ogni fotogramma: `guiNew` ne crea una e restituisce un handle intero, `guiLoad` ci mette un documento RML e restituisce un handle di documento, `guiDraw` la compone sopra l'overlay e `materialSetGui` la mostra su un materiale nella scena. I documenti sono RML simile all'HTML, con lo stile dato da RCSS simile al CSS, e vengono caricati tramite il file system virtuale come gli sprite; `Singe/gui.rcss` è il tema distribuito e `Singe/FreeSansBold.ttf` il carattere di riserva. Le funzioni di aiuto per id `guiGetValue`, `guiSetValue` e `guiSetHandler` raggiungono gli elementi senza il modello a oggetti; tutto il resto è l'API Lua di RmlUi sulla variabile globale `rmlui`, dove una GUI è il contesto chiamato `gui` seguito dal suo handle. Possono esistere fino a sedici GUI contemporaneamente, ciascuna con trentadue documenti. Una GUI ha bisogno del dispositivo GPU, quindi `guiNew` termina lo script su una macchina che non ce l'ha; un handle di GUI non valido solleva `No GUI N`, e un handle di documento non valido `No document N in GUI G`. Vedi <<gui,GUI>> in Sviluppo di giochi.
|
||
|
||
[#guiclose]
|
||
==== guiClose
|
||
|
||
[source,text]
|
||
----
|
||
guiClose(gui, document)
|
||
----
|
||
|
||
Scarica il documento dalla GUI, eliminando i suoi elementi, i suoi handler e i suoi script. Dopo, l'handle del documento non è più valido. Un documento chiuso dal proprio script tramite l'API di RmlUi (`document:Close()`) viene scaricato allo stesso modo. Solleva `No document N in GUI G` per un handle non valido.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guiload,guiLoad>>, <<guihide,guiHide>>, <<guidelete,guiDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La pagina di inserimento del punteggio record serve una volta sola; chiudila dopo che il nome è stato inserito.
|
||
guiSetHandler(gui, entryPage, "enter", "click", function(g, d)
|
||
scores[#scores + 1] = { name = guiGetValue(g, d, "name"), score = score }
|
||
guiClose(g, d)
|
||
entryPage = nil
|
||
end)
|
||
----
|
||
|
||
[#guidelete]
|
||
==== guiDelete
|
||
|
||
[source,text]
|
||
----
|
||
guiDelete(gui)
|
||
----
|
||
|
||
Libera la GUI, ogni documento che contiene e la sua texture, e ferma l'aggiornamento e il rendering per fotogramma che costava. I materiali che la mostrano tramite `materialSetGui` tornano alla propria texture. Dopo, l'handle non è più valido e termina lo script se viene usato di nuovo; dato che una GUI continua a essere resa che venga disegnata o no, elimina quelle con cui un gioco ha finito invece di nasconderle.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guinew,guiNew>>, <<guiclose,guiClose>>, <<materialsetgui,materialSetGui>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La pagina delle opzioni vive solo finché il giocatore ci sta dentro.
|
||
function closeOptions()
|
||
guiDelete(optionsGui)
|
||
optionsGui = nil
|
||
discPlay()
|
||
end
|
||
----
|
||
|
||
[#guidraw]
|
||
==== guiDraw
|
||
|
||
[source,text]
|
||
----
|
||
guiDraw(gui)
|
||
guiDraw(gui, x, y)
|
||
guiDraw(gui, x, y, width, height)
|
||
----
|
||
|
||
Compone la texture della GUI sopra l'overlay per questo fotogramma, in coordinate dell'overlay, quindi rispetta il bordo Sinden e la scala di overscan come tutto il resto sull'overlay: coprendo l'intero overlay con un solo argomento, alla propria dimensione con un angolo in `x, y`, oppure scalata nel rettangolo `width` per `height`. Il posto della chiamata nel disegno dello script decide che cosa sta sotto la GUI e che cosa sopra: tutto ciò che viene disegnato sull'overlay prima di essa resta sotto, e tutto ciò che viene disegnato dopo -- fino al successivo `guiDraw`, o al turno successivo dello script -- va su un livello tutto suo composto sopra quella GUI. Un puntatore disegnato dopo l'HUD viene disegnato sopra l'HUD; un secondo documento disegnato dopo ancora sta sopra il puntatore, con il proprio livello al di sopra. Fino a sedici livelli del genere per fotogramma; una chiamata oltre quel numero disegna la sua GUI senza niente sopra. `overlayClear` pulisce il livello su cui si sta disegnando, e `overlaySetMonochrome` rende grigio solo l'overlay di base. La chiamata dura un fotogramma; falla da `onOverlayUpdate` (o da `singeMain`) in ogni fotogramma in cui la GUI deve essere visibile. Le GUI si disegnano sopra l'overlay nell'ordine di chiamata (l'ultima in cima), sotto le particelle `PARTICLE_OVER` e l'indicatore di pausa, e la GUI disegnata per ultima sotto il puntatore è quella che riceve il mouse. Vengono onorate fino a sedici chiamate per fotogramma; quelle in più vengono ignorate. Qualsiasi altro numero di argomenti solleva `Expected 1, 3 or 5 arguments`.
|
||
|
||
*Parametri:*
|
||
|
||
* `x`, `y` -- l'angolo in alto a sinistra, in coordinate dell'overlay.
|
||
* `width`, `height` -- la dimensione a cui disegnare, in pixel dell'overlay; la dimensione propria della GUI quando vengono omessi.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guinew,guiNew>>, <<materialsetgui,materialSetGui>>, <<overlaysetresolution,overlaySetResolution>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- L'HUD sta nell'angolo in basso a destra, largo un terzo dell'overlay, e il mirino della pistola
|
||
-- viene disegnato dopo, così il pannello non lo nasconde mai.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
guiSetValue(hud, hudPage, "lives", tostring(lives))
|
||
guiDraw(hud, overlayGetWidth() * 0.66, overlayGetHeight() * 0.8, overlayGetWidth() * 0.33, overlayGetHeight() * 0.2)
|
||
local x, y = mouseGetPosition(0)
|
||
spriteDraw(sight, x, y, true)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#guidrawscreen]
|
||
==== guiDrawScreen
|
||
|
||
[source,text]
|
||
----
|
||
guiDrawScreen(gui)
|
||
guiDrawScreen(gui, x, y)
|
||
guiDrawScreen(gui, x, y, width, height)
|
||
----
|
||
|
||
Uguale a <<guidraw,`guiDraw`>>, salvo che il rettangolo viene misurato sull'intera finestra invece che sull'immagine. Le coordinate sono sempre coordinate dell'overlay -- `0, 0` è l'angolo in alto a sinistra della finestra e `overlayGetWidth(), overlayGetHeight()` quello in basso a destra -- così una GUI può stare sulla grafica del bezel accanto all'immagine, sopra o sotto di essa. Senza bezel, e senza `--scalefactor`, `--shiftx` o `--shifty`, l'immagine è l'intera finestra e questa è esattamente `guiDraw`.
|
||
|
||
Le GUI nello spazio dello schermo vengono disegnate dopo tutto il resto, grafica del bezel compresa, e condividono con `guiDraw` i sedici disegni per fotogramma. Un clic le trova allo stesso modo: il puntatore viene portato nello spazio dello schermo prima di essere verificato, quindi una GUI nello spazio dello schermo che accetta input riceve il mouse ovunque sia stata disegnata.
|
||
|
||
*Parametri:*
|
||
|
||
* `x`, `y` -- l'angolo in alto a sinistra, in coordinate dell'overlay misurate sulla finestra.
|
||
* `width`, `height` -- la dimensione a cui disegnare; la dimensione propria della GUI quando vengono omessi.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guidraw,guiDraw>>, xref:Manual.adoc#bezels[Bezel], <<scorebezelenable,scoreBezelEnable>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una mappa sul montante sinistro della grafica del cabinato, fuori dall'immagine.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
guiDrawScreen(mapGui, 8, 44, 38, 114)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#guigetheight]
|
||
==== guiGetHeight
|
||
|
||
[source,text]
|
||
----
|
||
height = guiGetHeight(gui)
|
||
----
|
||
|
||
L'altezza della GUI nei suoi pixel, come data a `guiNew`. Termina lo script per un handle non valido.
|
||
|
||
*Restituisce:* Un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guigetwidth,guiGetWidth>>, <<guinew,guiNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Centra una piccola finestra di dialogo sull'overlay.
|
||
local x = (overlayGetWidth() - guiGetWidth(dialog)) / 2
|
||
local y = (overlayGetHeight() - guiGetHeight(dialog)) / 2
|
||
guiDraw(dialog, x, y)
|
||
----
|
||
|
||
[#guigetstats]
|
||
==== guiGetStats
|
||
|
||
[source,text]
|
||
----
|
||
stats = guiGetStats()
|
||
----
|
||
|
||
Quanto è costato il rendering di tutte le GUI nell'ultimo fotogramma completato, per mettere a punto i documenti che usano filtri, maschere e ombre (ognuno dei quali disegna attraverso un livello grande quanto la GUI). I contatori coprono tutte le GUI insieme. Sono tutti zero in un fotogramma in cui l'immagine di nessuna GUI è cambiata, dato che non è stato ridisegnato nulla (vedi <<guirefresh,guiRefresh>>).
|
||
|
||
*Restituisce:* Una tabella con i campi `drawCalls` (disegni indicizzati, comprese le scritture della maschera, le composizioni e le passate dei filtri), `layerPushes` (livelli aperti per filtri, immagini maschera e ombre dei riquadri), `filterPasses` (passate a dimensione piena e copie dentro le catene di filtri; una sfocatura ne costa diverse), `maskWrites` (disegni nella maschera di ritaglio dello stencil), `layerBytes` (byte delle texture dei livelli attualmente allocate; ognuna è grande quanto la sua GUI, a quattro byte per pixel) e `passes` (passate di rendering iniziate; ogni cambio di livello ne inizia una).
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guinew,guiNew>>, <<gui,GUI>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Guarda quanto costa una schermata piena di effetti.
|
||
local s = guiGetStats()
|
||
debugPrint(string.format("gui draws %d layers %d filters %d passes %d, %d KB of layers", s.drawCalls, s.layerPushes, s.filterPasses, s.passes, s.layerBytes // 1024))
|
||
----
|
||
|
||
[#guigetvalue]
|
||
==== guiGetValue
|
||
|
||
[source,text]
|
||
----
|
||
value = guiGetValue(gui, document, id)
|
||
----
|
||
|
||
Il valore dell'elemento con quell’`id` nel documento, come stringa: il valore di un controllo di un modulo (il testo di un campo di testo, il `value` di un'opzione di `select`, un cursore nella formattazione propria di RmlUi come `"85.000000"`, l'attributo `value` di una casella di controllo o di un pulsante di opzione, `"on"` quando non ce l'ha, che sia selezionato o no; il suo stato è il suo attributo `checked`, letto tramite l'API di RmlUi), oppure, per qualsiasi altro elemento, il suo RML interno. Restituisce `nil` quando nessun elemento ha quell'id, così una pagina si può sondare senza terminare lo script; un handle di GUI o di documento non valido lo termina. I valori più lunghi di 4095 caratteri vengono troncati.
|
||
|
||
*Restituisce:* Una stringa, o `nil`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guisetvalue,guiSetValue>>, <<guisethandler,guiSetHandler>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Leggi l'intero modulo quando viene premuto Start.
|
||
guiSetHandler(gui, doc, "go", "click", function(g, d)
|
||
player.name = guiGetValue(g, d, "name")
|
||
player.hard = guiGetValue(g, d, "difficulty") == "hard"
|
||
player.volume = math.floor(tonumber(guiGetValue(g, d, "volume")))
|
||
end)
|
||
----
|
||
|
||
[#guigetwidth]
|
||
==== guiGetWidth
|
||
|
||
[source,text]
|
||
----
|
||
width = guiGetWidth(gui)
|
||
----
|
||
|
||
La larghezza della GUI nei suoi pixel, come data a `guiNew`. Termina lo script per un handle non valido.
|
||
|
||
*Restituisce:* Un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guigetheight,guiGetHeight>>, <<guinew,guiNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il punto sondato è nella metà destra della pagina del monitor?
|
||
local g, px, py = sceneProbeGui(mouseX, mouseY)
|
||
if g ~= nil and px > guiGetWidth(g) / 2 then
|
||
showRightTooltip()
|
||
end
|
||
----
|
||
|
||
[#guihide]
|
||
==== guiHide
|
||
|
||
[source,text]
|
||
----
|
||
guiHide(gui, document)
|
||
----
|
||
|
||
Nasconde il documento: non viene disegnato e non riceve input, ma conserva i suoi elementi e i suoi valori e `guiShow` lo riporta com'era. Solleva `No document N in GUI G` per un handle non valido.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guishow,guiShow>>, <<guiclose,guiClose>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Due pagine in una GUI: la pagina delle opzioni si nasconde mentre quella dei controlli si mostra.
|
||
guiSetHandler(gui, options, "controls", "click", function()
|
||
guiHide(gui, options)
|
||
guiShow(gui, controls)
|
||
end)
|
||
----
|
||
|
||
[#guiload]
|
||
==== guiLoad
|
||
|
||
[source,text]
|
||
----
|
||
document = guiLoad(gui, filename)
|
||
----
|
||
|
||
Carica un documento RML nella GUI, lo mostra e restituisce un handle intero di documento per le altre chiamate. Il nome passa per il file system virtuale come quello di uno sprite, risolto relativamente alla cartella da cui è stato avviato Singe, quindi anteponi `DIR` per i file distribuiti con il tuo gioco; i fogli di stile, le immagini e i font nominati dentro il documento vengono risolti relativamente al file che li nomina e poi tramite la stessa ricerca, così un gioco impacchettato li trova. I blocchi `<script>` e gli attributi `on*` del documento girano nello stato Lua del gioco, e il documento è raggiungibile come `rmlui.contexts["guiN"].documents["<body id>"]`. Un documento che non si riesce a caricare termina lo script con il nome e il motivo di RmlUi (un file mancante, un errore di analisi); gli avvisi di RmlUi stesso sul markup e sullo stile vanno alla console. Ogni GUI contiene fino a trentadue documenti.
|
||
|
||
*Restituisce:* L'handle del documento, un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guinew,guiNew>>, <<guishow,guiShow>>, <<guihide,guiHide>>, <<guiclose,guiClose>>, <<guiloadfont,guiLoadFont>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La pagina del titolo e l'HUD condividono una GUI grande quanto l'overlay.
|
||
gui = guiNew(overlayGetWidth(), overlayGetHeight())
|
||
title = guiLoad(gui, DIR .. "gui/title.rml")
|
||
hud = guiLoad(gui, DIR .. "gui/hud.rml")
|
||
guiHide(gui, hud)
|
||
----
|
||
|
||
[#guiloadfont]
|
||
==== guiLoadFont
|
||
|
||
[source,text]
|
||
----
|
||
guiLoadFont(filename)
|
||
guiLoadFont(filename, fallback)
|
||
----
|
||
|
||
Carica un carattere TrueType o OpenType per ogni GUI, così i documenti possono nominarlo in `font-family` con il nome di famiglia contenuto nel file (con `font-weight` e `font-style` che scelgono tra i caratteri di una stessa famiglia). Con `fallback` a `true` il carattere fornisce anche ogni glifo che nessun altro carattere ha, che è ciò che fa fino ad allora il `Singe/FreeSansBold.ttf` del motore (famiglia `FreeSans`, peso grassetto, il carattere che usa il tema distribuito). Carica i font prima dei documenti che li usano. Ogni carattere costa un atlante dei glifi per ogni dimensione a cui viene usato. Un file che non si riesce ad aprire termina lo script con `Unable to load` e il nome.
|
||
|
||
*Parametri:*
|
||
|
||
* `filename` -- il file del font, tramite il file system virtuale come `fontLoad`.
|
||
* `fallback` -- `true` per farne il carattere di riserva; `false` (il predefinito) per un carattere normale.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guiload,guiLoad>>, <<fontload,fontLoad>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il carattere da titolo del gioco per le intestazioni; il FreeSans distribuito resta per il corpo del testo.
|
||
guiLoadFont(DIR .. "fonts/Orbitron-Bold.ttf")
|
||
gui = guiNew(overlayGetWidth(), overlayGetHeight())
|
||
menu = guiLoad(gui, DIR .. "gui/menu.rml")
|
||
----
|
||
|
||
[#guinew]
|
||
==== guiNew
|
||
|
||
[source,text]
|
||
----
|
||
gui = guiNew(width, height)
|
||
----
|
||
|
||
Crea una GUI di `width` per `height` pixel, resa nella propria texture a ogni fotogramma che venga disegnata o no, e ne restituisce l'handle. La dimensione è lo spazio delle coordinate del documento: il `100%` del body è l'intera texture, e un `dp` è uno dei suoi pixel. Una GUI grande quanto l'overlay va bene per una pagina a schermo intero; un elemento dell'HUD o un monitor nella scena ne vogliono una più piccola, e un Raspberry Pi vuole tutto modesto. Una nuova GUI si limita a mostrare finché `guiSetInput` non attiva l'input. Solleva `The size must be positive` per una dimensione zero o negativa, e `No GUI available` quando ne sono già in uso sedici o la macchina non ha un dispositivo GPU (una GUI ha bisogno dello stesso dispositivo della scena 3D).
|
||
|
||
*Parametri:*
|
||
|
||
* `width` -- la larghezza della texture in pixel.
|
||
* `height` -- l'altezza della texture in pixel.
|
||
|
||
*Restituisce:* L'handle della GUI, un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guiload,guiLoad>>, <<guidraw,guiDraw>>, <<guidelete,guiDelete>>, <<materialsetgui,materialSetGui>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una pagina grande quanto l'overlay, disegnata sopra il disco.
|
||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||
gui = guiNew(overlayGetWidth(), overlayGetHeight())
|
||
doc = guiLoad(gui, DIR .. "gui/attract.rml")
|
||
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
guiDraw(gui)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#guirefresh]
|
||
==== guiRefresh
|
||
|
||
[source,text]
|
||
----
|
||
guiRefresh(gui)
|
||
----
|
||
|
||
Disegna di nuovo la GUI al fotogramma successivo anche se niente sembra esserne cambiato. Una GUI viene ridisegnata solo quando la sua immagine cambia: quando il documento di un fotogramma disegna esattamente ciò che ha disegnato il fotogramma precedente, e nel frattempo non è stato creato nulla di nuovo, la texture contiene già quell'immagine e viene tenuta, il che risparmia alla GPU gran parte del costo di una GUI nei fotogrammi in cui un HUD o un menu restano fermi. Le modifiche fatte tramite RML, RCSS, l'interfaccia Lua di RmlUi o le chiamate `gui*` vengono tutte rilevate. Questa chiamata è la via d'uscita nel caso una ne sfugga -- una GUI che smette di aggiornarsi quando dovrebbe -- e un gioco non dovrebbe averne bisogno; se ne ha bisogno, vale la pena segnalarlo. Una GUI che non esiste termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guidraw,guiDraw>>, <<guigetstats,guiGetStats>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un rimedio temporaneo mentre si segnala una modifica sfuggita: ridisegna l'HUD a ogni fotogramma.
|
||
function onOverlayUpdate()
|
||
guiRefresh(hud)
|
||
guiDraw(hud)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#guisethandler]
|
||
==== guiSetHandler
|
||
|
||
[source,text]
|
||
----
|
||
guiSetHandler(gui, document, id, event, function)
|
||
guiSetHandler(gui, document, id, event, nil)
|
||
----
|
||
|
||
Chiama `function(gui, document, id, event, value)` ogni volta che l'elemento con quell’`id` genera l'evento indicato: `"click"` per i pulsanti e le righe, `"change"` per i controlli dei moduli, `"submit"` per un modulo, o qualsiasi altro nome di evento di RmlUi (`"focus"`, `"mouseover"`, `"keydown"`, ...). `value` è il valore dell'elemento in quel momento come stringa, come lo restituirebbe `guiGetValue` (un cursore arriva come lo formatta RmlUi, `"85.000000"`, quindi passalo per `tonumber`). Si tiene una funzione per elemento ed evento; impostarne un'altra la sostituisce, e `nil` la rimuove. L'handler appartiene all’`id`, non a un elemento: quando l'elemento viene sostituito -- un elenco ricostruito tramite `inner_rml` crea elementi nuovi -- imposta di nuovo l'handler dopo la ricostruzione e si aggancia all'elemento che ora porta quell'id. Gli handler vengono eseguiti man mano che gli eventi accadono, tra le callback del gioco stesso, e un errore in uno di essi termina il gioco come un errore in qualsiasi callback. Gli id sono lunghi al massimo 63 caratteri. Solleva `No element "id" in document N of GUI G` quando l'elemento non esiste, e `Argument 5 must be a function or nil` per qualsiasi altra cosa in quella posizione.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- l'attributo `id` dell'elemento.
|
||
* `event` -- il nome dell'evento di RmlUi.
|
||
* `function` -- la funzione Lua da chiamare, o `nil` per smettere di chiamarne una.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guigetvalue,guiGetValue>>, <<guisetvalue,guiSetValue>>, <<guisetinput,guiSetInput>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un cursore del volume ha effetto mentre si muove; Start legge il resto del modulo.
|
||
guiSetHandler(gui, doc, "volume", "change", function(g, d, id, event, value)
|
||
soundSetVolume(math.floor(tonumber(value)))
|
||
end)
|
||
|
||
guiSetHandler(gui, doc, "go", "click", function(g, d)
|
||
startGame(guiGetValue(g, d, "name"), guiGetValue(g, d, "difficulty"))
|
||
end)
|
||
----
|
||
|
||
[#guisetinput]
|
||
==== guiSetInput
|
||
|
||
[source,text]
|
||
----
|
||
guiSetInput(gui, enabled)
|
||
----
|
||
|
||
Se il mouse e la pistola ottica, i tasti, il testo digitato e gli interruttori del framework da un controller o da un mouse arrivano alla GUI. Una nuova GUI si limita a mostrare, il che va bene per un HUD o un'insegna; attiva l'input per un menu o un modulo. Finché è attivo, il puntatore raggiunge la GUI ovunque sia disegnata piatta (l'ultima disegnata in cima) o mostrata su una superficie nella scena, ogni tasto va a essa tranne `Tab`, che la GUI riceve solo mentre si sta scrivendo in un campo di testo (RmlUi lo usa per spostare il focus e non lo restituisce mai, e un gioco i cui tasti includono `Tab` altrimenti lo perderebbe), un campo di testo con il focus attiva l'input di testo di SDL, e le direzioni del controller arrivano come tasti freccia con `ACTION_1` come Invio e `ACTION_2` come Esc. Ciò che un elemento usa non arriva mai a `onInputPressed`, `onKeyPressed` o alle callback del mouse; ciò che nessuno ha usato ricade su di loro come al solito. Termina lo script per un handle non valido.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guisethandler,guiSetHandler>>, <<guidraw,guiDraw>>, <<sceneprobegui,sceneProbeGui>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- L'HUD non riceve mai input; la pagina della pausa sì, finché è visibile.
|
||
guiSetInput(hud, false)
|
||
|
||
function onInputPressed(what)
|
||
if what == SWITCH_START1 then
|
||
guiShow(gui, pausePage)
|
||
guiSetInput(gui, true)
|
||
singeSetPauseFlag(true)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#guisetvalue]
|
||
==== guiSetValue
|
||
|
||
[source,text]
|
||
----
|
||
guiSetValue(gui, document, id, value)
|
||
----
|
||
|
||
Imposta il valore dell'elemento con quell’`id`: il valore di un controllo di un modulo (il testo di un campo di testo, il numero di un cursore, il `value` di un'opzione di `select`; lo stato `checked` di una casella di controllo è un attributo, da impostare tramite l'API di RmlUi), oppure, per qualsiasi altro elemento, il suo RML interno, così un paragrafo o un `div` si possono riscrivere con testo semplice o con del markup. Il documento si reimpagina al fotogramma successivo. Solleva `No element "id" in document N of GUI G` quando l'elemento non esiste.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- l'attributo `id` dell'elemento.
|
||
* `value` -- il nuovo valore, una stringa; i numeri vengono convertiti.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guigetvalue,guiGetValue>>, <<guisethandler,guiSetHandler>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La riga del punteggio e la fila delle vite vengono riscritte a ogni fotogramma; la fila è markup.
|
||
function onOverlayUpdate()
|
||
guiSetValue(hud, hudPage, "score", string.format("%08d", score))
|
||
guiSetValue(hud, hudPage, "lives", string.rep("<img src=\"ship.png\"/>", lives))
|
||
overlayClear()
|
||
guiDraw(hud)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#guishow]
|
||
==== guiShow
|
||
|
||
[source,text]
|
||
----
|
||
guiShow(gui, document)
|
||
----
|
||
|
||
Mostra un documento nascosto da `guiHide` (un documento è già mostrato quando `guiLoad` ritorna, quindi in quel caso la chiamata non serve). Un documento mostrato viene disegnato ovunque sia la sua GUI e riceve input quando lo riceve la sua GUI. Solleva `No document N in GUI G` per un handle non valido.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guihide,guiHide>>, <<guiload,guiLoad>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La pagina di game over compare quando se ne va l'ultima vita.
|
||
function onLastLifeLost()
|
||
guiSetValue(gui, gameOver, "final", string.format("%08d", score))
|
||
guiShow(gui, gameOver)
|
||
end
|
||
----
|
||
|
||
[#joint]
|
||
=== Giunto
|
||
|
||
I giunti collegano due corpi, o un corpo e il mondo quando il secondo nodo è `-1`, e restituiscono un handle intero del giunto che le altre chiamate accettano. Ancore e assi si danno nello spazio del mondo nel momento in cui il giunto viene creato, con i corpi dove si trovano allora, quindi posiziona i nodi e dai loro un corpo prima di unirli con un giunto. Le distanze sono in unità del mondo e gli angoli delle cerniere in gradi. Eliminare uno dei due corpi, o il suo nodo, elimina il giunto; un handle già eliminato termina lo script quando viene passato di nuovo. Vedi <<physics,Fisica>> in Sviluppo di giochi, Giunti.
|
||
|
||
[#jointhinge]
|
||
==== jointHinge
|
||
|
||
[source,text]
|
||
----
|
||
joint = jointHinge(nodeA, nodeB, ax, ay, az, dx, dy, dz)
|
||
----
|
||
|
||
Crea una cerniera tra i due corpi passante per il punto del mondo `(ax, ay, az)` lungo l'asse `(dx, dy, dz)`: una porta, una ruota, una leva, un'altalena a bilico. I corpi mantengono in comune quel punto e quell'asse e possono solo ruotarvi attorno. L'asse viene normalizzato, quindi la sua lunghezza non conta; un asse nullo diventa `(0, 1, 0)`. Entrambi i nodi devono avere un corpo, oppure `nodeB` può essere `-1` per incernierare `nodeA` al mondo. Termina lo script quando un nodo non ha un corpo o il giunto non può essere creato.
|
||
|
||
*Parametri:*
|
||
|
||
* `nodeA` -- il corpo di cui si misura la rotazione; un angolo positivo di `jointSetLimits` è la sua rotazione destrorsa attorno all'asse.
|
||
* `nodeB` -- l'altro corpo, o `-1` per il mondo.
|
||
* `ax`, `ay`, `az` -- l'ancora, un punto del mondo per cui passa l'asse della cerniera.
|
||
* `dx`, `dy`, `dz` -- l'asse della cerniera nello spazio del mondo.
|
||
|
||
*Restituisce:* L'handle del giunto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<jointsetlimits,jointSetLimits>>, <<jointdelete,jointDelete>>, <<jointball,jointBall>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una porta incernierata al mondo lungo il bordo sinistro, che si apre di 100 gradi in un solo verso.
|
||
door = nodeNew()
|
||
nodeSetMesh(door, meshBox(1.4, 2.2, 0.1), wood)
|
||
nodeSetPosition(door, -0.1, -0.62, 1.4)
|
||
bodyNew(door, BODY_DYNAMIC, SHAPE_BOX, 1.4, 2.2, 0.1)
|
||
bodySetMass(door, 8)
|
||
hinge = jointHinge(door, -1, -0.85, -0.62, 1.4, 0, 1, 0)
|
||
jointSetLimits(hinge, 0, 100)
|
||
-- Più tardi, una spinta vicino alla maniglia la spalanca.
|
||
bodyApplyImpulse(door, 0, 0, -60, 0.5, -0.62, 1.45)
|
||
----
|
||
|
||
[#jointball]
|
||
==== jointBall
|
||
|
||
[source,text]
|
||
----
|
||
joint = jointBall(nodeA, nodeB, ax, ay, az)
|
||
----
|
||
|
||
Crea un giunto sferico nel punto del mondo `(ax, ay, az)`: i corpi possono ruotarvi attorno in ogni direzione ma non separarsi. Un pendolo, un anello di catena, un'insegna appesa, la spalla di un ragdoll. Entrambi i nodi devono avere un corpo, oppure `nodeB` può essere `-1` per appendere `nodeA` a un punto fisso del mondo. Un giunto sferico non ha limiti, quindi `jointSetLimits` lo rifiuta. Termina lo script quando un nodo non ha un corpo o il giunto non può essere creato.
|
||
|
||
*Parametri:*
|
||
|
||
* `nodeA` -- il primo corpo.
|
||
* `nodeB` -- l'altro corpo, o `-1` per il mondo.
|
||
* `ax`, `ay`, `az` -- l'ancora, il punto del mondo attorno a cui ruotano i corpi.
|
||
|
||
*Restituisce:* L'handle del giunto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<jointhinge,jointHinge>>, <<jointdelete,jointDelete>>, <<bodyapplyimpulse,bodyApplyImpulse>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una catena di quattro anelli appesa a un gancio, ognuno unito con un giunto sferico a quello sopra.
|
||
local above = -1
|
||
for i = 1, 4 do
|
||
local link = nodeNew()
|
||
nodeSetMesh(link, meshSphere(0.12, 16), iron)
|
||
nodeSetPosition(link, 2, 3 - i * 0.3, 0)
|
||
bodyNew(link, BODY_DYNAMIC, SHAPE_SPHERE, 0.12)
|
||
bodySetMass(link, 1)
|
||
jointBall(link, above, 2, 3.15 - i * 0.3, 0)
|
||
above = link
|
||
end
|
||
----
|
||
|
||
[#jointslider]
|
||
==== jointSlider
|
||
|
||
[source,text]
|
||
----
|
||
joint = jointSlider(nodeA, nodeB, ax, ay, az, dx, dy, dz)
|
||
----
|
||
|
||
Lascia scorrere i corpi l'uno rispetto all'altro solo lungo l'asse `(dx, dy, dz)` passante per l'ancora `(ax, ay, az)`, mantenendo il loro orientamento: un cassetto, un pistone, un montacarichi, una porta scorrevole. L'asse viene normalizzato, quindi la sua lunghezza non conta; un asse nullo diventa `(0, 1, 0)`. Entrambi i nodi devono avere un corpo, oppure `nodeB` può essere `-1` per far correre `nodeA` lungo una rotaia fissa nel mondo. Termina lo script quando un nodo non ha un corpo o il giunto non può essere creato.
|
||
|
||
*Parametri:*
|
||
|
||
* `nodeA` -- il corpo di cui si misura lo spostamento; una distanza positiva di `jointSetLimits` è lungo l'asse.
|
||
* `nodeB` -- l'altro corpo, o `-1` per il mondo.
|
||
* `ax`, `ay`, `az` -- un punto del mondo sulla rotaia.
|
||
* `dx`, `dy`, `dz` -- la direzione dello spostamento nello spazio del mondo.
|
||
|
||
*Restituisce:* L'handle del giunto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<jointsetlimits,jointSetLimits>>, <<jointhinge,jointHinge>>, <<bodysetvelocity,bodySetVelocity>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una cassa su una rotaia lungo X, spinta di lato all'inizio e fermata 2.4 unità più in là.
|
||
crate = nodeNew()
|
||
nodeSetMesh(crate, meshBox(0.5, 0.5, 0.5), wood)
|
||
nodeSetPosition(crate, -0.6, 0.9, -3.0)
|
||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.5, 0.5, 0.5)
|
||
slider = jointSlider(crate, -1, -0.6, 0.9, -3.0, 1, 0, 0)
|
||
jointSetLimits(slider, 0, 2.4)
|
||
bodySetVelocity(crate, 3, 0, 0)
|
||
----
|
||
|
||
[#jointsetlimits]
|
||
==== jointSetLimits
|
||
|
||
[source,text]
|
||
----
|
||
jointSetLimits(joint, low, high)
|
||
----
|
||
|
||
Limita l'angolo di una cerniera in gradi, o lo spostamento di una guida in unità del mondo, da una parte e dall'altra della posizione in cui il giunto è partito. Un angolo positivo della cerniera è una rotazione destrorsa di `nodeA` attorno all'asse; una distanza positiva della guida è lungo l'asse. `low` viene limitato al massimo a `0` e `high` almeno a `0`, quindi la posa iniziale è sempre dentro l'intervallo. Richiamala per cambiare l'intervallo in qualsiasi momento. Termina lo script per un giunto sferico o per un handle che non esiste più.
|
||
|
||
*Parametri:*
|
||
|
||
* `joint` -- l'handle restituito da `jointHinge` o `jointSlider`.
|
||
* `low` -- l'angolo o la distanza negativa più lontana, `0` o meno.
|
||
* `high` -- l'angolo o la distanza positiva più lontana, `0` o più.
|
||
|
||
*Note:* Se i due corpi vengono dati nell'ordine inverso si scambiano i ruoli, il che inverte il segno dell'intervallo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<jointhinge,jointHinge>>, <<jointslider,jointSlider>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una leva che oscilla di 30 gradi per parte, poi si blocca una volta risolto l'enigma.
|
||
lever = nodeNew()
|
||
nodeSetMesh(lever, meshBox(0.1, 0.8, 0.1), iron)
|
||
nodeSetPosition(lever, 4, 1.2, 0)
|
||
bodyNew(lever, BODY_DYNAMIC, SHAPE_BOX, 0.1, 0.8, 0.1)
|
||
pivot = jointHinge(lever, -1, 4, 0.8, 0, 0, 0, 1)
|
||
jointSetLimits(pivot, -30, 30)
|
||
|
||
function lockLever()
|
||
jointSetLimits(pivot, 0, 0)
|
||
end
|
||
----
|
||
|
||
[#jointdelete]
|
||
==== jointDelete
|
||
|
||
[source,text]
|
||
----
|
||
jointDelete(joint)
|
||
----
|
||
|
||
Rimuove il giunto; i corpi tornano liberi l'uno dall'altro e mantengono la velocità che avevano. I giunti spariscono anche insieme a uno dei loro corpi, quindi questa chiamata serve solo a liberare due corpi che restano entrambi. Termina lo script per un handle che non esiste o che è già stato eliminato.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<jointhinge,jointHinge>>, <<jointball,jointBall>>, <<bodydelete,bodyDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La fune si spezza quando la cassa appesa viene colpita abbastanza forte.
|
||
function onCollision(a, b, x, y, z, speed)
|
||
if (a == crate or b == crate) and speed > 6 and rope then
|
||
jointDelete(rope)
|
||
rope = nil
|
||
soundPlay(snap)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#joystick]
|
||
=== Joystick
|
||
|
||
Ogni dispositivo che Singe apre è un controller e si legge con le chiamate
|
||
`controller*`; un dispositivo che SDL non riconosce ci arriva tramite
|
||
un'assegnazione che Singe scrive apposta, quindi non c'è nient'altro da chiamare.
|
||
Vedi xref:Manual.adoc#devices[Ogni dispositivo è un controller]. Le due chiamate
|
||
qui servono a far fare a una levetta le veci di un mouse.
|
||
|
||
[#joymouseenable]
|
||
==== joyMouseEnable
|
||
|
||
[source,text]
|
||
----
|
||
joyMouseEnable(enabled)
|
||
----
|
||
|
||
Estensione di Hypseus. Attiva o disattiva il joystick come mouse mentre il gioco è in esecuzione. Quando è attivo, la levetta sinistra del controller nello slot `0` muove il puntatore che il gioco vede, attraverso lo stesso `onMouseMoved` a cui arriva un mouse vero, tenuto dentro l'immagine; `--js_range` è la velocità e l'asse rispetta la propria zona morta. È ciò che rende giocabile un gioco con pistola ottica su un cabinato con una levetta e senza mouse.
|
||
|
||
Parte dal valore di `--joymouse`, disattivato per impostazione predefinita, e un `singeReload` lo riporta lì. In questa impostazione predefinita Singe differisce da Hypseus: il `controls.cfg` distribuito assegna già quella levetta ai quattro interruttori di direzione, e muovere anche il cursore senza che nessuno l'abbia chiesto sorprenderebbe ogni gioco esistente.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00 (Hypseus)
|
||
*Vedi anche:* <<joymouseisenabled,joyMouseIsEnabled>>, <<onmousemoved,onMouseMoved>>, <<mousegetposition,mouseGetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un gioco di tiro che lascia mirare con la levetta quando non c'è un mouse collegato.
|
||
if mouseHowMany() == 0 then
|
||
joyMouseEnable(true)
|
||
end
|
||
----
|
||
|
||
[#joymouseisenabled]
|
||
==== joyMouseIsEnabled
|
||
|
||
[source,text]
|
||
----
|
||
enabled = joyMouseIsEnabled()
|
||
----
|
||
|
||
Se in questo momento la levetta sta muovendo il cursore del mouse, che l'abbia deciso `--joymouse` o `joyMouseEnable`.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<joymouseenable,joyMouseEnable>>
|
||
|
||
[#keyboard]
|
||
=== Tastiera
|
||
|
||
Singe legge la tastiera in una di due modalità, scelta con `keyboardSetMode`. In `MODE_NORMAL` (quella predefinita) i tasti vengono tradotti, tramite le assegnazioni di `controls.cfg`, nei valori `SWITCH_*` che arrivano a `onInputPressed` e `onInputReleased`, e i tasti non assegnati vengono ignorati; in `MODE_FULL` ogni tasto arriva grezzo allo script, attraverso `onKeyPressed` e `onKeyReleased` oltre che `onInputPressed` e `onInputReleased`. Qualunque sia la modalità, `keyboardIsDown`, `keyboardGetLastDown` e `keyboardGetLastUp` interrogano lo stato attuale della tastiera, che è ciò che serve al modello a thread (vedi <<threaded,A thread>>). Uno scancode indica la posizione fisica di un tasto (il tasto `A` è lo scancode `4` con ogni layout), mentre un keysym è il carattere che il tasto produce; la tabella `SCANCODE` di `Framework.singe` dà un nome a ogni scancode, `keyboardIsDown` accetta scancode e `onKeyPressed` li consegna entrambi. Le callback stesse sono descritte in <<enginecallbacks,Callback del motore>>.
|
||
|
||
[#keyboardcatchquit]
|
||
==== keyboardCatchQuit
|
||
|
||
[source,text]
|
||
----
|
||
keyboardCatchQuit(catch)
|
||
----
|
||
|
||
Estensione di Hypseus. Affida l'interruttore di uscita allo script, esattamente come fa `singeSetQuitKeyEnabled(not catch)`: `keyboardCatchQuit(true)` impedisce al motore di uscire su `INPUT_QUIT`, e `false` glielo restituisce. È una funzione definita in `Framework.singe`, mantenuta perché i giochi scritti per Hypseus Singe funzionino senza modifiche.
|
||
|
||
*Note:* Alias storico.
|
||
|
||
WARNING: Il codice nuovo dovrebbe chiamare <<singesetquitkeyenabled,singeSetQuitKeyEnabled>>.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<singesetquitkeyenabled,singeSetQuitKeyEnabled>>, <<singequit,singeQuit>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Codice di un gioco Hypseus; la forma moderna è singeSetQuitKeyEnabled(false).
|
||
if keyboardCatchQuit ~= nil then
|
||
keyboardCatchQuit(true)
|
||
end
|
||
|
||
function onInputPressed(what)
|
||
if what == SWITCH_QUIT then
|
||
askToQuit()
|
||
end
|
||
end
|
||
----
|
||
|
||
[#keyboardgetlastdown]
|
||
==== keyboardGetLastDown
|
||
|
||
[source,text]
|
||
----
|
||
scancode = keyboardGetLastDown()
|
||
----
|
||
|
||
Restituisce lo scancode del tasto premuto più di recente nel fotogramma corrente, o `0` quando non è stato premuto nulla dall'ultimo fotogramma. Il valore viene azzerato dopo ogni chiamata a `onOverlayUpdate`, il che lo rende un'interrogazione da una volta sola del tipo «è appena stato premuto un tasto?» per il modello a thread. Qui vengono registrati anche i pulsanti dei controller e del mouse, con i loro codici `GAMEPAD_N` e `MOUSE_N`, perché il motore li fa passare per la stessa strada dei tasti. Il valore viene conservato indipendentemente dalla modalità della tastiera e dalle assegnazioni di `controls.cfg`.
|
||
|
||
*Restituisce:* scancode intero, o `0`.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<keyboardgetlastup,keyboardGetLastUp>>, <<keyboardisdown,keyboardIsDown>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Modello a thread: reagisci a una pressione di tasto per fotogramma.
|
||
function singeMain()
|
||
while true do
|
||
local key = keyboardGetLastDown()
|
||
if key == SCANCODE.ESCAPE.value then
|
||
singeQuit()
|
||
elseif key == SCANCODE.SPACE.value then
|
||
jump()
|
||
end
|
||
singeYield()
|
||
end
|
||
end
|
||
----
|
||
|
||
[#keyboardgetlastup]
|
||
==== keyboardGetLastUp
|
||
|
||
[source,text]
|
||
----
|
||
scancode = keyboardGetLastUp()
|
||
----
|
||
|
||
Restituisce lo scancode del tasto rilasciato più di recente nel fotogramma corrente, o `0` quando non è stato rilasciato nulla dall'ultimo fotogramma. Viene azzerato dopo ogni chiamata a `onOverlayUpdate`, come `keyboardGetLastDown`, e registra allo stesso modo i codici dei pulsanti dei controller e del mouse.
|
||
|
||
*Restituisce:* scancode intero, o `0`.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<keyboardgetlastdown,keyboardGetLastDown>>, <<keyboardisdown,keyboardIsDown>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un colpo caricato parte quando si lascia il tasto di fuoco.
|
||
function singeMain()
|
||
while true do
|
||
if keyboardIsDown(SCANCODE.LCTRL.value) then
|
||
charge = math.min(charge + 1, chargeMax)
|
||
elseif keyboardGetLastUp() == SCANCODE.LCTRL.value then
|
||
fireShot(charge)
|
||
charge = 0
|
||
end
|
||
singeYield()
|
||
end
|
||
end
|
||
----
|
||
|
||
[#keyboardgetmode]
|
||
==== keyboardGetMode
|
||
|
||
[source,text]
|
||
----
|
||
mode = keyboardGetMode()
|
||
----
|
||
|
||
Restituisce la modalità della tastiera in vigore: `MODE_NORMAL` (`0`) o `MODE_FULL` (`1`). Ogni script parte in `MODE_NORMAL`, anche dopo un ricaricamento.
|
||
|
||
*Restituisce:* la modalità, un intero.
|
||
|
||
*Disponibile da:* 1.x (RDG)
|
||
*Vedi anche:* <<keyboardsetmode,keyboardSetMode>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Funzione condivisa per l'inserimento di testo che ripristina la modalità del chiamante.
|
||
function promptForName()
|
||
local previous = keyboardGetMode()
|
||
keyboardSetMode(MODE_FULL)
|
||
entering = true
|
||
restoreMode = previous
|
||
end
|
||
----
|
||
|
||
[#keyboardgetmodifiers]
|
||
==== keyboardGetModifiers
|
||
|
||
[source,text]
|
||
----
|
||
modifiers = keyboardGetModifiers()
|
||
----
|
||
|
||
Restituisce i tasti modificatori tenuti premuti in questo momento come maschera di bit SDL `KMOD_*`. Verificala con un AND bit a bit con i valori della tabella `MODIFIER`: le voci per un solo tasto come `MODIFIER.LSHIFT` e quelle combinate `MODIFIER.SHIFT`, `MODIFIER.CTRL`, `MODIFIER.ALT` e `MODIFIER.GUI`, che valgono per entrambi i lati. Legge lo stato attuale, quindi funziona in ogni modalità della tastiera e da qualsiasi callback.
|
||
|
||
*Restituisce:* maschera di bit intera.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<keyboardgetlastdown,keyboardGetLastDown>>, <<onkeypressedonkeyreleased,onKeyPressed>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Maiusc+Tab mostra o nasconde l'overlay delle statistiche.
|
||
function onKeyPressed(keysym, scancode)
|
||
if scancode == SCANCODE.TAB.value then
|
||
if keyboardGetModifiers() & MODIFIER.SHIFT.value ~= 0 then
|
||
showStats = not showStats
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
[#keyboardisdown]
|
||
==== keyboardIsDown
|
||
|
||
[source,text]
|
||
----
|
||
down = keyboardIsDown(scancode)
|
||
----
|
||
|
||
Verifica se un tasto è fisicamente tenuto premuto in questo momento, in base allo stato che il motore conserva per ogni evento di tasto che vede. Non dipende dalla modalità della tastiera né da `controls.cfg`, e funziona da qualsiasi callback, quindi si presta a un movimento che deve continuare finché un tasto resta premuto. Gli scancode fuori dall'intervallo definito da SDL restituiscono `false` invece di terminare lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `scancode` -- un valore della tabella `SCANCODE`, come `SCANCODE.LEFT.value`.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<keyboardgetlastdown,keyboardGetLastDown>>, <<controllergetbutton,controllerGetButton>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Movimento in otto direzioni dai tasti tenuti premuti.
|
||
function onOverlayUpdate()
|
||
local dx = 0
|
||
local dy = 0
|
||
if keyboardIsDown(SCANCODE.LEFT.value) then dx = dx - 1 end
|
||
if keyboardIsDown(SCANCODE.RIGHT.value) then dx = dx + 1 end
|
||
if keyboardIsDown(SCANCODE.UP.value) then dy = dy - 1 end
|
||
if keyboardIsDown(SCANCODE.DOWN.value) then dy = dy + 1 end
|
||
playerX = playerX + dx * speed
|
||
playerY = playerY + dy * speed
|
||
drawPlayer()
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#keyboardsetmode]
|
||
==== keyboardSetMode
|
||
|
||
[source,text]
|
||
----
|
||
keyboardSetMode(mode)
|
||
----
|
||
|
||
Passa da uno all'altro dei due modelli di tastiera. In `MODE_NORMAL` arrivano allo script solo i tasti assegnati in `controls.cfg`, come valori `SWITCH_*`, la ripetizione dei tasti viene scartata così che ogni pressione arrivi una volta sola, e il motore agisce sui propri interruttori (pausa, uscita, screenshot, cattura del mouse). In `MODE_FULL` ogni tasto arriva a `onInputPressed` e `onInputReleased` come keysym e a `onKeyPressed` e `onKeyReleased` come keysym e scancode, il testo prodotto da una pressione arriva a `onTextInput`, la ripetizione dei tasti viene consegnata per l'inserimento di testo, e le assegnazioni da tastiera degli interruttori del motore vengono ignorate così che il gioco tenga per sé ogni tasto; le assegnazioni dei pulsanti dei controller e del mouse continuano ad agire, e quei pulsanti arrivano ancora allo script come i loro valori `SWITCH_*`, dato che non si possono digitare. La maggior parte dei giochi imposta la modalità una volta all'avvio; un gioco con inserimento di testo passa a `MODE_FULL` per il tempo necessario e poi torna indietro. Qualsiasi altro valore termina lo script. In `MODE_FULL` i keysym di quattro tasti sono anche numeri di interruttore -- Backspace è `8` (`SWITCH_BUTTON3`), Tab `9` (`SWITCH_COIN1`), Invio `13` ed Esc `27` -- quindi uno script che legge `onInputPressed` per un interruttore di un controller o del mouse e si aspetta anche quei tasti dovrebbe controllare il tasto con `keyboardIsDown` prima di trattare il valore come l'interruttore.
|
||
|
||
*Parametri:*
|
||
|
||
* `mode` -- `MODE_NORMAL` (`0`) o `MODE_FULL` (`1`).
|
||
|
||
*Disponibile da:* 1.x (RDG)
|
||
*Vedi anche:* <<keyboardgetmode,keyboardGetMode>>, <<oninputpressedoninputreleased,onInputPressed>>, <<onkeypressedonkeyreleased,onKeyPressed>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- L'inserimento del punteggio record prende i tasti grezzi, poi torna la modalità di gioco.
|
||
function beginInitials()
|
||
initials = ""
|
||
keyboardSetMode(MODE_FULL)
|
||
end
|
||
|
||
function onKeyPressed(keysym, scancode)
|
||
if scancode == SCANCODE.RETURN.value then
|
||
keyboardSetMode(MODE_NORMAL)
|
||
saveScore(initials, score)
|
||
elseif keysym >= string.byte("a") and keysym <= string.byte("z") and #initials < 3 then
|
||
initials = initials .. string.char(keysym):upper()
|
||
end
|
||
end
|
||
----
|
||
|
||
[#light]
|
||
=== Luce
|
||
|
||
Le luci sono nodi che portano una luce, creati da `lightNew` e spostati, resi figli di altri nodi e animati con le chiamate dei nodi. In un fotogramma risplendono fino a otto luci visibili, nell'ordine dei nodi; `sceneSetAmbient` e un cielo di `sceneSetSky` aggiungono luce da ogni parte. Le luci direzionali e i faretti illuminano lungo il -Z del nodo, quindi puntali con `nodeLookAt` o `nodeSetRotation`; le luci puntiformi hanno bisogno solo di una posizione. I colori sono interi da `0` a `255`, le intensità sono moltiplicatori nella luce lineare della scena, gli angoli sono in gradi e le distanze in unità del mondo. Ogni chiamata `lightSet*` termina lo script per un nodo che non porta una luce. Vedi <<scenes3d,Scene 3D>> per illuminazione, esposizione e ombre.
|
||
|
||
[#lightnew]
|
||
==== lightNew
|
||
|
||
[source,text]
|
||
----
|
||
node = lightNew(type)
|
||
node = lightNew(type, parent)
|
||
----
|
||
|
||
Crea un nuovo nodo che porta una luce e restituisce l'handle del nodo. La luce parte bianca con intensità `1`, senza limite di portata e, per un faretto, con un cono da `20` a `30` gradi. Posizionala e puntala con le chiamate dei nodi: `nodeSetPosition` per una luce puntiforme o un faretto, `nodeLookAt` per una luce direzionale o un faretto. Una luce figlia di un nodo in movimento si muove con esso, quindi un faro su un'auto o una torcia in mano richiedono una sola chiamata. Termina lo script per un tipo sconosciuto o quando non si può creare un nodo.
|
||
|
||
*Parametri:*
|
||
|
||
* `type` -- `LIGHT_DIRECTIONAL` (un sole; conta solo la sua direzione), `LIGHT_POINT` (una lampadina che si affievolisce con la distanza in ogni direzione) o `LIGHT_SPOT` (un cono lungo il -Z del nodo).
|
||
* `parent` -- il nodo genitore; la radice se omesso.
|
||
|
||
*Restituisce:* L'handle del nodo della luce, un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<lightsetcolor,lightSetColor>>, <<lightsetintensity,lightSetIntensity>>, <<lightsetrange,lightSetRange>>, <<lightsetcone,lightSetCone>>, <<scenesetambient,sceneSetAmbient>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Luce principale calda dall'alto a sinistra, una luce di riempimento fredda da destra,
|
||
-- e un faro che viaggia con l'auto.
|
||
key = lightNew(LIGHT_DIRECTIONAL)
|
||
nodeSetPosition(key, -3, 4, 3)
|
||
nodeLookAt(key, 0, 0, 0)
|
||
lightSetColor(key, 255, 235, 200)
|
||
lightSetIntensity(key, 1.4)
|
||
|
||
fill = lightNew(LIGHT_POINT)
|
||
nodeSetPosition(fill, 3, 1, 2)
|
||
lightSetColor(fill, 150, 190, 255)
|
||
lightSetIntensity(fill, 4)
|
||
lightSetRange(fill, 10)
|
||
|
||
headlight = lightNew(LIGHT_SPOT, car)
|
||
nodeSetPosition(headlight, 0.6, 0.5, -1.8)
|
||
lightSetIntensity(headlight, 30)
|
||
----
|
||
|
||
[#lightsetcolor]
|
||
==== lightSetColor
|
||
|
||
[source,text]
|
||
----
|
||
lightSetColor(node, r, g, b)
|
||
----
|
||
|
||
Il colore della luce, da `0` a `255` per canale (i valori fuori intervallo vengono limitati), trattato come sRGB; bianco per impostazione predefinita. La luminosità spetta a `lightSetIntensity`, quindi usa il colore per la tinta (una lampadina calda `255, 230, 190`, una luna fredda `150, 190, 255`) e lascia almeno un canale vicino a `255`. Termina lo script quando il nodo non porta una luce.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<lightsetintensity,lightSetIntensity>>, <<lightnew,lightNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- L'insegna al neon passa ciclicamente per i suoi colori.
|
||
signLight = lightNew(LIGHT_POINT)
|
||
nodeSetPosition(signLight, 2, 3, -1)
|
||
lightSetIntensity(signLight, 8)
|
||
lightSetRange(signLight, 6)
|
||
neon = { { 255, 40, 120 }, { 40, 220, 255 }, { 255, 210, 40 } }
|
||
|
||
function onOverlayUpdate()
|
||
local phase = math.floor(singeGetTicks() / 700) % #neon + 1
|
||
lightSetColor(signLight, neon[phase][1], neon[phase][2], neon[phase][3])
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#lightsetcone]
|
||
==== lightSetCone
|
||
|
||
[source,text]
|
||
----
|
||
lightSetCone(node, innerDegrees, outerDegrees)
|
||
----
|
||
|
||
La forma del cono di un faretto, con entrambi gli angoli misurati dall'asse (il -Z del nodo): piena luminosità entro `innerDegrees`, che sfuma dolcemente fino a zero a `outerDegrees`. I valori predefiniti sono `20` e `30`. Uno scarto stretto tra i due dà un faro da teatro dal bordo netto, uno ampio una pozza di luce morbida. I valori vengono memorizzati per ogni luce, ma solo una `LIGHT_SPOT` li usa. Termina lo script quando il nodo non porta una luce.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo della luce.
|
||
* `innerDegrees` -- semiangolo di piena luminosità, in gradi dall'asse.
|
||
* `outerDegrees` -- semiangolo in cui la luce si annulla, in gradi dall'asse; portato a `innerDegrees` se più piccolo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<lightnew,lightNew>>, <<lightsetrange,lightSetRange>>, <<nodelookat,nodeLookAt>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un faro di scena sul cantante, dal bordo netto, dal traliccio delle luci.
|
||
spot = lightNew(LIGHT_SPOT)
|
||
nodeSetPosition(spot, 0, 8, 4)
|
||
nodeLookAt(spot, nodeGetWorldPosition(singer))
|
||
lightSetColor(spot, 255, 245, 220)
|
||
lightSetIntensity(spot, 80)
|
||
lightSetCone(spot, 8, 10)
|
||
lightSetShadow(spot, true)
|
||
----
|
||
|
||
[#lightsetintensity]
|
||
==== lightSetIntensity
|
||
|
||
[source,text]
|
||
----
|
||
lightSetIntensity(node, intensity)
|
||
----
|
||
|
||
Il moltiplicatore di luminosità della luce nella luce lineare della scena, `1` per impostazione predefinita. Una luce direzionale di intensità `1` porta al bianco una superficie bianca rivolta verso di essa. Una luce puntiforme o un faretto si affievoliscono con la distanza come `1 / (1 + d^2)`, quindi la loro intensità è la luminosità alla luce stessa, la metà a un'unità di distanza e un centesimo a dieci unità; le lampadine di solito vogliono valori ben sopra `1`, da `10` a `50` per una stanza, e `sceneSetExposure` riporta l'intera scena nell'intervallo. Termina lo script quando il nodo non porta una luce.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<lightsetrange,lightSetRange>>, <<scenesetexposure,sceneSetExposure>>, <<lightsetcolor,lightSetColor>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una lanterna che tremola nel vento.
|
||
lantern = lightNew(LIGHT_POINT, lanternNode)
|
||
lightSetColor(lantern, 255, 200, 140)
|
||
lightSetRange(lantern, 9)
|
||
|
||
function onOverlayUpdate()
|
||
local flicker = 10 + math.random() * 3
|
||
lightSetIntensity(lantern, flicker)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#lightsetrange]
|
||
==== lightSetRange
|
||
|
||
[source,text]
|
||
----
|
||
lightSetRange(node, range)
|
||
----
|
||
|
||
Fin dove arriva una luce puntiforme o un faretto, in unità del mondo; `0` (il valore predefinito) significa nessun limite. La luce sfuma esattamente a zero alla portata, oltre all'attenuazione con la distanza, così le luci non scattano quando gli oggetti attraversano quel limite. Una portata limita anche fin dove deve arrivare la mappa delle ombre di una luce che proietta ombre, il che mantiene nitide le sue ombre. Le luci direzionali la ignorano. Termina lo script quando il nodo non porta una luce.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<lightsetintensity,lightSetIntensity>>, <<lightsetshadow,lightSetShadow>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Lampioni lungo la strada, ognuno illumina il proprio tratto.
|
||
for i = 0, 9 do
|
||
local lamp = lightNew(LIGHT_POINT)
|
||
nodeSetPosition(lamp, 4, 5, -i * 20)
|
||
lightSetColor(lamp, 255, 220, 170)
|
||
lightSetIntensity(lamp, 20)
|
||
lightSetRange(lamp, 14)
|
||
end
|
||
----
|
||
|
||
[#lightsetshadow]
|
||
==== lightSetShadow
|
||
|
||
[source,text]
|
||
----
|
||
lightSetShadow(node, shadow)
|
||
----
|
||
|
||
Fa proiettare alla luce le ombre di tutto ciò che è opaco nella scena, o smette di farlo; disattivato per impostazione predefinita. Qualsiasi delle otto luci può proiettare ombre, in qualsiasi combinazione. Un faretto riceve una mappa delle ombre; una luce direzionale una mappa per cascata (`sceneSetShadowCascades`); una luce puntiforme una cube map di sei viste, che copre ogni direzione (una lampadina dentro una stanza) a un costo sei volte maggiore. `nodeSetShadow` esonera una mesh dal proiettare ombre. Termina lo script quando il nodo non porta una luce.
|
||
|
||
*Note:* Ogni mappa delle ombre costa un passaggio in più sulla scena per fotogramma, una cube map sei, e un sole a cascate tanti quante sono le sue cascate; la dimensione delle mappe è impostata da `sceneSetShadowSize`. Una GPU integrata moderna gestisce comodamente otto luci che proiettano ombre; un Raspberry Pi ne vuole una o due a `512`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<scenesetshadowsize,sceneSetShadowSize>>, <<scenesetshadowcascades,sceneSetShadowCascades>>, <<nodesetshadow,nodeSetShadow>>, <<lightnew,lightNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il sole proietta ombre; le lampade illuminano soltanto. Il vetro stesso
|
||
-- della lampadina non deve fare ombra alla sua lampada.
|
||
sun = lightNew(LIGHT_DIRECTIONAL)
|
||
nodeSetPosition(sun, 3, 6, 4)
|
||
nodeLookAt(sun, 0, 0, 0)
|
||
lightSetShadow(sun, true)
|
||
|
||
lamp = lightNew(LIGHT_POINT)
|
||
nodeSetPosition(lamp, 3, 3.1, -2)
|
||
lightSetIntensity(lamp, 12)
|
||
lightSetRange(lamp, 14)
|
||
nodeSetShadow(bulbGlass, false)
|
||
----
|
||
|
||
[#material]
|
||
=== Materiale
|
||
|
||
Un materiale è l'aspetto di una mesh: un colore di base con alfa, una texture di base facoltativa presa da uno sprite, da un file KTX2, da un video o da una vista renderizzata, e i parametri metallic-roughness di glTF con quattro mappe facoltative. `materialNew` restituisce un handle intero; passalo a `nodeSetMesh` o a `nodeSetMaterial`, e a ogni chiamata `materialSet*`. I materiali sono condivisi, quindi cambiarne uno cambia ogni mesh disegnata con esso, e un nodo senza materiale viene disegnato bianco e ruvido a metà. I colori sono interi da `0` a `255` per canale e vengono trattati come sRGB; fattori come la rugosità vanno da `0` a `1`. Un handle non valido termina lo script. Vedi <<scenes3d,Scene 3D>> per come vengono illuminati i materiali.
|
||
|
||
[#materialnew]
|
||
==== materialNew
|
||
|
||
[source,text]
|
||
----
|
||
material = materialNew()
|
||
----
|
||
|
||
Alloca un nuovo materiale e ne restituisce l'handle. Parte bianco e opaco (colore `255, 255, 255, 255`), con rugosità `0.5`, metallicità `0`, nessun colore emissivo, ripetizione `1, 1`, filtro lineare, a una sola faccia, illuminato e senza texture né mappe. Imposta ciò che differisce da questo e passa l'handle a `nodeSetMesh`. I modelli caricati portano i propri materiali, quindi questa chiamata serve per le mesh costruite dallo script. Termina lo script su una macchina senza una GPU utilizzabile.
|
||
|
||
*Restituisce:* L'handle del materiale, un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetcolor,materialSetColor>>, <<materialsettexture,materialSetTexture>>, <<materialdelete,materialDelete>>, <<nodesetmesh,nodeSetMesh>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Oro spazzolato per un trofeo, vetro blu traslucido per la sua teca.
|
||
gold = materialNew()
|
||
materialSetColor(gold, 255, 200, 60)
|
||
materialSetMetallic(gold, 1)
|
||
materialSetRoughness(gold, 0.35)
|
||
|
||
glass = materialNew()
|
||
materialSetColor(glass, 80, 160, 255, 90)
|
||
materialSetBlend(glass, true)
|
||
materialSetRoughness(glass, 0.1)
|
||
|
||
trophy = nodeNew()
|
||
nodeSetMesh(trophy, meshCone(0.3, 0.8, 24), gold)
|
||
----
|
||
|
||
[#materialdelete]
|
||
==== materialDelete
|
||
|
||
[source,text]
|
||
----
|
||
materialDelete(material)
|
||
----
|
||
|
||
Libera il materiale e qualsiasi texture abbia caricato sulla GPU. Ogni nodo che era disegnato con esso torna all'aspetto predefinito (bianco, ruvido a metà), quindi riassegna prima quei nodi con `nodeSetMaterial` se devono mantenere un aspetto. Dopo, l'handle non è più valido e termina lo script se usato di nuovo. Il materiale privato che un nodo crea per sé con `nodeSetSprite` o `nodeSetText` appartiene al nodo e non può essere eliminato; la chiamata termina lo script, e togliere lo sprite o il testo lo libera. I materiali costano poco; eliminarli conta soprattutto per quelli che portavano texture grandi.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialnew,materialNew>>, <<nodesetmaterial,nodeSetMaterial>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Cambia l'aspetto del terreno del livello quando cambia la stagione.
|
||
function setSeason(texture)
|
||
local look = materialNew()
|
||
materialSetTexture(look, texture)
|
||
materialSetTiling(look, 16, 16)
|
||
nodeSetMaterial(ground, look)
|
||
if groundLook then
|
||
materialDelete(groundLook)
|
||
end
|
||
groundLook = look
|
||
end
|
||
----
|
||
|
||
[#materialsetblend]
|
||
==== materialSetBlend
|
||
|
||
[source,text]
|
||
----
|
||
materialSetBlend(material, blend)
|
||
----
|
||
|
||
Con `true` il materiale viene disegnato traslucido, miscelato secondo l'alfa del colore di base moltiplicato per l'alfa della texture, dopo ogni mesh opaca e in ordine dalla più lontana. Le mesh miscelate non scrivono la profondità, quindi lascialo a `false` (il valore predefinito) per le superfici solide: una mesh opaca con la miscelazione attiva può lasciar vedere attraverso di sé le cose che stanno dietro. Usalo con un alfa inferiore a `255` impostato da `materialSetColor` o con una texture con trasparenza.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetcolor,materialSetColor>>, <<materialsettexture,materialSetTexture>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un campo di forza che compare in dissolvenza quando viene colpito.
|
||
field = materialNew()
|
||
materialSetColor(field, 60, 200, 255, 0)
|
||
materialSetBlend(field, true)
|
||
materialSetDoubleSided(field, true)
|
||
nodeSetMesh(shield, meshSphere(2, 32), field)
|
||
|
||
function onShieldHit()
|
||
materialSetColor(field, 60, 200, 255, 160)
|
||
end
|
||
----
|
||
|
||
[#materialsetcolor]
|
||
==== materialSetColor
|
||
|
||
[source,text]
|
||
----
|
||
materialSetColor(material, r, g, b)
|
||
materialSetColor(material, r, g, b, a)
|
||
----
|
||
|
||
Imposta il colore di base, da `0` a `255` per canale (i valori fuori intervallo vengono limitati), con `a` che vale `255` per impostazione predefinita. Il colore è trattato come sRGB e illuminato in luce lineare. Quando il materiale ha una texture, un video o una vista, il colore li moltiplica, quindi il bianco lascia l'immagine com'è e un colore più scuro la tinge. L'alfa si vede solo con `materialSetBlend`.
|
||
|
||
*Parametri:*
|
||
|
||
* `material` -- l'handle del materiale.
|
||
* `r, g, b` -- rosso, verde e blu, da `0` a `255`.
|
||
* `a` -- opacità, da `0` a `255`; `255` se omesso.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsettexture,materialSetTexture>>, <<materialsetblend,materialSetBlend>>, <<materialsetemissive,materialSetEmissive>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- I colori delle squadre sull'unica mesh dell'auto.
|
||
teamLooks = {}
|
||
for team, rgb in ipairs({ { 220, 40, 40 }, { 40, 90, 220 }, { 240, 200, 30 } }) do
|
||
local look = materialNew()
|
||
materialSetColor(look, rgb[1], rgb[2], rgb[3])
|
||
materialSetMetallic(look, 0.6)
|
||
materialSetRoughness(look, 0.3)
|
||
teamLooks[team] = look
|
||
end
|
||
nodeSetMesh(playerCar, carMesh, teamLooks[1])
|
||
----
|
||
|
||
[#materialsetcutoff]
|
||
==== materialSetCutoff
|
||
|
||
[source,text]
|
||
----
|
||
materialSetCutoff(material, cutoff)
|
||
----
|
||
|
||
Scarta ogni texel il cui alfa è sotto `cutoff` e disegna il resto del tutto opaco, nella passata illuminata come in quella delle ombre, così una card di foglie proietta l'ombra delle foglie invece che quella del suo quad. È il mascheramento alfa di glTF: a differenza di <<materialsetblend,`materialSetBlend`>> non ha bisogno di ordinamento, ma un texel è tenuto intero o scartato, mai miscelato. La soglia è limitata tra `0` e `1`, e `0`, il valore predefinito, disattiva il mascheramento. Un modello caricato da glTF arriva con la soglia che il suo file dà a un materiale `MASK`.
|
||
|
||
*Parametri:*
|
||
|
||
* `material` -- handle intero del materiale.
|
||
* `cutoff` -- numero; l'alfa, da `0` a `1`, che serve a un texel per essere disegnato.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetblend,materialSetBlend>>, <<materialsetdoublesided,materialSetDoubleSided>>, <<materialsettexture,materialSetTexture>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una rete metallica: un solo quad con texture, i buchi ritagliati, disegnata su entrambi i lati.
|
||
fence = materialNew()
|
||
materialSetTexture(fence, spriteLoad(DIR .. "Art/chainLink.png"))
|
||
materialSetCutoff(fence, 0.5)
|
||
materialSetDoubleSided(fence, true)
|
||
panel = nodeNew()
|
||
nodeSetMesh(panel, meshPlane(4, 2), fence)
|
||
nodeSetPosition(panel, 0, 1, -5)
|
||
nodeSetRotation(panel, 90, 0, 0)
|
||
----
|
||
|
||
[#materialsetdoublesided]
|
||
==== materialSetDoubleSided
|
||
|
||
[source,text]
|
||
----
|
||
materialSetDoubleSided(material, doubleSided)
|
||
----
|
||
|
||
Disegna entrambe le facce di ogni triangolo invece di scartare quella posteriore. Serve per le forme aperte viste da dietro, come un singolo `meshPlane` visto da sotto, una bandiera o un quad con delle foglie. Le forme chiuse appaiono uguali in entrambi i casi, quindi per loro lascialo a `false` (il valore predefinito): un materiale a doppia faccia costa fill rate e le ombre vengono proiettate anch'esse da entrambe le facce.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<meshplane,meshPlane>>, <<materialsetblend,materialSetBlend>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Uno striscione appeso sopra la strada, leggibile da entrambi i lati.
|
||
cloth = materialNew()
|
||
materialSetTexture(cloth, spriteLoad(DIR .. "Art/banner.png"))
|
||
materialSetDoubleSided(cloth, true)
|
||
materialSetRoughness(cloth, 0.9)
|
||
banner = nodeNew()
|
||
nodeSetMesh(banner, meshPlane(3, 1), cloth)
|
||
nodeSetPosition(banner, 0, 4, -6)
|
||
nodeSetRotation(banner, 90, 0, 0)
|
||
----
|
||
|
||
[#materialsetemissive]
|
||
==== materialSetEmissive
|
||
|
||
[source,text]
|
||
----
|
||
materialSetEmissive(material, r, g, b)
|
||
----
|
||
|
||
La luce che la superficie emette da sé, da `0` a `255` per canale (i valori fuori intervallo vengono limitati), aggiunta sopra l'illuminazione: schermi, lampadine, neon, quadri strumenti. Il valore predefinito è nero, nessun bagliore. Non illumina nient'altro e non proietta luce; per questo metti un nodo luce nello stesso punto. Con `materialSetEmissiveMap` la mappa dice in quale parte della superficie si applica questo colore. Le superfici emissive abbastanza luminose da superare la soglia di `sceneSetBloom` brillano.
|
||
|
||
*Note:* Un materiale non illuminato (`materialSetUnlit`) mostra solo il suo colore di base; su di esso il colore emissivo viene ignorato.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetemissivemap,materialSetEmissiveMap>>, <<materialsetunlit,materialSetUnlit>>, <<scenesetbloom,sceneSetBloom>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una spia che pulsa di rosso quando il reattore si surriscalda.
|
||
lampLook = materialNew()
|
||
materialSetColor(lampLook, 60, 10, 10)
|
||
nodeSetMesh(lamp, meshSphere(0.15, 16), lampLook)
|
||
|
||
function onOverlayUpdate()
|
||
local pulse = (math.sin(singeGetTicks() / 120) + 1) / 2
|
||
if overheating then
|
||
materialSetEmissive(lampLook, math.floor(255 * pulse), 20, 20)
|
||
else
|
||
materialSetEmissive(lampLook, 0, 0, 0)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#materialsetemissivemap]
|
||
==== materialSetEmissiveMap
|
||
|
||
[source,text]
|
||
----
|
||
materialSetEmissiveMap(material, sprite)
|
||
materialSetEmissiveMap(material, "file.ktx2")
|
||
materialSetEmissiveMap(material, nil)
|
||
materialSetEmissiveMap(material)
|
||
----
|
||
|
||
Una texture che dice quali parti della superficie brillano. Il suo colore viene moltiplicato per il colore di `materialSetEmissive`, quindi imposta anche quello: bianco per usare i colori della mappa stessa, o una tinta. L'immagine viene da uno sprite caricato (ne viene caricata una copia sulla GPU, quindi poi lo sprite può essere scaricato) o da un file `.ktx2` indicato per nome, tenuto compresso sulla GPU. Con `nil` o senza secondo argomento la mappa viene rimossa. Termina lo script quando la texture non può essere caricata sulla GPU o il file non è una texture KTX2.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetemissive,materialSetEmissive>>, <<materialsettexture,materialSetTexture>>, <<materialsettiling,materialSetTiling>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un pannello di controllo i cui pulsanti e indicatori si accendono.
|
||
panel = materialNew()
|
||
materialSetTexture(panel, spriteLoad(DIR .. "Art/panel.png"))
|
||
materialSetEmissiveMap(panel, spriteLoad(DIR .. "Art/panelLights.png"))
|
||
materialSetEmissive(panel, 255, 255, 255)
|
||
materialSetRoughness(panel, 0.6)
|
||
nodeSetMesh(nodeFind("Panel", cockpit), meshPlane(1.2, 0.4), panel)
|
||
|
||
function onPowerDown()
|
||
materialSetEmissive(panel, 0, 0, 0)
|
||
end
|
||
----
|
||
|
||
[#materialsetfilter]
|
||
==== materialSetFilter
|
||
|
||
[source,text]
|
||
----
|
||
materialSetFilter(material, filter)
|
||
----
|
||
|
||
Come vengono campionate le texture del materiale. `FILTER_LINEAR` (il valore predefinito) fonde i texel vicini e i livelli di mipmap, con il filtro anisotropico sulle superfici viste di striscio. `FILTER_NEAREST` prende il texel più vicino e il livello di mipmap più vicino, così la pixel art resta nitida su un quad. Ogni texture ha una catena completa di mipmap, quindi una texture vista in piccolo non sfarfalla in nessuno dei due casi. Qualsiasi altro valore termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsettexture,materialSetTexture>>, <<materialsetunlit,materialSetUnlit>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una piastrella di 16 per 16 pixel disegnata su un blocco senza sbavature.
|
||
pixelLook = materialNew()
|
||
materialSetTexture(pixelLook, spriteLoad(DIR .. "Art/dirt16.png"))
|
||
materialSetFilter(pixelLook, FILTER_NEAREST)
|
||
materialSetRoughness(pixelLook, 1)
|
||
block = nodeNew()
|
||
nodeSetMesh(block, meshBox(1, 1, 1), pixelLook)
|
||
----
|
||
|
||
[#materialsetgui]
|
||
==== materialSetGui
|
||
|
||
[source,text]
|
||
----
|
||
materialSetGui(material, gui)
|
||
materialSetGui(material, nil)
|
||
materialSetGui(material)
|
||
----
|
||
|
||
Mostra una GUI di `guiNew` come texture del colore di base del materiale, aggiornata a ogni fotogramma: un monitor, un pannello di cabina di pilotaggio, un'insegna, con lo stesso documento che un `guiDraw` mostrerebbe in piano. Di solito va in coppia con `materialSetUnlit` perché la pagina non venga ombreggiata dalle luci, ed è tinta dal colore di base come qualsiasi texture. Con `nil` o senza secondo argomento il materiale torna alla propria texture, che resta conservata sotto; eliminare la GUI fa lo stesso. Impostare una GUI toglie qualsiasi video o vista il materiale stesse mostrando. Finché la GUI accetta input, il mouse o una pistola ottica puntati su una superficie disegnata con il materiale la raggiungono attraverso lo stesso sondaggio di `sceneProbeGui`. Termina lo script per un handle di GUI non valido.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guinew,guiNew>>, <<guidraw,guiDraw>>, <<sceneprobegui,sceneProbeGui>>, <<materialsetview,materialSetView>>, <<materialsetvideo,materialSetVideo>>, <<materialsetunlit,materialSetUnlit>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La pagina delle opzioni sullo schermo del cabinato modellato.
|
||
options = guiNew(720, 480)
|
||
page = guiLoad(options, DIR .. "gui/options.rml")
|
||
screen = materialNew()
|
||
materialSetGui(screen, options)
|
||
materialSetUnlit(screen, true)
|
||
nodeSetMesh(nodeFind("Screen", cabinet), meshPlane(3.0, 2.0), screen)
|
||
----
|
||
|
||
[#materialsetmetallic]
|
||
==== materialSetMetallic
|
||
|
||
[source,text]
|
||
----
|
||
materialSetMetallic(material, metallic)
|
||
----
|
||
|
||
Quanto è metallica la superficie, da `0` per i dielettrici (vernice, plastica, legno, pelle) a `1` per i metalli, che tingono i riflessi speculari e le riflessioni con il colore di base. Il valore predefinito è `0`. I valori intermedi servono per miscele e superfici consumate; la maggior parte dei materiali vuole un estremo o l'altro. Con `materialSetMetallicRoughnessMap` il canale blu della mappa moltiplica questo valore. Un metallo ha poco da riflettere senza un cielo di `sceneSetSky`, quindi usa i due insieme.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetroughness,materialSetRoughness>>, <<materialsetmetallicroughnessmap,materialSetMetallicRoughnessMap>>, <<scenesetsky,sceneSetSky>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Paraurti cromato, carrozzeria verniciata.
|
||
chrome = materialNew()
|
||
materialSetColor(chrome, 240, 240, 245)
|
||
materialSetMetallic(chrome, 1)
|
||
materialSetRoughness(chrome, 0.15)
|
||
|
||
paint = materialNew()
|
||
materialSetColor(paint, 30, 80, 200)
|
||
materialSetMetallic(paint, 0)
|
||
materialSetRoughness(paint, 0.4)
|
||
nodeSetMaterial(nodeFind("Bumper", car), chrome)
|
||
nodeSetMaterial(nodeFind("Body", car), paint)
|
||
----
|
||
|
||
[#materialsetmetallicroughnessmap]
|
||
==== materialSetMetallicRoughnessMap
|
||
|
||
[source,text]
|
||
----
|
||
materialSetMetallicRoughnessMap(material, sprite)
|
||
materialSetMetallicRoughnessMap(material, "file.ktx2")
|
||
materialSetMetallicRoughnessMap(material, nil)
|
||
materialSetMetallicRoughnessMap(material)
|
||
----
|
||
|
||
Una texture che fa variare la finitura sulla superficie: il suo canale verde moltiplica la rugosità di `materialSetRoughness` e il canale blu il valore metallico di `materialSetMetallic`, come li impacchetta glTF, quindi una mappa prodotta da uno strumento glTF funziona così com'è. Imposta entrambi i fattori a `1` per usare solo i valori della mappa. L'immagine viene da uno sprite o da un file `.ktx2` indicato per nome; `nil` o nessun secondo argomento rimuove la mappa. Termina lo script quando la texture non può essere caricata sulla GPU.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetmetallic,materialSetMetallic>>, <<materialsetroughness,materialSetRoughness>>, <<materialsettexture,materialSetTexture>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Placcatura metallica graffiata: lucida dove la vernice si è consumata.
|
||
plating = materialNew()
|
||
materialSetTexture(plating, DIR .. "Textures/plating.ktx2")
|
||
materialSetMetallicRoughnessMap(plating, DIR .. "Textures/platingMetalRough.ktx2")
|
||
materialSetMetallic(plating, 1)
|
||
materialSetRoughness(plating, 1)
|
||
materialSetNormalMap(plating, DIR .. "Textures/platingNormal.ktx2")
|
||
nodeSetMesh(hullPanel, meshPlane(4, 2), plating)
|
||
----
|
||
|
||
[#materialsetnormalmap]
|
||
==== materialSetNormalMap
|
||
|
||
[source,text]
|
||
----
|
||
materialSetNormalMap(material, sprite)
|
||
materialSetNormalMap(material, sprite, strength)
|
||
materialSetNormalMap(material, "file.ktx2")
|
||
materialSetNormalMap(material, "file.ktx2", strength)
|
||
materialSetNormalMap(material, nil)
|
||
materialSetNormalMap(material)
|
||
----
|
||
|
||
Una normal map in spazio tangente (il solito tipo bluastro, piatta a `128, 128, 255`) che inclina l'illuminazione per texel, così mattoni, scanalature e rivetti catturano la luce senza geometria in più. Ogni mesh ha le tangenti che le servono: quelle del modello quando il file le contiene, altrimenti calcolate dalle UV. L'immagine viene da uno sprite o da un file `.ktx2` indicato per nome; `nil` o nessun secondo argomento rimuove la mappa. Termina lo script quando la texture non può essere caricata sulla GPU.
|
||
|
||
*Parametri:*
|
||
|
||
* `material` -- l'handle del materiale.
|
||
* `sprite` o `"file.ktx2"` -- la mappa, da uno sprite caricato o da un file KTX2; `nil` o l'argomento omesso la rimuovono.
|
||
* `strength` -- quanto i rilievi inclinano l'illuminazione, `1` (il valore predefinito) com'è stata creata, `0` piatta, sopra `1` esagerata.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsettexture,materialSetTexture>>, <<materialsetocclusionmap,materialSetOcclusionMap>>, <<materialsettiling,materialSetTiling>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un muro di mattoni da un piano piatto, con le linee di malta che catturano il sole basso.
|
||
brick = materialNew()
|
||
materialSetTexture(brick, spriteLoad(DIR .. "Art/brick.png"))
|
||
materialSetNormalMap(brick, spriteLoad(DIR .. "Art/brickNormal.png"), 1.5)
|
||
materialSetRoughness(brick, 0.9)
|
||
materialSetTiling(brick, 6, 2)
|
||
wall = nodeNew()
|
||
nodeSetMesh(wall, meshPlane(12, 4), brick)
|
||
nodeSetPosition(wall, 0, 2, -3)
|
||
nodeSetRotation(wall, 90, 0, 0)
|
||
----
|
||
|
||
[#materialsetoccluder]
|
||
==== materialSetOccluder
|
||
|
||
[source,text]
|
||
----
|
||
materialSetOccluder(material, occluder)
|
||
----
|
||
|
||
Fa scrivere al materiale la profondità e nessun colore. Ciò che la mesh copre
|
||
viene nascosto come se la mesh fosse solida, ma la mesh stessa non si vede: ciò
|
||
che è stato disegnato dietro di essa -- un fondale dipinto, il disco -- traspare.
|
||
È il sostituto di qualcosa che sta in un'immagine ma non nella scena: un
|
||
pilastro in una stanza dipinta dietro cui un personaggio 3D deve passare, una
|
||
porta, un tavolo. Un occlusore non proietta ombre, dato che il dipinto ne ha
|
||
già una, e non riceve luce. `false` (il valore predefinito) rende di nuovo
|
||
normale il materiale.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetblend,materialSetBlend>>, <<materialsetunlit,materialSetUnlit>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un pilastro che sta nel dipinto: un parallelepipedo invisibile dove si trova, così
|
||
-- l'eroe sparisce dietro di esso e ricompare dall'altra parte.
|
||
hidden = materialNew()
|
||
materialSetOccluder(hidden, true)
|
||
pillar = nodeNew()
|
||
nodeSetMesh(pillar, meshBox(0.8, 4, 0.8), hidden)
|
||
nodeSetPosition(pillar, 2, 2, -3)
|
||
----
|
||
|
||
[#materialsetocclusionmap]
|
||
==== materialSetOcclusionMap
|
||
|
||
[source,text]
|
||
----
|
||
materialSetOcclusionMap(material, sprite)
|
||
materialSetOcclusionMap(material, sprite, strength)
|
||
materialSetOcclusionMap(material, "file.ktx2")
|
||
materialSetOcclusionMap(material, "file.ktx2", strength)
|
||
materialSetOcclusionMap(material, nil)
|
||
materialSetOcclusionMap(material)
|
||
----
|
||
|
||
Una mappa di occlusione ambientale, con l'occlusione nel canale rosso (bianco per le parti aperte, scuro per le fessure), che scurisce la luce ambiente e quella del cielo negli angoli e nelle pieghe. La luce diretta dei nodi luce non ne è influenzata, come in glTF. L'immagine viene da uno sprite o da un file `.ktx2` indicato per nome; `nil` o nessun secondo argomento rimuove la mappa. Termina lo script quando la texture non può essere caricata sulla GPU.
|
||
|
||
*Parametri:*
|
||
|
||
* `material` -- l'handle del materiale.
|
||
* `sprite` o `"file.ktx2"` -- la mappa, da uno sprite caricato o da un file KTX2; `nil` o l'argomento omesso la rimuovono.
|
||
* `strength` -- quanta parte della mappa applicare, da `0` per niente a `1` (il valore predefinito) per intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetnormalmap,materialSetNormalMap>>, <<scenesetambient,sceneSetAmbient>>, <<scenesetenvironment,sceneSetEnvironment>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ombreggiatura precalcolata per le pareti della grotta, a metà intensità perché le lampade arrivino ancora agli angoli.
|
||
rock = materialNew()
|
||
materialSetTexture(rock, DIR .. "Textures/caveRock.ktx2")
|
||
materialSetNormalMap(rock, DIR .. "Textures/caveRockNormal.ktx2")
|
||
materialSetOcclusionMap(rock, DIR .. "Textures/caveRockAO.ktx2", 0.5)
|
||
materialSetRoughness(rock, 1)
|
||
nodeSetMaterial(nodeFind("Walls", cave), rock)
|
||
----
|
||
|
||
[#materialsetroughness]
|
||
==== materialSetRoughness
|
||
|
||
[source,text]
|
||
----
|
||
materialSetRoughness(material, roughness)
|
||
----
|
||
|
||
Quanto è ruvida la superficie, da `0` per un riflesso stretto come quello di uno specchio a `1` per una superficie opaca. Il valore predefinito è `0.5`. I riflessi seguono il modello GGX, quindi valori bassi danno una piccola macchia brillante e riflessi del cielo nitidi, valori alti una lucentezza ampia e morbida e riflessi sfocati. Con `materialSetMetallicRoughnessMap` il canale verde della mappa moltiplica questo valore.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetmetallic,materialSetMetallic>>, <<materialsetmetallicroughnessmap,materialSetMetallicRoughnessMap>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Bagna la strada quando piove: lo stesso asfalto, riflessi più stretti.
|
||
asphalt = materialNew()
|
||
materialSetColor(asphalt, 50, 50, 55)
|
||
materialSetRoughness(asphalt, 0.95)
|
||
nodeSetMesh(road, meshPlane(8, 200), asphalt)
|
||
|
||
function onWeatherChange(raining)
|
||
if raining then
|
||
materialSetRoughness(asphalt, 0.25)
|
||
else
|
||
materialSetRoughness(asphalt, 0.95)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#materialsettexture]
|
||
==== materialSetTexture
|
||
|
||
[source,text]
|
||
----
|
||
materialSetTexture(material, sprite)
|
||
materialSetTexture(material, "file.ktx2")
|
||
materialSetTexture(material, nil)
|
||
materialSetTexture(material)
|
||
----
|
||
|
||
Usa l'immagine di uno sprite come texture del colore di base (ne viene caricata una copia sulla GPU, quindi poi lo sprite può essere scaricato), o un file KTX2 indicato per nome, tenuto compresso sulla GPU nel formato a blocchi che la macchina supporta (vedi Texture compresse in <<scenes3d,Scene 3D>>). Con `nil` o senza secondo argomento la texture viene rimossa e il materiale mostra solo il suo colore di base. Impostare una texture toglie anche qualsiasi video o vista il materiale stesse mostrando. Le coordinate di texture vengono dalla mesh; le primitive stendono un'immagine una volta su ogni faccia, e `materialSetTiling` la ripete. Un nome che non è un file KTX2 termina lo script (carica le altre immagini con `spriteLoad`), come pure un caricamento sulla GPU non riuscito.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<spriteload,spriteLoad>>, <<materialsettiling,materialSetTiling>>, <<materialsetvideo,materialSetVideo>>, <<materialsetview,materialSetView>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una cassa con la texture di uno sprite che il gioco carica già per il menu,
|
||
-- e un pavimento da una texture compressa distribuita con il gioco.
|
||
crateArt = spriteLoad(DIR .. "Art/crate.png")
|
||
crateLook = materialNew()
|
||
materialSetTexture(crateLook, crateArt)
|
||
crate = nodeNew()
|
||
nodeSetMesh(crate, meshBox(1, 1, 1), crateLook)
|
||
|
||
floorLook = materialNew()
|
||
materialSetTexture(floorLook, DIR .. "Textures/tiles.ktx2")
|
||
materialSetTiling(floorLook, 10, 10)
|
||
nodeSetMesh(floor, meshPlane(20, 20), floorLook)
|
||
----
|
||
|
||
[#materialsettiling]
|
||
==== materialSetTiling
|
||
|
||
[source,text]
|
||
----
|
||
materialSetTiling(material, u, v)
|
||
----
|
||
|
||
Quante volte le texture del materiale (quella di base come le mappe) si ripetono nell'intervallo UV da `0` a `1` di una superficie, `1` e `1` per impostazione predefinita. `24, 24` su una heightmap larga 120 unità ripete una texture ogni cinque unità. Le texture si ripetono oltre i loro bordi, quindi l'immagine deve affiancarsi senza giunture visibili. I valori sotto `1` mostrano parte dell'immagine; i valori negativi la specchiano.
|
||
|
||
*Parametri:*
|
||
|
||
* `material` -- l'handle del materiale.
|
||
* `u` -- ripetizioni lungo la direzione U della superficie.
|
||
* `v` -- ripetizioni lungo la direzione V della superficie.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsettexture,materialSetTexture>>, <<meshheightmap,meshHeightmap>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Erba ogni cinque unità sulle colline.
|
||
ground = materialNew()
|
||
materialSetTexture(ground, DIR .. "Textures/grass.ktx2")
|
||
materialSetTiling(ground, 24, 24)
|
||
materialSetRoughness(ground, 0.95)
|
||
hills = nodeNew()
|
||
nodeSetMesh(hills, meshHeightmap(DIR .. "Art/hills.png", 120, 14, 120), ground)
|
||
bodyNew(hills, BODY_STATIC, SHAPE_MESH)
|
||
----
|
||
|
||
[#materialsetunlit]
|
||
==== materialSetUnlit
|
||
|
||
[source,text]
|
||
----
|
||
materialSetUnlit(material, unlit)
|
||
----
|
||
|
||
Mostra il colore di base e la texture esattamente come sono, ignorando ogni luce, la luce ambiente e il cielo: per schermi video, monitor che mostrano una vista, skybox e aspetti a ombreggiatura piatta. La nebbia si applica comunque. Il colore emissivo e le mappe non vengono usati su un materiale non illuminato. Il valore predefinito è `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetemissive,materialSetEmissive>>, <<materialsetvideo,materialSetVideo>>, <<materialsetview,materialSetView>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il marquee del cabinato: un'immagine illuminata dall'interno, quindi le
|
||
-- luci della stanza non devono ombreggiarla.
|
||
marquee = materialNew()
|
||
materialSetTexture(marquee, spriteLoad(DIR .. "Art/marquee.png"))
|
||
materialSetUnlit(marquee, true)
|
||
nodeSetMesh(nodeFind("Marquee", cabinet), meshPlane(0.7, 0.2), marquee)
|
||
----
|
||
|
||
[#materialsetview]
|
||
==== materialSetView
|
||
|
||
[source,text]
|
||
----
|
||
materialSetView(material, view)
|
||
materialSetView(material, nil)
|
||
materialSetView(material)
|
||
----
|
||
|
||
Mostra una vista renderizzata di `viewNew` come texture del colore di base del materiale, aggiornata a ogni fotogramma: un monitor di sorveglianza, uno specchietto retrovisore, un portale. Di solito va in coppia con `materialSetUnlit` perché l'immagine non venga ombreggiata dalle luci, ed è tinta dal colore di base come qualsiasi texture. Con `nil` o senza secondo argomento il materiale torna alla propria texture, che resta conservata sotto. Impostare una vista toglie qualsiasi video il materiale stesse mostrando. Termina lo script per un handle di vista non valido.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<viewnew,viewNew>>, <<viewsetcamera,viewSetCamera>>, <<materialsetvideo,materialSetVideo>>, <<materialsetunlit,materialSetUnlit>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Uno specchietto retrovisore sull'auto del giocatore.
|
||
rearCamera = nodeNew(car)
|
||
nodeSetPosition(rearCamera, 0, 1.2, -0.5)
|
||
nodeSetRotation(rearCamera, 0, 180, 0)
|
||
rearView = viewNew(384, 256)
|
||
viewSetCamera(rearView, rearCamera)
|
||
mirror = materialNew()
|
||
materialSetView(mirror, rearView)
|
||
materialSetUnlit(mirror, true)
|
||
nodeSetMesh(nodeFind("Mirror", car), meshPlane(0.3, 0.1), mirror)
|
||
----
|
||
|
||
[#materialsetvideo]
|
||
==== materialSetVideo
|
||
|
||
[source,text]
|
||
----
|
||
materialSetVideo(material)
|
||
materialSetVideo(material, video)
|
||
----
|
||
|
||
Usa il laserdisc (senza `video`) o un video caricato con `videoLoad` come texture del colore di base, aggiornata a ogni fotogramma mentre è in riproduzione. Di solito va in coppia con `materialSetUnlit` perché l'immagine non venga tinta dalle luci. Il video continua a essere riprodotto che sia disegnato o meno anche sull'overlay, e più materiali possono mostrare lo stesso video. Impostare un video toglie qualsiasi texture o vista il materiale avesse; `materialSetTexture` lo toglie di nuovo.
|
||
|
||
*Note:* La forma con il disco termina lo script in un gioco senza disco, e lo stesso fa un handle di video non valido.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<videoload,videoLoad>>, <<materialsetunlit,materialSetUnlit>>, <<materialsettexture,materialSetTexture>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un cabinato modellato il cui schermo mostra il laserdisc, e una parete di
|
||
-- monitor che ripetono in loop una clip della modalità attract.
|
||
cabinet = modelInstance(modelLoad(DIR .. "Models/Cabinet.glb"))
|
||
screen = materialNew()
|
||
materialSetVideo(screen)
|
||
materialSetUnlit(screen, true)
|
||
nodeSetMesh(nodeFind("Screen", cabinet), meshPlane(0.6, 0.45), screen)
|
||
|
||
attract = videoLoad(DIR .. "Video/attract.mp4")
|
||
videoPlay(attract)
|
||
monitors = materialNew()
|
||
materialSetVideo(monitors, attract)
|
||
materialSetUnlit(monitors, true)
|
||
----
|
||
|
||
[#mesh]
|
||
=== Mesh
|
||
|
||
Una mesh è geometria sulla GPU: una primitiva che il motore costruisce, un
|
||
terreno ricavato da un'immagine o dati di vertici forniti dal tuo script. Gli
|
||
handle sono interi restituiti dai costruttori `mesh*`; un numero qualsiasi di
|
||
nodi può condividere una mesh, e i nodi che disegnano la stessa mesh con lo
|
||
stesso materiale vengono disegnati con un'unica chiamata instanziata, quindi
|
||
crea una sola mesh e molti nodi. Le dimensioni sono in unità della scena e ogni
|
||
primitiva è centrata sull'origine del suo nodo, salvo indicazione contraria,
|
||
così la trasformazione del nodo la posiziona, la ruota e la scala. Le
|
||
coordinate di texture vanno da `0` a `1` una volta su ogni faccia o superficie.
|
||
Ogni costruttore richiede il renderer 3D descritto in <<scenes3d,Scene 3D>> e
|
||
termina lo script quando non è disponibile.
|
||
|
||
[#meshbox]
|
||
==== meshBox
|
||
|
||
[source,text]
|
||
----
|
||
mesh = meshBox(width, height, depth)
|
||
----
|
||
|
||
Un parallelepipedo largo `width` lungo X, alto `height` lungo Y e profondo
|
||
`depth` lungo Z, centrato sull'origine. Ognuna delle sei facce ha i propri
|
||
quattro vertici con una normale piatta, quindi gli spigoli sono ombreggiati in
|
||
modo netto, e la texture è stesa una volta su ogni faccia. Casse, muri,
|
||
pavimenti e carrozzerie sono tutti parallelepipedi.
|
||
|
||
*Restituisce:* L'handle della mesh.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetmesh,nodeSetMesh>>, <<meshplane,meshPlane>>, <<bodynew,bodyNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una lastra di pietra per pavimento e una pila di casse che condividono una mesh.
|
||
local floor = nodeNew()
|
||
nodeSetMesh(floor, meshBox(16, 0.2, 10), stone)
|
||
nodeSetPosition(floor, 0, -0.1, 0)
|
||
local crateMesh = meshBox(0.6, 0.6, 0.6)
|
||
for i = 1, 4 do
|
||
local crate = nodeNew()
|
||
nodeSetMesh(crate, crateMesh, wood)
|
||
nodeSetPosition(crate, 0, i * 0.6 - 0.3, -2)
|
||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
|
||
end
|
||
----
|
||
|
||
[#meshcone]
|
||
==== meshCone
|
||
|
||
[source,text]
|
||
----
|
||
mesh = meshCone(radius, height)
|
||
mesh = meshCone(radius, height, segments)
|
||
----
|
||
|
||
Un cono in piedi lungo Y con la base chiusa: il cerchio di base di raggio
|
||
`radius` a `-height / 2` e il vertice a `+height / 2`. `segments` (predefinito
|
||
`24`, almeno `3`) è il numero di sfaccettature attorno; i lati sono ombreggiati
|
||
in modo continuo dall'una all'altra. La texture avvolge una volta i lati ed è
|
||
stesa una volta sulla base.
|
||
|
||
*Restituisce:* L'handle della mesh.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<meshcylinder,meshCylinder>>, <<nodesetmesh,nodeSetMesh>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un paralume sopra la lampadina, e una fila di pini fatti di un cono ciascuno.
|
||
local shade = nodeNew(stand)
|
||
nodeSetMesh(shade, meshCone(0.45, 0.5, 24), brass)
|
||
nodeSetPosition(shade, 0, 0.9, 0)
|
||
local foliage = meshCone(0.8, 2.0, 12)
|
||
for i = 1, 5 do
|
||
local tree = nodeNew()
|
||
nodeSetMesh(tree, foliage, pine)
|
||
nodeSetPosition(tree, i * 3 - 9, 1.0, -6)
|
||
end
|
||
----
|
||
|
||
[#meshcylinder]
|
||
==== meshCylinder
|
||
|
||
[source,text]
|
||
----
|
||
mesh = meshCylinder(radius, height)
|
||
mesh = meshCylinder(radius, height, segments)
|
||
----
|
||
|
||
Un cilindro lungo Y, chiuso a entrambe le estremità, da `-height / 2` a
|
||
`+height / 2`. `segments` (predefinito `24`, almeno `3`) è il numero di
|
||
sfaccettature attorno, ombreggiate in modo continuo. Per una ruota o una
|
||
moneta, ruota il nodo di `90` gradi attorno a X o a Z così che l'asse stia
|
||
dove ti serve.
|
||
|
||
*Restituisce:* L'handle della mesh.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<meshcone,meshCone>>, <<meshtorus,meshTorus>>, <<nodesetrotation,nodeSetRotation>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un lampione, e uno pneumatico coricato su un fianco come ruota.
|
||
local post = nodeNew()
|
||
nodeSetMesh(post, meshCylinder(0.08, 3, 10), dark)
|
||
nodeSetPosition(post, 3, 1.5, -2)
|
||
local tire = nodeNew(wheel)
|
||
nodeSetMesh(tire, meshCylinder(0.35, 0.25, 16), rubber)
|
||
nodeSetRotation(tire, 0, 0, 90)
|
||
----
|
||
|
||
[#meshdelete]
|
||
==== meshDelete
|
||
|
||
[source,text]
|
||
----
|
||
meshDelete(mesh)
|
||
----
|
||
|
||
Libera i buffer della mesh sulla GPU e la sua copia sulla CPU. I nodi che la
|
||
usavano restano senza mesh (e senza pesi di morph) e non disegnano nulla finché
|
||
non ne ricevono un'altra; l'handle non è più valido e può essere riutilizzato da
|
||
un costruttore successivo. Le mesh non vengono liberate quando si elimina un
|
||
nodo, quindi elimina tu una mesh quando niente la userà più.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<meshnew,meshNew>>, <<nodesetmesh,nodeSetMesh>>, <<nodedelete,nodeDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ricostruisci il tracciato da una nuova heightmap tra un round e l'altro.
|
||
function loadTrack(image)
|
||
if trackMesh then
|
||
meshDelete(trackMesh)
|
||
end
|
||
trackMesh = meshHeightmap(image, 200, 20, 200)
|
||
nodeSetMesh(track, trackMesh, tarmac)
|
||
bodyNew(track, BODY_STATIC, SHAPE_MESH)
|
||
end
|
||
----
|
||
|
||
[#meshheightmap]
|
||
==== meshHeightmap
|
||
|
||
[source,text]
|
||
----
|
||
mesh = meshHeightmap(image, sizeX, sizeY, sizeZ)
|
||
----
|
||
|
||
Una mesh di terreno da un'immagine in scala di grigi, da `2` a `1025` pixel per
|
||
lato: un vertice per pixel su `sizeX` per `sizeZ` unità centrate sul nodo,
|
||
ciascuno sollevato del canale rosso del pixel (da `0` nero a `255` bianco) per
|
||
`sizeY`, con la riga superiore dell'immagine sul bordo lontano (`-Z`). Le
|
||
normali vengono calcolate dalle pendenze e le coordinate di texture vanno da
|
||
`0` a `1` sull'insieme, quindi `materialSetTiling` imposta la ripetizione della
|
||
texture. I campioni di altezza vengono conservati: `terrainGetHeight` li legge,
|
||
e un corpo `SHAPE_MESH` sul nodo diventa un campo di altezze. Un file che non
|
||
si può aprire o decodificare, una dimensione sbagliata o l'assenza del renderer
|
||
3D terminano lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `image` -- il nome dell'immagine relativo al gioco, in qualsiasi formato letto da `spriteLoad`.
|
||
* `sizeX` -- la larghezza del terreno lungo X in unità del mondo.
|
||
* `sizeY` -- l'altezza di un pixel bianco sopra uno nero.
|
||
* `sizeZ` -- la profondità del terreno lungo Z in unità del mondo.
|
||
|
||
*Restituisce:* L'handle della mesh.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<terraingetheight,terrainGetHeight>>, <<materialsettiling,materialSetTiling>>, <<bodynew,bodyNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
local ground = materialNew()
|
||
materialSetTexture(ground, spriteLoad("grass.png"))
|
||
materialSetTiling(ground, 24, 24)
|
||
materialSetRoughness(ground, 0.95)
|
||
local hills = nodeNew()
|
||
nodeSetMesh(hills, meshHeightmap("hills.png", 120, 14, 120), ground)
|
||
bodyNew(hills, BODY_STATIC, SHAPE_MESH)
|
||
-- I piedi del drago sul pendio, ovunque si trovi lì il terreno.
|
||
nodeSetPosition(dragon, -8, terrainGetHeight(hills, -8, 3) or 0, 3)
|
||
----
|
||
|
||
[#meshnew]
|
||
==== meshNew
|
||
|
||
[source,text]
|
||
----
|
||
mesh = meshNew(positions, normals, uvs, indices)
|
||
----
|
||
|
||
Costruisce la geometria da tabelle di numeri. Le tangenti per il normal mapping
|
||
vengono calcolate per te, e così le normali smussate quando `normals` è `nil`,
|
||
facendo la media dei triangoli che condividono ogni vertice. I triangoli sono
|
||
rivolti in avanti quando i loro vertici girano in senso antiorario visti
|
||
dall'esterno; dai al materiale `materialSetDoubleSided` se il retro deve
|
||
vedersi. Una tabella della forma sbagliata, o un indice fuori dall'elenco dei
|
||
vertici, termina lo script con un messaggio che dice quale.
|
||
|
||
*Parametri:*
|
||
|
||
* `positions` -- `x, y, z` per ogni vertice, uno dopo l'altro; almeno un vertice.
|
||
* `normals` -- `x, y, z` per vertice, nello stesso numero di `positions`, o `nil` per farle calcolare.
|
||
* `uvs` -- `u, v` per vertice (da `0` a `1`, con `v` verso il basso dell'immagine), o `nil` per nessuna (ogni vertice a `0, 0`).
|
||
* `indices` -- tre numeri di vertice per triangolo, contati da `1` nell'ordine in cui i vertici compaiono in `positions`; almeno un triangolo.
|
||
|
||
*Restituisce:* L'handle della mesh.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetmesh,nodeSetMesh>>, <<materialsetdoublesided,materialSetDoubleSided>>, <<meshdelete,meshDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una freccia piatta che punta lungo +X, vista dall'alto (a doppia faccia, così funziona anche da sotto).
|
||
local arrow = meshNew(
|
||
{ 0, 0, -0.5, 1, 0, 0, 0, 0, 0.5, -0.6, 0, -0.5, -0.6, 0, 0.5 },
|
||
nil,
|
||
nil,
|
||
{ 1, 2, 3, 4, 1, 3, 4, 3, 5 })
|
||
local marker = materialNew()
|
||
materialSetColor(marker, 255, 200, 40)
|
||
materialSetDoubleSided(marker, true)
|
||
local pointer = nodeNew()
|
||
nodeSetMesh(pointer, arrow, marker)
|
||
nodeSetPosition(pointer, 0, 0.05, 0)
|
||
----
|
||
|
||
[#meshplane]
|
||
==== meshPlane
|
||
|
||
[source,text]
|
||
----
|
||
mesh = meshPlane(width, depth)
|
||
mesh = meshPlane(width, depth, columns)
|
||
mesh = meshPlane(width, depth, columns, rows)
|
||
----
|
||
|
||
Un rettangolo piatto nel piano XZ rivolto verso +Y, largo `width` lungo X e
|
||
profondo `depth` lungo Z, centrato sull'origine, con la texture stesa una volta
|
||
sopra. Due argomenti danno un singolo quad. `columns` (e `rows`, che per
|
||
impostazione predefinita vale `columns`) lo suddividono in altrettanti quad per
|
||
lato, cosa di cui un corpo morbido (`softNew`) ha bisogno per piegarsi come un
|
||
tessuto; entrambi devono essere almeno `1`. Ruota il nodo di `90` gradi attorno
|
||
a X per metterlo in piedi rivolto verso +Z. È a una sola faccia a meno che il
|
||
suo materiale non sia a doppia faccia.
|
||
|
||
*Restituisce:* L'handle della mesh.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetdoublesided,materialSetDoubleSided>>, <<materialsetvideo,materialSetVideo>>, <<softnew,softNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il terreno, uno schermo che mostra il disco e una bandiera che diventerà un tessuto.
|
||
local plain = nodeNew()
|
||
nodeSetMesh(plain, meshPlane(60, 60), ground)
|
||
local tv = nodeNew()
|
||
nodeSetMesh(tv, meshPlane(3.0, 2.2), screen)
|
||
nodeSetPosition(tv, 0, 1.6, -4)
|
||
nodeSetRotation(tv, 90, 0, 0)
|
||
local flag = nodeNew()
|
||
nodeSetMesh(flag, meshPlane(1.6, 1.0, 16, 10), cloth)
|
||
nodeSetPosition(flag, 0.8, 2.5, 0)
|
||
nodeSetRotation(flag, 90, 0, 0)
|
||
----
|
||
|
||
[#meshsphere]
|
||
==== meshSphere
|
||
|
||
[source,text]
|
||
----
|
||
mesh = meshSphere(radius)
|
||
mesh = meshSphere(radius, segments)
|
||
----
|
||
|
||
Una sfera di raggio `radius`, centrata sull'origine, costruita a fasce di
|
||
latitudine e longitudine: `segments` (predefinito `32`, almeno `3`) attorno
|
||
all'equatore e la metà da polo a polo. La texture la avvolge una volta, con
|
||
`v` dal polo superiore a quello inferiore. Meno segmenti fanno una mesh più
|
||
leggera per palle viste da lontano; più segmenti danno una silhouette più
|
||
liscia da vicino.
|
||
|
||
*Restituisce:* L'handle della mesh.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<meshtorus,meshTorus>>, <<nodesetmesh,nodeSetMesh>>, <<bodynew,bodyNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una palla di vetro sul pavimento, e una piccola sfera indicatrice che condivide un aspetto.
|
||
local ball = nodeNew()
|
||
nodeSetMesh(ball, meshSphere(0.7, 48), glass)
|
||
nodeSetPosition(ball, -1.5, 0.7, 0)
|
||
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.7)
|
||
local dot = nodeNew()
|
||
nodeSetMesh(dot, meshSphere(0.08, 16), marker)
|
||
nodeSetPosition(dot, 0, 1.2, 0)
|
||
----
|
||
|
||
[#meshtorus]
|
||
==== meshTorus
|
||
|
||
[source,text]
|
||
----
|
||
mesh = meshTorus(radius, tubeRadius)
|
||
mesh = meshTorus(radius, tubeRadius, segments)
|
||
----
|
||
|
||
Un anello disteso nel piano XZ attorno a Y, centrato sull'origine: `radius`
|
||
dal centro al centro del tubo e `tubeRadius` il raggio del tubo stesso.
|
||
`segments` (predefinito `32`, almeno `3`) girano attorno all'anello e la metà
|
||
(almeno `3`) attorno al tubo. La texture avvolge una volta l'anello in `u` e
|
||
una volta il tubo in `v`.
|
||
|
||
*Restituisce:* L'handle della mesh.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<meshsphere,meshSphere>>, <<meshcylinder,meshCylinder>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un anello d'oro in piedi sul bordo, attraverso cui far rotolare la palla.
|
||
local gold = materialNew()
|
||
materialSetColor(gold, 255, 200, 80)
|
||
materialSetMetallic(gold, 1)
|
||
materialSetRoughness(gold, 0.25)
|
||
local ring = nodeNew()
|
||
nodeSetMesh(ring, meshTorus(1.2, 0.25, 40), gold)
|
||
nodeSetPosition(ring, 0, 1.2, -2)
|
||
nodeSetRotation(ring, 90, 0, 0)
|
||
----
|
||
|
||
[#_microphone]
|
||
=== Microfono
|
||
|
||
Un microfono, per un gioco di canto o per qualsiasi cosa che ascolti. Si apre
|
||
un dispositivo di registrazione alla volta, ascoltato come un solo canale a
|
||
48000 campioni al secondo qualunque cosa fornisca l'hardware; la conversione la
|
||
fa SDL. Singe conserva gli ultimi otto secondi di ciò che sente, e ogni chiamata
|
||
qui sotto risponde da lì, quindi chiedere non costa nulla che il gioco non
|
||
abbia già pagato.
|
||
|
||
Ciò che di solito vuole un gioco è <<micpitch,`micPitch`>> -- la nota cantata --
|
||
e <<miclevel,`micLevel`>>. Un gioco che analizza il suono da sé, o lo invia da
|
||
qualche parte, legge i campioni con <<micread,`micRead`>>; le chiamate di
|
||
<<voice,Voce>> li codificano per una rete.
|
||
|
||
Il microfono si chiude quando il gioco finisce. Non si apre nulla finché uno script non lo chiede.
|
||
|
||
[NOTE]
|
||
====
|
||
Una nota cantata arriva al gioco un po' dopo essere stata cantata: la scheda
|
||
audio, il driver e i buffer la trattengono ciascuno per un momento. Un gioco di
|
||
canto che giudica la tempistica sottrae questo ritardo.
|
||
<<miccalibrate,`micCalibrate`>> lo misura riproducendo dei clic dagli
|
||
altoparlanti e cronometrando il loro ritorno, e il risultato viene conservato
|
||
per la macchina, come la <<singesetaudiocalibration,calibrazione audio>>.
|
||
Finora il monitoraggio e la calibrazione sono stati provati solo senza hardware
|
||
reale; intonazione, livello, lettura e registrazione sono stati provati con un
|
||
segnale noto.
|
||
====
|
||
|
||
[#miccalibrate]
|
||
==== micCalibrate
|
||
|
||
[source,text]
|
||
----
|
||
started = micCalibrate()
|
||
----
|
||
|
||
Misura la latenza del microfono: riproduce cinque clic dagli altoparlanti, a
|
||
una frazione di secondo l'uno dall'altro, e cronometra il ritorno di ciascuno
|
||
attraverso il microfono. Richiede qualche secondo; <<miccalibration,`micCalibration`>>
|
||
dice quando ha finito. Il risultato è il valore mediano dei clic sentiti, e
|
||
viene conservato per questa macchina. Falla in una stanza silenziosa, con gli
|
||
altoparlanti abbastanza alti perché il microfono li senta.
|
||
|
||
*Restituisce:* `true` quando è partita; `false` senza un microfono aperto o
|
||
senza un altoparlante da cui riprodurre i clic.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<miccalibration,micCalibration>>, <<miclatency,micLatency>>
|
||
|
||
[#miccalibration]
|
||
==== micCalibration
|
||
|
||
[source,text]
|
||
----
|
||
state, latency = micCalibration()
|
||
----
|
||
|
||
Come sta andando una calibrazione.
|
||
|
||
*Restituisce:* `state`, uno tra `"idle"`, `"running"`, `"done"` o `"failed"`
|
||
(sono stati risentiti meno di tre clic), e la latenza in millisecondi una volta
|
||
nota, altrimenti `nil`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<miccalibrate,micCalibrate>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
local state, latency = micCalibration()
|
||
if state == "done" then
|
||
message = "Your microphone runs " .. latency .. " ms behind."
|
||
elseif state == "failed" then
|
||
message = "I could not hear the clicks. Turn the speakers up and try again."
|
||
end
|
||
----
|
||
|
||
[#micclose]
|
||
==== micClose
|
||
|
||
[source,text]
|
||
----
|
||
micClose()
|
||
----
|
||
|
||
Smette di ascoltare, e con questo ferma anche qualsiasi registrazione e
|
||
monitoraggio. Il microfono si chiude anche quando il gioco finisce.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#micisopen]
|
||
==== micIsOpen
|
||
|
||
[source,text]
|
||
----
|
||
open = micIsOpen()
|
||
----
|
||
|
||
*Restituisce:* `true` finché un microfono è aperto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#miclatency]
|
||
==== micLatency
|
||
|
||
[source,text]
|
||
----
|
||
milliseconds = micLatency()
|
||
----
|
||
|
||
*Restituisce:* il tempo tra il momento in cui un suono viene prodotto e quello
|
||
in cui il gioco lo sente, come misurato su questa macchina da
|
||
<<miccalibrate,`micCalibrate`>> (in qualsiasi momento, in qualsiasi gioco) o
|
||
impostato da <<micsetlatency,`micSetLatency`>>; `nil` fino ad allora.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#miclevel]
|
||
==== micLevel
|
||
|
||
[source,text]
|
||
----
|
||
rms, peak = micLevel()
|
||
----
|
||
|
||
Quanto era forte l'ultimo istante sentito.
|
||
|
||
*Restituisce:* l'RMS e il picco dell'ultimo blocco di campioni, ciascuno da `0`
|
||
a `1`. Un tono costante a fondo scala dà circa `0.71` di RMS. Entrambi sono `0`
|
||
senza un microfono aperto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#miclist]
|
||
==== micList
|
||
|
||
[source,text]
|
||
----
|
||
names = micList()
|
||
----
|
||
|
||
*Restituisce:* un elenco dei nomi dei microfoni che questa macchina ha, per
|
||
<<micopen,`micOpen`>>. Viene ricavato da capo ogni volta, quindi compare anche
|
||
un microfono collegato dopo l'ultima chiamata.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#micmonitor]
|
||
==== micMonitor
|
||
|
||
[source,text]
|
||
----
|
||
ok = micMonitor(on [, volume])
|
||
----
|
||
|
||
Riproduce dagli altoparlanti ciò che il microfono sente, così chi canta sente
|
||
la propria voce sopra la musica. È spento finché non lo si chiede: sulla
|
||
maggior parte dell'hardware la voce torna con un ritardo abbastanza grande da
|
||
dare fastidio, e su un portatile gli altoparlanti rientrano nel microfono.
|
||
Tutto ciò che aspetta di essere riprodotto da più di un decimo di secondo viene
|
||
scartato invece di essere sentito in ritardo.
|
||
|
||
*Parametri:*
|
||
|
||
* `on` -- booleano.
|
||
* `volume` -- numero, da `0` a `1`; `1` se non indicato.
|
||
|
||
*Restituisce:* `true`, o `false` senza un microfono aperto o senza un altoparlante su cui riprodurre.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#micopen]
|
||
==== micOpen
|
||
|
||
[source,text]
|
||
----
|
||
opened = micOpen([name])
|
||
----
|
||
|
||
Comincia ad ascoltare, dal microfono indicato (da <<miclist,`micList`>>) o da
|
||
quello predefinito della macchina. Un microfono già aperto viene prima chiuso.
|
||
|
||
*Restituisce:* `true` quando si è aperto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
if not micOpen() then
|
||
message = "No microphone. This game needs one."
|
||
end
|
||
----
|
||
|
||
[#micpitch]
|
||
==== micPitch
|
||
|
||
[source,text]
|
||
----
|
||
hz, clarity = micPitch()
|
||
----
|
||
|
||
La nota cantata, trovata negli ultimi 43 millisecondi sentiti. Trova una
|
||
fondamentale tra 50 e 2000 Hz, che copre ogni voce cantata, e su un tono pulito
|
||
risponde con lo scarto di un hertz o due.
|
||
|
||
*Restituisce:* la fondamentale in Hz, e quanto chiaramente il suono è una sola
|
||
nota, da `0` a `1`. Senza nulla di intonato -- silenzio, un respiro, un colpo
|
||
di tosse, una folla -- `hz` è `0`. Trasforma gli Hz in un numero di nota con
|
||
`69 + 12 * math.log(hz / 440, 2)`: 69 è il La sopra il Do centrale, e ogni 1 è
|
||
un semitono.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
local hz, clarity = micPitch()
|
||
if hz > 0 and clarity > 0.8 then
|
||
local note = 69 + 12 * math.log(hz / 440, 2)
|
||
local off = note - targetNote -- semitoni crescenti (positivo) o calanti
|
||
if math.abs(off) < 0.5 then
|
||
score = score + 1
|
||
end
|
||
end
|
||
----
|
||
|
||
[#micread]
|
||
==== micRead
|
||
|
||
[source,text]
|
||
----
|
||
samples = micRead([max])
|
||
----
|
||
|
||
Ciò che è stato sentito dall'ultima chiamata, dal più vecchio.
|
||
|
||
*Parametri:*
|
||
|
||
* `max` -- numero, il massimo di campioni da restituire; tutto ciò che è in attesa se non indicato.
|
||
|
||
*Restituisce:* una stringa di campioni a 16 bit, due byte ciascuno, little
|
||
endian, un solo canale a 48000 al secondo: `#samples / 2` campioni. Leggine uno
|
||
con `string.unpack("<i2", samples, n * 2 - 1)`. Un gioco che non legge per più
|
||
di otto secondi perde i più vecchi.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#micrecordstart]
|
||
==== micRecordStart
|
||
|
||
[source,text]
|
||
----
|
||
started = micRecordStart(name)
|
||
----
|
||
|
||
Scrive tutto ciò che viene sentito da ora in poi in un file WAV (un canale, 16
|
||
bit, 48000 al secondo) fino a <<micrecordstop,`micRecordStop`>>. Il nome è
|
||
qualsiasi file che il gioco può scrivere, e finisce dove vanno gli altri file
|
||
scritti dal gioco.
|
||
|
||
*Restituisce:* `true` quando il file è stato aperto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#micrecordstop]
|
||
==== micRecordStop
|
||
|
||
[source,text]
|
||
----
|
||
micRecordStop()
|
||
----
|
||
|
||
Conclude la registrazione.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#micsetlatency]
|
||
==== micSetLatency
|
||
|
||
[source,text]
|
||
----
|
||
micSetLatency(milliseconds | nil)
|
||
----
|
||
|
||
Imposta la latenza del microfono di questa macchina, come fa un menu di
|
||
servizio a partire da una misura presa in un altro modo; `nil` la dimentica.
|
||
Viene conservata con le altre impostazioni della macchina.
|
||
|
||
*Parametri:*
|
||
|
||
* `milliseconds` -- numero, da `0` a `1000`, o `nil`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#midi]
|
||
=== MIDI
|
||
|
||
Due cose non collegate condividono questo nome, e conviene tenerle distinte.
|
||
|
||
Un **file MIDI** è musica, e si riproduce come qualsiasi altra musica: passa il
|
||
`.mid` a <<musicload,`musicLoad`>>. Singe lo sintetizza da un SoundFont, perché
|
||
un file MIDI contiene note e non suono. Singe non include alcun SoundFont --
|
||
uno buono pesa decine di megabyte, per un formato che quasi nessun gioco usa --
|
||
quindi indicane uno con `--soundfont`, mettine uno in `Singe/soundfont.sf2`
|
||
perché un gioco impacchettato porti il proprio, o lascia che Singe trovi quello
|
||
installato dalla tua distribuzione. La riga `SoundFont:` dell'intestazione della
|
||
traccia dice quale è stato usato. Senza un SoundFont da nessuna parte, caricare
|
||
un `.mid` fallisce e lo dice.
|
||
|
||
Le **porte MIDI** sono le chiamate qui sotto. Inviano e ricevono messaggi dal
|
||
vivo su una porta reale: un cabinato che pilota un modulo sonoro esterno, oppure
|
||
una tastiera, una fader box o una superficie di controllo lette come un altro
|
||
dispositivo di input. Qui niente sintetizza nulla, e niente tocca un file MIDI.
|
||
|
||
Le porte sono numerate da `0`. Non si apre nulla finché uno script non lo
|
||
chiede la prima volta -- una porta MIDI costa un descrittore di file e un giro
|
||
di configurazione del driver, e un gioco che non nomina mai il MIDI non lo paga
|
||
mai -- quindi è la prima di queste chiamate a fare l'inventario della macchina,
|
||
e <<midirescan,`midiRescan`>> cerca di nuovo un dispositivo collegato nel
|
||
frattempo. Possono essere aperte una porta di ingresso
|
||
e una porta di uscita alla volta. I canali vanno da `1` a `16` come li conta
|
||
una persona, non da `0` a `15` come sul cavo, e ogni altro valore va da `0` a
|
||
`127`. Su una macchina senza alcun MIDI -- nessuna porta, o nessuna libreria
|
||
MIDI installata -- i conteggi sono `0`, le chiamate di apertura rispondono
|
||
`false` e le chiamate di invio rispondono `false`; non fallisce nulla.
|
||
|
||
Linux raggiunge ogni porta attraverso il sequencer ALSA, che comprende sia le
|
||
interfacce hardware sia qualsiasi sintetizzatore o altro programma che ne
|
||
registri una. macOS usa CoreMIDI e Windows le chiamate MIDI multimediali, dove
|
||
il dispositivo di uscita `0` è di solito il sintetizzatore fornito dal sistema
|
||
stesso.
|
||
|
||
[#midicloseinput]
|
||
==== midiCloseInput
|
||
|
||
[source,text]
|
||
----
|
||
midiCloseInput()
|
||
----
|
||
|
||
Smette di ascoltare la porta di ingresso. <<onmidimessage,`onMidiMessage`>> non viene più chiamata. Chiudere una porta che non è aperta non fa nulla.
|
||
|
||
*Parametri:*
|
||
|
||
* Nessuno.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midiopeninput,midiOpenInput>>
|
||
|
||
[#midicloseoutput]
|
||
==== midiCloseOutput
|
||
|
||
[source,text]
|
||
----
|
||
midiCloseOutput()
|
||
----
|
||
|
||
Rilascia la porta di uscita. Da ora in poi le chiamate di invio rispondono `false`. Chiudere una porta che non è aperta non fa nulla.
|
||
|
||
*Parametri:*
|
||
|
||
* Nessuno.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midiopenoutput,midiOpenOutput>>
|
||
|
||
[#midicontrolchange]
|
||
==== midiControlChange
|
||
|
||
[source,text]
|
||
----
|
||
sent = midiControlChange(channel, control, value)
|
||
----
|
||
|
||
Muove un controller sulla porta di uscita aperta: il volume è il controller `7`, il pan è `10`, il pedale di sustain è `64`.
|
||
|
||
*Parametri:*
|
||
|
||
* `channel` -- numero; da `1` a `16`.
|
||
* `control` -- numero; da `0` a `127`.
|
||
* `value` -- numero; da `0` a `127`.
|
||
|
||
*Restituisce:* `true` quando il messaggio è partito.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midisend,midiSend>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Abbassa il volume del modulo mentre un personaggio parla.
|
||
midiControlChange(1, 7, 40)
|
||
----
|
||
|
||
[#midiinputcount]
|
||
==== midiInputCount
|
||
|
||
[source,text]
|
||
----
|
||
count = midiInputCount()
|
||
----
|
||
|
||
Quante porte di ingresso MIDI ha questa macchina. `0` quando non ce ne sono, o quando la piattaforma non ha alcun MIDI.
|
||
|
||
*Parametri:*
|
||
|
||
* Nessuno.
|
||
|
||
*Restituisce:* un numero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midiinputname,midiInputName>>
|
||
|
||
[#midiinputname]
|
||
==== midiInputName
|
||
|
||
[source,text]
|
||
----
|
||
name = midiInputName(index)
|
||
----
|
||
|
||
Il nome di una porta di ingresso, per una schermata delle impostazioni che lascia scegliere una porta al giocatore.
|
||
|
||
*Parametri:*
|
||
|
||
* `index` -- numero; da `0` a `midiInputCount() - 1`.
|
||
|
||
*Restituisce:* una stringa, o `nil` quando l'indice non corrisponde a nessuna porta.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midiinputcount,midiInputCount>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una schermata delle impostazioni che elenca le porte di ingresso, una per riga.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
for i = 0, midiInputCount() - 1 do
|
||
overlayPrint(1, i + 1, midiInputName(i))
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#midiisinputopen]
|
||
==== midiIsInputOpen
|
||
|
||
[source,text]
|
||
----
|
||
open = midiIsInputOpen()
|
||
----
|
||
|
||
Se è aperta una porta di ingresso.
|
||
|
||
*Parametri:*
|
||
|
||
* Nessuno.
|
||
|
||
*Restituisce:* `true` o `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midiopeninput,midiOpenInput>>
|
||
|
||
[#midiisoutputopen]
|
||
==== midiIsOutputOpen
|
||
|
||
[source,text]
|
||
----
|
||
open = midiIsOutputOpen()
|
||
----
|
||
|
||
Se è aperta una porta di uscita. Conviene chiederlo prima di una raffica di messaggi, perché altrimenti ognuno risponderebbe `false` per conto suo.
|
||
|
||
*Parametri:*
|
||
|
||
* Nessuno.
|
||
|
||
*Restituisce:* `true` o `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midiopenoutput,midiOpenOutput>>
|
||
|
||
[#midinoteoff]
|
||
==== midiNoteOff
|
||
|
||
[source,text]
|
||
----
|
||
sent = midiNoteOff(channel, key)
|
||
----
|
||
|
||
Rilascia una nota sulla porta di uscita aperta. Una nota lasciata accesa suona finché qualcosa non la ferma, quindi ogni `midiNoteOn` ha bisogno di una di queste.
|
||
|
||
*Parametri:*
|
||
|
||
* `channel` -- numero; da `1` a `16`.
|
||
* `key` -- numero; da `0` a `127`, dove `60` è il Do centrale.
|
||
|
||
*Restituisce:* `true` quando il messaggio è partito.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midinoteon,midiNoteOn>>
|
||
|
||
[#midinoteon]
|
||
==== midiNoteOn
|
||
|
||
[source,text]
|
||
----
|
||
sent = midiNoteOn(channel, key, velocity)
|
||
----
|
||
|
||
Suona una nota sulla porta di uscita aperta.
|
||
|
||
*Parametri:*
|
||
|
||
* `channel` -- numero; da `1` a `16`. Il canale `10` è quello delle percussioni su un dispositivo General MIDI.
|
||
* `key` -- numero; da `0` a `127`, dove `60` è il Do centrale.
|
||
* `velocity` -- numero; da `0` a `127`, quanto forte è stato colpito il tasto.
|
||
|
||
*Restituisce:* `true` quando il messaggio è partito.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midinoteoff,midiNoteOff>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Uno stacco musicale su un modulo esterno quando il giocatore viene colpito.
|
||
midiNoteOn(10, 49, 110)
|
||
midiNoteOff(10, 49)
|
||
----
|
||
|
||
[#midiopeninput]
|
||
==== midiOpenInput
|
||
|
||
[source,text]
|
||
----
|
||
opened = midiOpenInput(index)
|
||
----
|
||
|
||
Ascolta una porta di ingresso. <<onmidimessage,`onMidiMessage`>> viene poi chiamata per ogni messaggio che arriva. La porta di ingresso eventualmente aperta viene prima chiusa.
|
||
|
||
*Parametri:*
|
||
|
||
* `index` -- numero; da `0` a `midiInputCount() - 1`.
|
||
|
||
*Restituisce:* `true` quando la porta si è aperta.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midicloseinput,midiCloseInput>>, <<onmidimessage,onMidiMessage>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
if midiInputCount() > 0 then
|
||
midiOpenInput(0)
|
||
end
|
||
----
|
||
|
||
[#midiopenoutput]
|
||
==== midiOpenOutput
|
||
|
||
[source,text]
|
||
----
|
||
opened = midiOpenOutput(index)
|
||
----
|
||
|
||
Prende una porta di uscita per le chiamate di invio. La porta di uscita eventualmente aperta viene prima chiusa.
|
||
|
||
*Parametri:*
|
||
|
||
* `index` -- numero; da `0` a `midiOutputCount() - 1`.
|
||
|
||
*Restituisce:* `true` quando la porta si è aperta.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midicloseoutput,midiCloseOutput>>
|
||
|
||
[#midioutputcount]
|
||
==== midiOutputCount
|
||
|
||
[source,text]
|
||
----
|
||
count = midiOutputCount()
|
||
----
|
||
|
||
Quante porte di uscita MIDI ha questa macchina.
|
||
|
||
*Parametri:*
|
||
|
||
* Nessuno.
|
||
|
||
*Restituisce:* un numero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midioutputname,midiOutputName>>
|
||
|
||
[#midioutputname]
|
||
==== midiOutputName
|
||
|
||
[source,text]
|
||
----
|
||
name = midiOutputName(index)
|
||
----
|
||
|
||
Il nome di una porta di uscita.
|
||
|
||
*Parametri:*
|
||
|
||
* `index` -- numero; da `0` a `midiOutputCount() - 1`.
|
||
|
||
*Restituisce:* una stringa, o `nil` quando l'indice non corrisponde a nessuna porta.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midioutputcount,midiOutputCount>>
|
||
|
||
[#midipitchbend]
|
||
==== midiPitchBend
|
||
|
||
[source,text]
|
||
----
|
||
sent = midiPitchBend(channel, value)
|
||
----
|
||
|
||
Applica il pitch bend a un canale. `8192` è il centro, cioè nessuna variazione; fin dove arrivano `0` e `16383` è un'impostazione del dispositivo ricevente, di solito due semitoni.
|
||
|
||
*Parametri:*
|
||
|
||
* `channel` -- numero; da `1` a `16`.
|
||
* `value` -- numero; da `0` a `16383`.
|
||
|
||
*Restituisce:* `true` quando il messaggio è partito.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#midiprogramchange]
|
||
==== midiProgramChange
|
||
|
||
[source,text]
|
||
----
|
||
sent = midiProgramChange(channel, program)
|
||
----
|
||
|
||
Sceglie il suono che un canale suona. I numeri sono quelli del dispositivo ricevente; su un dispositivo General MIDI `0` è un pianoforte a coda.
|
||
|
||
*Parametri:*
|
||
|
||
* `channel` -- numero; da `1` a `16`.
|
||
* `program` -- numero; da `0` a `127`.
|
||
|
||
*Restituisce:* `true` quando il messaggio è partito.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#midirescan]
|
||
==== midiRescan
|
||
|
||
[source,text]
|
||
----
|
||
midiRescan()
|
||
----
|
||
|
||
Cerca di nuovo le porte, per un dispositivo collegato mentre il gioco è in esecuzione. Una porta aperta resta aperta. Gli indici sono stabili solo quanto l'elenco, quindi dopo questa chiamata rileggi i nomi invece di dare per scontato che la porta `2` sia ancora quella di prima.
|
||
|
||
*Parametri:*
|
||
|
||
* Nessuno.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midiinputcount,midiInputCount>>, <<midioutputcount,midiOutputCount>>
|
||
|
||
[#midisend]
|
||
==== midiSend
|
||
|
||
[source,text]
|
||
----
|
||
sent = midiSend(byte [, byte [, byte]])
|
||
sent = midiSend(string)
|
||
----
|
||
|
||
Qualsiasi messaggio, per quelli che le chiamate con un nome non coprono. Con dei numeri li invia così come sono, cioè un byte di stato e fino a due byte di dati -- e nota che il byte di stato porta il canale nei suoi quattro bit bassi, contando da `0`, a differenza di ogni altra chiamata qui. Con una stringa invia l'intera stringa, ed è così che parte un messaggio system exclusive.
|
||
|
||
*Parametri:*
|
||
|
||
* `byte` -- numero; da `0` a `255`, da uno a tre.
|
||
* `string` -- una stringa di byte, inviata senza modifiche.
|
||
|
||
*Restituisce:* `true` quando il messaggio è partito.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midinoteon,midiNoteOn>>, <<midicontrolchange,midiControlChange>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Tutte le note spente sul canale 1, che non ha una chiamata con un nome.
|
||
midiSend(0xB0, 123, 0)
|
||
|
||
-- Un messaggio system exclusive, byte per byte.
|
||
midiSend(string.char(0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7))
|
||
----
|
||
|
||
[#model]
|
||
=== Modello
|
||
|
||
I modelli sono file binari glTF 2.0 (`.glb`) caricati una volta e piazzati
|
||
quante volte vuoi. Un modello deve essere autosufficiente (buffer e immagini
|
||
dentro il file); uno che fa riferimento a un file accanto a sé viene rifiutato.
|
||
Il caricamento dà un handle intero; `modelInstance` costruisce i nodi del
|
||
modello nella scena e ne restituisce la radice, che ogni chiamata `node*` e
|
||
`animation*` accetta poi. Posizioni e dimensioni dentro un modello sono in
|
||
unità del mondo, come le memorizza il file; il nodo radice scala l'intera
|
||
istanza. Vedi <<scenes3d,Scene 3D>>.
|
||
|
||
[#modelload]
|
||
==== modelLoad
|
||
|
||
[source,text]
|
||
----
|
||
model = modelLoad(name)
|
||
----
|
||
|
||
Legge un `.glb` attraverso la stessa ricerca di ogni altra risorsa (cartella dei giochi, cartella dei dati, database impacchettato), carica una volta sola sulla GPU le sue mesh e i suoi materiali, e conserva il suo albero di nodi, le skin e le animazioni per crearne istanze. Chiamala una volta all'avvio e crea istanze dell'handle tutte le volte che serve; caricare due volte lo stesso file crea due copie sulla GPU. Richiede la scena 3D, quindi su una macchina senza una GPU adatta fallisce come qualsiasi altra chiamata 3D. Un file non valido termina lo script con un messaggio che nomina il problema: il file non si è potuto leggere, non è glTF, fa riferimento a un buffer o a un'immagine esterni, non supera la validazione, contiene una primitiva che non si è potuta caricare sulla GPU, ha una skin con più di `128` giunti, o ha una mesh con skin che nomina un giunto numerato `128` o più. Una mesh con skin che nomina un giunto oltre le skin che la usano viene caricata, come pure un'animazione i cui keyframe non si possono leggere (quel campionatore viene saltato); entrambi i casi vengono annotati nella traccia del programma. Una texture che non si può decodificare, o un'immagine KTX2 mancante, non fa fallire il caricamento; il materiale resta senza quella mappa e la traccia del programma (`--program`) dice quale.
|
||
|
||
*Restituisce:* L'handle del modello, un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<modelinstance,modelInstance>>, <<modelgetanimations,modelGetAnimations>>, <<modeldelete,modelDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Carica una volta, piazza tre volte.
|
||
treeModel = modelLoad(DIR .. "Models/Tree.glb")
|
||
for i = 1, 3 do
|
||
local tree = modelInstance(treeModel)
|
||
nodeSetPosition(tree, i * 3 - 6, 0, -4)
|
||
nodeSetRotation(tree, 0, i * 70, 0)
|
||
end
|
||
----
|
||
|
||
[#modeldelete]
|
||
==== modelDelete
|
||
|
||
[source,text]
|
||
----
|
||
modelDelete(model)
|
||
----
|
||
|
||
Libera le mesh, i materiali e i dati analizzati del modello. Ogni istanza creata da esso viene dimenticata nello stesso momento: i suoi nodi restano nella scena (posizione, genitore e figli intatti) ma non disegnano nulla, le sue animazioni si fermano dove sono, e una successiva chiamata `animation*` sulla sua radice termina lo script perché il nodo non è più un'istanza di modello. Elimina le radici delle istanze con `nodeDelete` quando non vuoi più nemmeno i nodi. Un handle che non è un modello caricato termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<modelload,modelLoad>>, <<modelinstance,modelInstance>>, <<nodedelete,nodeDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Uscita dal livello dell'hangar: via i suoi oggetti di scena e il modello che c'è dietro.
|
||
function unloadHangar()
|
||
for _, crate in ipairs(hangarCrates) do
|
||
nodeDelete(crate)
|
||
end
|
||
hangarCrates = {}
|
||
modelDelete(crateModel)
|
||
crateModel = nil
|
||
end
|
||
----
|
||
|
||
[#modelgetanimations]
|
||
==== modelGetAnimations
|
||
|
||
[source,text]
|
||
----
|
||
names = modelGetAnimations(model)
|
||
----
|
||
|
||
Elenca le clip di animazione del modello nell'ordine del file come tabella di stringhe, così `names[3]` è la clip che `animationPlay(node, 3)` riproduce. Una clip lasciata senza nome dall'esportatore dà una stringa vuota; si può comunque riprodurre per numero. Un modello senza animazioni dà una tabella vuota. Un handle che non è un modello caricato termina lo script.
|
||
|
||
*Restituisce:* Una tabella di stringhe indicizzata da `1`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<animationplay,animationPlay>>, <<modelload,modelLoad>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Trova le clip per nome una volta, così il resto del gioco usa i numeri.
|
||
guardModel = modelLoad(DIR .. "Models/Guard.glb")
|
||
guardClips = {}
|
||
for index, name in ipairs(modelGetAnimations(guardModel)) do
|
||
guardClips[name] = index
|
||
end
|
||
if guardClips["Idle"] == nil then
|
||
debugPrint("Guard.glb has no Idle clip")
|
||
end
|
||
----
|
||
|
||
[#modelinstance]
|
||
==== modelInstance
|
||
|
||
[source,text]
|
||
----
|
||
node = modelInstance(model)
|
||
node = modelInstance(model, parent)
|
||
----
|
||
|
||
Costruisce l'albero di nodi del modello sotto un nuovo nodo radice, a sua volta sotto `parent` o sotto la radice della scena. La radice è un nodo semplice: posizionala, ruotala, scalala o dalle un genitore come a qualsiasi altro nodo, ed eliminala con `nodeDelete` per rimuovere l'intera istanza. I nodi del modello prendono i loro nomi glTF (o il nome della loro mesh quando il nodo non ha nome), così `nodeFind(name, node)` li raggiunge; una mesh con più materiali diventa un nodo figlio per ogni parte; le luci nel file diventano luci della scena con il colore, l'intensità, la portata e il cono del file; le skin vengono collegate e i pesi di morph impostati. I nodi restano nella posa del file finché non parte un'animazione. Un handle che non è un modello caricato o un `parent` che non è un nodo termina lo script, come pure l'esaurimento dei nodi.
|
||
|
||
*Restituisce:* L'handle del nodo radice dell'istanza.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<modelload,modelLoad>>, <<nodefind,nodeFind>>, <<animationplay,animationPlay>>, <<nodedelete,nodeDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una torretta sullo scafo del carro armato, così gira con lo scafo e ruota anche da sola.
|
||
tank = modelInstance(hullModel)
|
||
turret = modelInstance(turretModel, tank)
|
||
nodeSetPosition(turret, 0, 1.1, -0.3)
|
||
barrel = nodeFind("Barrel", turret)
|
||
|
||
function onOverlayUpdate()
|
||
nodeSetRotation(turret, 0, aimYaw, 0)
|
||
nodeSetRotation(barrel, aimPitch, 0, 0)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#mouse]
|
||
=== Mouse
|
||
|
||
Singe legge fino a quattro mouse, e tratta le pistole ottiche come mouse. Nella modalità predefinita a mouse singolo ogni dispositivo di puntamento muove un unico cursore riportato come mouse `0`; `mouseSetMode(MOUSE_MANY)` distingue i dispositivi, cosa che serve a un cabinato con pistole ottiche per due giocatori. Le posizioni arrivano in coordinate dell'overlay, non in pixel dello schermo, attraverso `onMouseMoved` (vedi <<enginecallbacks,Callback del motore>>) e su richiesta da `mouseGetPosition`. I pulsanti del mouse e la rotellina sono interruttori come i tasti: arrivano a `onInputPressed` e `onInputReleased` quando `controls.cfg` assegna un codice delle tabelle da `MOUSE_0` a `MOUSE_3`, e le impostazioni predefinite distribuite assegnano il pulsante sinistro a `SWITCH_BUTTON3`, il destro a `SWITCH_BUTTON1` e quello centrale a `SWITCH_BUTTON2`. Il motore parte con il mouse catturato, e attivo a meno che non sia stato dato `--nomouse` o non sia stato trovato alcun mouse.
|
||
|
||
[#mousedisable]
|
||
==== mouseDisable
|
||
|
||
[source,text]
|
||
----
|
||
mouseDisable()
|
||
----
|
||
|
||
Disattiva l'input del mouse, esattamente come fa `mouseSetEnabled(false)`. È una funzione definita in `Framework.singe`, mantenuta perché i giochi più vecchi funzionino senza modifiche.
|
||
|
||
*Note:* Alias storico.
|
||
|
||
WARNING: Il codice nuovo dovrebbe chiamare <<mousesetenabled,mouseSetEnabled>> con `false`.
|
||
|
||
*Disponibile da:* 1.18 (RDG)
|
||
*Vedi anche:* <<mousesetenabled,mouseSetEnabled>>, <<mouseenable,mouseEnable>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Codice di un gioco più vecchio; la forma moderna è mouseSetEnabled(false).
|
||
function cutsceneBegin()
|
||
mouseDisable()
|
||
discSearch(CUTSCENE_START)
|
||
end
|
||
----
|
||
|
||
[#mouseenable]
|
||
==== mouseEnable
|
||
|
||
[source,text]
|
||
----
|
||
mouseEnable()
|
||
----
|
||
|
||
Attiva l'input del mouse, esattamente come fa `mouseSetEnabled(true)`, e come quella lascia il mouse spento quando Singe è stato avviato con `--nomouse`. È una funzione definita in `Framework.singe`, mantenuta perché i giochi più vecchi funzionino senza modifiche.
|
||
|
||
*Note:* Alias storico.
|
||
|
||
WARNING: Il codice nuovo dovrebbe chiamare <<mousesetenabled,mouseSetEnabled>> con `true`.
|
||
|
||
*Disponibile da:* 1.18 (RDG)
|
||
*Vedi anche:* <<mousesetenabled,mouseSetEnabled>>, <<mousedisable,mouseDisable>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Codice di un gioco più vecchio; la forma moderna è mouseSetEnabled(true).
|
||
function cutsceneEnd()
|
||
mouseEnable()
|
||
discSearch(LEVEL_START)
|
||
end
|
||
----
|
||
|
||
[#mousegetname]
|
||
==== mouseGetName
|
||
|
||
[source,text]
|
||
----
|
||
name = mouseGetName(mouse)
|
||
----
|
||
|
||
Il nome che il driver del mouse dà a un dispositivo, o `nil` quando non c'è nessun dispositivo con quell'indice. Su una schermata di servizio permette di distinguere due pistole su un cabinato, cosa che altrimenti è un tirare a indovinare quando si presentano come mouse `0` e `1`. Un indice non compreso tra `0` e `3` termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `mouse` -- indice del dispositivo, da `0` a `3`.
|
||
|
||
*Restituisce:* stringa, o `nil`.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<mousehowmany,mouseHowMany>>, <<mousesetmode,mouseSetMode>>, <<controllergetname,controllerGetName>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Mostra i nomi delle pistole sulla schermata di calibrazione.
|
||
mouseSetMode(MOUSE_MANY)
|
||
for device = 0, mouseHowMany() - 1 do
|
||
fontPrint(20, 20 + device * 18, string.format("Gun %d: %s", device + 1, mouseGetName(device)))
|
||
end
|
||
----
|
||
|
||
[#mousegetpadding]
|
||
==== mouseGetPadding
|
||
|
||
[source,text]
|
||
----
|
||
padding = mouseGetPadding()
|
||
----
|
||
|
||
Estensione di Hypseus, sempre `0`, per il motivo spiegato in <<controllergetpadding,`controllerGetPadding`>>. Hypseus aggiunge questo scostamento ai codici di input che i suoi mouse riportano in modalità relativa; i codici di Singe non hanno alcuno scostamento del genere, quindi non c'è nulla da sottrarre.
|
||
|
||
*Restituisce:* un intero, sempre `0`.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<controllergetpadding,controllerGetPadding>>
|
||
|
||
[#mousegetposition]
|
||
==== mouseGetPosition
|
||
|
||
[source,text]
|
||
----
|
||
x, y = mouseGetPosition(index)
|
||
----
|
||
|
||
Restituisce l'ultima posizione riportata di un mouse in coordinate dell'overlay, gli stessi valori portati dall'ultimo `onMouseMoved` per quel dispositivo. In modalità a mouse singolo si muove solo il mouse `0`; in modalità a più mouse ogni dispositivo ha la propria posizione, che parte dal centro del video. La posizione smette di aggiornarsi mentre l'input del mouse è disattivato. Interrogala da `onOverlayUpdate` o da `singeMain` quando è più semplice che seguire la callback. Un indice non compreso tra `0` e `3` termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `index` -- indice del mouse, da `0` a `3`.
|
||
|
||
*Restituisce:* due interi, `x` e `y`, in coordinate dell'overlay.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<mousehowmany,mouseHowMany>>, <<onmousemoved,onMouseMoved>>, <<singewantscrosshairs,singeWantsCrosshairs>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Disegna il mirino dove punta la pistola.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
drawTargets()
|
||
if singeWantsCrosshairs() then
|
||
local x, y = mouseGetPosition(0)
|
||
spriteDraw(crosshair, x, y, true)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#mousehowmany]
|
||
==== mouseHowMany
|
||
|
||
[source,text]
|
||
----
|
||
count = mouseHowMany()
|
||
----
|
||
|
||
Restituisce il numero di mouse che il motore ha trovato all'avvio, da `0` a `4`. I dispositivi collegati dopo non vengono contati. Quando non ne è stato trovato nessuno il mouse resta disattivato. Usala per decidere se offrire un secondo giocatore con pistola ottica.
|
||
|
||
*Restituisce:* intero, da `0` a `4`.
|
||
|
||
*Disponibile da:* 1.18 (RDG)
|
||
*Vedi anche:* <<mousesetmode,mouseSetMode>>, <<mousegetposition,mouseGetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Due pistole presenti: dai un cursore a ogni giocatore.
|
||
if mouseHowMany() >= 2 then
|
||
mouseSetMode(MOUSE_MANY)
|
||
players = 2
|
||
else
|
||
players = 1
|
||
end
|
||
----
|
||
|
||
[#mousesetcaptured]
|
||
==== mouseSetCaptured
|
||
|
||
[source,text]
|
||
----
|
||
mouseSetCaptured(captured)
|
||
----
|
||
|
||
Cattura il mouse, confinandolo nella finestra di Singe e nascondendo il cursore, oppure lo rilascia e mostra di nuovo il cursore. Il motore cattura il mouse all'avvio, quindi la maggior parte dei giochi non la chiama mai; un editor di livelli in finestra o un gioco che vuole visibile il cursore del desktop lo rilascia. L'interruttore assegnato a `INPUT_GRAB` in `controls.cfg` commuta lo stesso stato dalla tastiera.
|
||
|
||
*Parametri:*
|
||
|
||
* `captured` -- booleano; `true` cattura e nasconde, `false` rilascia e mostra.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<mousesetenabled,mouseSetEnabled>>, <<mousegetposition,mouseGetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- L'editor di livelli vuole il cursore del desktop; durante il gioco torna nascosto.
|
||
function editorToggle()
|
||
editing = not editing
|
||
mouseSetCaptured(not editing)
|
||
end
|
||
----
|
||
|
||
[#mousesetenabled]
|
||
==== mouseSetEnabled
|
||
|
||
[source,text]
|
||
----
|
||
mouseSetEnabled(enabled)
|
||
----
|
||
|
||
Attiva o disattiva l'input del mouse. Quando è disattivato, gli eventi di movimento, dei pulsanti e della rotellina vengono scartati prima di arrivare allo script: `onMouseMoved` non viene più chiamata, i pulsanti del mouse non producono più interruttori, e `mouseGetPosition` continua a restituire l'ultima posizione che aveva. Il cursore stesso si muove ancora. L'attivazione rispetta `--nomouse`; quando Singe è stato avviato con quell'opzione il mouse resta spento. Un argomento non booleano termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<mousesetcaptured,mouseSetCaptured>>, <<mousesetmode,mouseSetMode>>, <<mouseenable,mouseEnable>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ignora la pistola mentre gira il ciclo della modalità attract.
|
||
function attractBegin()
|
||
mouseSetEnabled(false)
|
||
discSearch(ATTRACT_START)
|
||
end
|
||
|
||
function attractEnd()
|
||
mouseSetEnabled(true)
|
||
end
|
||
----
|
||
|
||
[#mousesetmode]
|
||
==== mouseSetMode
|
||
|
||
[source,text]
|
||
----
|
||
mouseSetMode(mode)
|
||
----
|
||
|
||
Sceglie come vengono letti i mouse. In `MOUSE_SINGLE` (il valore predefinito) gli eventi del puntatore della finestra muovono un unico cursore riportato come dispositivo `0`, con le posizioni assolute e il movimento relativo convertiti entrambi in coordinate dell'overlay. In `MOUSE_MANY` ogni dispositivo fisico viene letto separatamente: il movimento relativo viene integrato in una posizione per dispositivo limitata all'area del video, i dispositivi assoluti come le tavolette e alcune pistole ottiche vengono mappati sul video, e `onMouseMoved` riporta l'indice del dispositivo. Entrambe le modalità consegnano come interruttori gli stessi cinque pulsanti e la rotellina verticale. `SINGLE_MOUSE` e `MANY_MOUSE` sono accettati come grafie più vecchie; qualsiasi altro valore termina lo script. `--manymouse` è una decisione dell'utente e ha la precedenza su questa chiamata: se è indicato, la modalità è `MOUSE_MANY` fin dall'inizio e `mouseSetMode(MOUSE_SINGLE)` non la disattiva, così un cabinato con due pistole si può far funzionare dall'esterno di un gioco che non lo chiede mai.
|
||
|
||
*Parametri:*
|
||
|
||
* `mode` -- `MOUSE_SINGLE` (`100`) o `MOUSE_MANY` (`200`).
|
||
|
||
*Disponibile da:* 1.18 (RDG)
|
||
*Vedi anche:* <<mousehowmany,mouseHowMany>>, <<onmousemoved,onMouseMoved>>, <<mousegetposition,mouseGetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Gioco cooperativo con pistole ottiche: ogni pistola guida il proprio cursore.
|
||
mouseSetMode(MOUSE_MANY)
|
||
|
||
function onMouseMoved(x, y, xr, yr, which)
|
||
if which < 2 then
|
||
cursorX[which + 1] = x
|
||
cursorY[which + 1] = y
|
||
end
|
||
end
|
||
----
|
||
|
||
[#musicref]
|
||
=== Musica
|
||
|
||
Musica di sottofondo, caricata e riprodotta separatamente dagli effetti sonori e con un proprio volume. Hypseus fa girare la musica su un secondo mixer proprio per questo motivo, mentre Singe le dà invece un proprio guadagno: <<musicsetvolume,`musicSetVolume`>> non sposta mai un effetto sonoro e <<soundsetvolume,`soundSetVolume`>> non sposta mai la musica.
|
||
|
||
Un handle di musica è un tipo di handle a sé, né un handle di suono né un canale: un handle è un brano, e si riproduce, si mette in pausa, riprende e si ferma per conto suo. Non c'è un limite di canali, a differenza dei sedici canali degli effetti sonori.
|
||
|
||
La scala del volume è quella di Hypseus, da `0` a `128`, quasi il doppio della scala da `0` a `63` che usa la famiglia `sound`, così un gioco scritto per Hypseus mantiene i propri numeri. La musica parte forte quanto gli effetti sonori, al livello impostato da `--volume_nonvldp`; Hypseus invece fa partire il suo mixer della musica a un terzo, valore che ogni gioco Hypseus sovrascrive subito con `musicSetVolume(soundGetVolume() * 2)`.
|
||
|
||
Qui si legge ogni formato che legge la famiglia dei suoni: WAV, Ogg Vorbis, Opus, FLAC, MP3, WavPack e gli altri. L'intestazione della traccia scritta da `--program` e `--trace` elenca esattamente quali decoder contiene la build, nella sua riga `Audio:`.
|
||
|
||
Mettere in pausa il motore (il tasto di pausa, o `singeSetPauseFlag`) mette in pausa anche la musica, e riprendere la fa ripartire; un ricaricamento ferma e libera ogni brano.
|
||
|
||
[#musicgetposition]
|
||
==== musicGetPosition
|
||
|
||
[source,text]
|
||
----
|
||
seconds = musicGetPosition(id)
|
||
----
|
||
|
||
A che punto di un brano in riproduzione si trova chi ascolta, in secondi: la posizione del mixer stesso, meno ciò che è ancora in viaggio verso gli altoparlanti -- la coda del dispositivo, la calibrazione audio della macchina e il ritardo audio del gioco -- così un'immagine sincronizzata su di essa arriva insieme al suono. È l'orologio su cui animare quando è la musica a guidare, come in una demo o in un gioco ritmico, e resta al passo con la canzone per quanto a lungo suoni, mentre contare fotogrammi o tick va alla deriva. Il mixer sposta la sua posizione un buffer alla volta, quindi tra uno spostamento e l'altro la risposta avanza con l'orologio, mai per più di 50 ms, e si muove in modo fluido a ogni fotogramma. Un brano in pausa resta fermo. Un brano che non è in riproduzione risponde `-1`.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; un handle di musica.
|
||
|
||
*Restituisce:* numero; secondi dall'inizio del brano, o `-1`.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<musicplay,musicPlay>>, <<singegetaudiolatency,singeGetAudioLatency>>, <<singegetaudiocalibration,singeGetAudioCalibration>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un lampo a ogni battito di una canzone a 120 battiti al minuto, sul battito così come lo si sente.
|
||
function onOverlayUpdate()
|
||
colorBackground(0, 0, 0)
|
||
overlayClear()
|
||
local t = musicGetPosition(song)
|
||
if t >= 0 and (t * 2) % 1 < 0.1 then
|
||
colorBackground(255, 255, 255, 120)
|
||
overlayClear()
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#musicisplaying]
|
||
==== musicIsPlaying
|
||
|
||
[source,text]
|
||
----
|
||
playing = musicIsPlaying([id])
|
||
----
|
||
|
||
Estensione di Hypseus. Se quel brano è in riproduzione, o -- senza handle -- se c'è della musica in riproduzione. Un brano in pausa non è in riproduzione.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero facoltativo; un handle di musica.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<musicplay,musicPlay>>, <<musicpause,musicPause>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
if not musicIsPlaying(theme) then
|
||
musicPlay(theme, -1)
|
||
end
|
||
----
|
||
|
||
[#musicload]
|
||
==== musicLoad
|
||
|
||
[source,text]
|
||
----
|
||
id = musicLoad(filename)
|
||
----
|
||
|
||
Estensione di Hypseus. Carica un brano musicale attraverso il file system virtuale e ne restituisce l'handle. L'intero file viene decodificato subito, come un suono, quindi un brano lungo costa memoria, tranne un chiptune che non dice quanto dura, o ciò che decodificato supererebbe i 512 MB, che vengono decodificati man mano che suonano; l'handle è valido fino a <<musicunload,`musicUnload`>> o a un ricaricamento.
|
||
|
||
Un file mancante, o che nessun decoder compilato nella build sa leggere, termina lo script, come fa <<soundload,`soundLoad`>>.
|
||
|
||
*Parametri:*
|
||
|
||
* `filename` -- stringa; un percorso che il file system virtuale può raggiungere.
|
||
|
||
*Restituisce:* numero, l'handle di musica.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<musicplay,musicPlay>>, <<musicunload,musicUnload>>, <<soundload,soundLoad>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
local theme = musicLoad("music/attract.mp3")
|
||
musicSetVolume(96)
|
||
musicPlay(theme, -1)
|
||
----
|
||
|
||
[#musicpause]
|
||
==== musicPause
|
||
|
||
[source,text]
|
||
----
|
||
musicPause([id])
|
||
----
|
||
|
||
Estensione di Hypseus. Mette in pausa quel brano dove si trova, o tutta la musica quando non viene dato un handle. <<musicresume,`musicResume`>> la fa ripartire.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero facoltativo; un handle di musica.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<musicresume,musicResume>>, <<musicstop,musicStop>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il giocatore ha aperto la mappa; sospendi la musica invece di farla ripartire da capo.
|
||
musicPause()
|
||
----
|
||
|
||
[#musicplay]
|
||
==== musicPlay
|
||
|
||
[source,text]
|
||
----
|
||
id = musicPlay(id [, loops])
|
||
----
|
||
|
||
Estensione di Hypseus. Riproduce un brano dall'inizio e ne restituisce l'handle, che è il modo in cui i giochi di Hypseus lo conservano (`sndHandle = musicPlay(music)`) per fermare il pezzo più tardi. `loops` significa ciò che significa per <<soundplay,`soundPlay`>>: `0`, il valore predefinito, riproduce una volta, `-1` all'infinito, e `N` ripete altre `N` volte. Il conteggio viene limitato tra `-1` e `64`, come lo limita Hypseus, così un gioco che chiede di più ottiene ciò che avrebbe ottenuto lì.
|
||
|
||
Riprodurre un brano già in riproduzione lo fa ripartire dall'inizio.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; un handle di musica.
|
||
* `loops` -- numero facoltativo; da `-1` a `64`. Predefinito `0`.
|
||
|
||
*Restituisce:* numero; l'handle ricevuto.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<musicstop,musicStop>>, <<musicsetvolume,musicSetVolume>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
musicPlay(theme, -1) -- Gira e rigira finché qualcosa non lo ferma.
|
||
----
|
||
|
||
[#musicresume]
|
||
==== musicResume
|
||
|
||
[source,text]
|
||
----
|
||
musicResume([id])
|
||
----
|
||
|
||
Estensione di Hypseus. Riprende un brano in pausa, o tutta la musica in pausa quando non viene dato un handle.
|
||
|
||
La forma senza argomenti di Hypseus non fa proprio nulla; quella di Singe riprende tutto, che è ciò che fa coppia con <<musicpause,`musicPause`>>.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero facoltativo; un handle di musica.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<musicpause,musicPause>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
musicResume()
|
||
----
|
||
|
||
[#musicsetvolume]
|
||
==== musicSetVolume
|
||
|
||
[source,text]
|
||
----
|
||
volume = musicSetVolume(volume)
|
||
----
|
||
|
||
Estensione di Hypseus. Imposta in un colpo solo il guadagno di tutta la musica, sulla scala da `0` a `128` di Hypseus, e restituisce il livello applicato. Non sposta nient'altro che la musica: gli effetti sonori mantengono ciò che gli ha dato <<soundsetvolume,`soundSetVolume`>>, e `soundSetVolume` lascia stare la musica.
|
||
|
||
Un livello non compreso tra `0` e `128` termina lo script. Il livello si applica ai brani già in riproduzione e a quelli caricati dopo, e torna al livello iniziale con un ricaricamento.
|
||
|
||
*Parametri:*
|
||
|
||
* `volume` -- numero; da `0` a `128`.
|
||
|
||
*Restituisce:* numero, il livello impostato.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<soundsetvolume,soundSetVolume>>, <<musicplay,musicPlay>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ciò che scrive un gioco Hypseus: il livello del suono, raddoppiato sulla scala della musica.
|
||
musicSetVolume(soundGetVolume() * 2)
|
||
----
|
||
|
||
[#musicstop]
|
||
==== musicStop
|
||
|
||
[source,text]
|
||
----
|
||
musicStop([id [, fadeMs]])
|
||
----
|
||
|
||
Estensione di Hypseus. Ferma quel brano, o tutta la musica quando non viene dato un handle (un handle `nil` conta come nessuno). `fadeMs` lo sfuma in uscita in quel numero di millisecondi invece di troncarlo; `0`, il valore predefinito, lo ferma subito. Una dissolvenza negativa termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero facoltativo; un handle di musica.
|
||
* `fadeMs` -- numero facoltativo; i millisecondi in cui sfumare in uscita. Predefinito `0`.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<musicplay,musicPlay>>, <<musicpause,musicPause>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
musicStop(theme, 1500) -- Sfuma via in un secondo e mezzo.
|
||
----
|
||
|
||
[#musicunload]
|
||
==== musicUnload
|
||
|
||
[source,text]
|
||
----
|
||
musicUnload(id)
|
||
----
|
||
|
||
Estensione di Hypseus. Ferma il brano e lo libera. Dopo, l'handle non è più valido e usarlo termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; un handle di musica.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<musicload,musicLoad>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
musicUnload(theme)
|
||
----
|
||
|
||
[#nav]
|
||
=== Navigazione
|
||
|
||
Navmesh precalcolate dalla geometria della scena, e gli agenti della folla che le percorrono (vedi <<scenes3d,Scene 3D>>, Navigazione). `navNew` e `navLoad` restituiscono un handle di navmesh e `navAgentNew` un handle di agente; entrambi sono interi, controllati a ogni chiamata, e uno non valido termina lo script. Le posizioni sono in unità del mondo, le velocità in unità al secondo e le pendenze in gradi. Fino a otto navmesh alla volta, 128 agenti su ciascuna e 256 in tutto. Le interrogazioni e gli agenti hanno bisogno di una navmesh che `navBuild` abbia precalcolato o che `navLoad` abbia letto; prima di allora le interrogazioni restituiscono `nil`, `navDraw` non disegna nulla e `navAgentNew` termina lo script.
|
||
|
||
[#navaddnode]
|
||
==== navAddNode
|
||
|
||
[source,text]
|
||
----
|
||
navAddNode(nav, node)
|
||
----
|
||
|
||
Aggiunge la mesh del nodo, e la mesh di ogni nodo sotto di esso, alla geometria da cui verrà precalcolata la navmesh, così come si trovano nel mondo in questo momento, quindi posizionali prima. Il nodo radice di un'istanza di modello aggiunge l'intero modello, e un livello costruito con le primitive si può aggiungere nodo per nodo. Conta solo la geometria delle mesh: corpi, sprite e testo non aggiungono nulla. Termina lo script dopo che la navmesh è stata costruita.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navnew,navNew>>, <<navbuild,navBuild>>, <<modelinstance,modelInstance>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il terreno e i muri sono geometria percorribile; le casse no, si muovono.
|
||
local nav = navNew(0.4, 1.8, 45, 0.4)
|
||
navAddNode(nav, ground)
|
||
for _, wall in ipairs(walls) do
|
||
navAddNode(nav, wall)
|
||
end
|
||
navAddNode(nav, modelInstance(towerModel))
|
||
navBuild(nav)
|
||
----
|
||
|
||
[#navagentdelete]
|
||
==== navAgentDelete
|
||
|
||
[source,text]
|
||
----
|
||
navAgentDelete(agent)
|
||
----
|
||
|
||
Toglie l'agente dalla navmesh e ne libera l'handle; il nodo resta dov'è e non viene più spostato. Eliminare la navmesh con `navDelete` fa questo per ogni agente che vi si trova. Un handle non valido termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navagentnew,navAgentNew>>, <<navdelete,navDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una guardia che muore smette di navigare; da qui in poi il modello passa al ragdoll.
|
||
function killGuard(guard)
|
||
navAgentDelete(guard.agent)
|
||
guard.agent = nil
|
||
ragdollActivate(guard.model)
|
||
ragdollApplyImpulse(guard.model, "b_Spine02_03", 0, 20, -30)
|
||
end
|
||
----
|
||
|
||
[#navagentgetvelocity]
|
||
==== navAgentGetVelocity
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z = navAgentGetVelocity(agent)
|
||
----
|
||
|
||
Come la folla sta muovendo l'agente in questo fotogramma, in unità del mondo al secondo lungo ciascun asse; la sua lunghezza arriva fino alla velocità dell'agente e scende a zero quando si ferma. Serve per scegliere tra un'animazione da fermo, di camminata e di corsa, o per adattare il ciclo di camminata al terreno percorso.
|
||
|
||
*Restituisce:* Tre numeri.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navagentmoveto,navAgentMoveTo>>, <<animationplay,animationPlay>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
for agent, fox in pairs(foxes) do
|
||
local vx, vy, vz = navAgentGetVelocity(agent)
|
||
local speed = math.sqrt(vx * vx + vz * vz)
|
||
local clip = (speed < 0.1) and "Survey" or ((speed < 2.5) and "Walk" or "Run")
|
||
if fox.clip ~= clip then
|
||
animationPlay(fox.node, clip, true, 1, 0.2)
|
||
fox.clip = clip
|
||
end
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#navagentisarrived]
|
||
==== navAgentIsArrived
|
||
|
||
[source,text]
|
||
----
|
||
arrived = navAgentIsArrived(agent)
|
||
----
|
||
|
||
Se l'agente ha raggiunto l'ultima destinazione che gli ha dato `navAgentMoveTo`: entro una volta e mezza il suo raggio dalla destinazione e quasi fermo. Diventa `true` nel momento in cui scatta `onNavArrived` e resta `true` fino al successivo `navAgentMoveTo`; è `false` per un agente che non è mai stato mandato da nessuna parte o che è stato fermato lungo la strada.
|
||
|
||
*Restituisce:* Vero o falso.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<onnavarrived,onNavArrived>>, <<navagentmoveto,navAgentMoveTo>>, <<navagentstop,navAgentStop>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La pattuglia aspetta un po' a ogni postazione prima di proseguire.
|
||
function onOverlayUpdate()
|
||
if navAgentIsArrived(patrol) then
|
||
waitFrames = waitFrames + 1
|
||
if waitFrames > 180 then
|
||
post = post % #posts + 1
|
||
waitFrames = 0
|
||
navAgentMoveTo(patrol, posts[post][1], posts[post][2], posts[post][3])
|
||
end
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#navagentmoveto]
|
||
==== navAgentMoveTo
|
||
|
||
[source,text]
|
||
----
|
||
ok = navAgentMoveTo(agent, x, y, z)
|
||
----
|
||
|
||
Manda l'agente a camminare verso il punto della navmesh più vicino a `x, y, z`, cercandolo fino a 2 unità di lato e 4 sopra o sotto. La folla pianifica il percorso nei fotogrammi successivi e guida l'agente attorno agli altri agenti lungo la strada; `onNavArrived` scatta quando arriva. Restituisce `false`, e lascia l'agente com'era, quando nessun punto della navmesh è abbastanza vicino o la folla rifiuta la richiesta. Una nuova destinazione sostituisce la vecchia in qualsiasi momento.
|
||
|
||
*Parametri:*
|
||
|
||
* `agent` -- l'handle restituito da `navAgentNew`.
|
||
* `x`, `y`, `z` -- la destinazione in unità del mondo; viene agganciata alla navmesh.
|
||
|
||
*Restituisce:* Vero quando l'agente è partito.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navagentstop,navAgentStop>>, <<navnearest,navNearest>>, <<navpath,navPath>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La spalla segue l'eroe, con la destinazione aggiornata due volte al secondo.
|
||
function onOverlayUpdate()
|
||
ticks = ticks + 1
|
||
if ticks % 30 == 0 then
|
||
local hx, hy, hz = nodeGetWorldPosition(hero)
|
||
if not navAgentMoveTo(sidekickAgent, hx, hy, hz) then
|
||
navAgentStop(sidekickAgent) -- L'eroe è fuori dalla navmesh: salta, nuota
|
||
end
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#navagentnew]
|
||
==== navAgentNew
|
||
|
||
[source,text]
|
||
----
|
||
agent = navAgentNew(nav, node, radius, height, speed)
|
||
----
|
||
|
||
Mette sulla navmesh un agente di quelle dimensioni nel punto più vicino alla posizione del nodo nel mondo (la posizione così com'è quando non c'è nulla entro 2 unità di lato e 4 sopra o sotto), che cammina a `speed` unità al secondo e accelera a 8 unità al secondo quadrato. Il raggio e l'altezza vengono mantenuti a `0.01` o più, come `navNew` fa con i propri. Da quel momento il motore sposta il nodo lungo la navmesh a ogni fotogramma, nello spazio del suo genitore, e lo gira attorno a Y perché guardi nella direzione in cui va ogni volta che si muove a più di `0.05` unità al secondo; i nodi guardano verso -Z, come i modelli. La folla guida gli agenti l'uno attorno all'altro, anticipa le curve e li tiene separati. Per pilotare un controller del giocatore sul nodo invece di posizionare il nodo, vedi `navAgentSetPlayer`. Termina lo script quando la navmesh non è costruita o ci sono già 128 agenti.
|
||
|
||
*Parametri:*
|
||
|
||
* `nav` -- una navmesh costruita, da `navNew` o da `navLoad`.
|
||
* `node` -- il nodo da muovere; di solito la radice di un'istanza di modello.
|
||
* `radius` -- il raggio dell'agente per tenersi lontano dagli altri, in unità del mondo; non più del raggio della navmesh.
|
||
* `height` -- l'altezza dell'agente, in unità del mondo.
|
||
* `speed` -- la velocità massima di camminata, in unità del mondo al secondo.
|
||
|
||
*Restituisce:* L'handle dell'agente, un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navagentmoveto,navAgentMoveTo>>, <<navagentsetplayer,navAgentSetPlayer>>, <<navagentdelete,navAgentDelete>>, <<navrandompoint,navRandomPoint>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
for i = 1, 6 do
|
||
local fox = modelInstance(foxModel)
|
||
nodeSetScale(fox, 0.01)
|
||
nodeSetPosition(fox, navRandomPoint(nav))
|
||
animationPlay(fox, "Walk", true)
|
||
local agent = navAgentNew(nav, fox, 0.35, 1.0, 1.4)
|
||
navAgentMoveTo(agent, navRandomPoint(nav))
|
||
end
|
||
|
||
function onNavArrived(agent)
|
||
navAgentMoveTo(agent, navRandomPoint(nav))
|
||
end
|
||
----
|
||
|
||
[#navagentsetplayer]
|
||
==== navAgentSetPlayer
|
||
|
||
[source,text]
|
||
----
|
||
navAgentSetPlayer(agent, player)
|
||
----
|
||
|
||
Con `true`, l'agente smette di posizionare il suo nodo e passa invece la propria velocità, a ogni fotogramma, al controller del giocatore sul nodo come un `playerMove` (solo orizzontale), così scale, pendenze, spinte e collisioni continuano a funzionare; prima di ogni aggiornamento della folla la posizione del controller nel mondo viene riletta come quella dell'agente, così il percorso tiene conto degli spintoni che lo portano fuori rotta. Il nodo si gira comunque nella direzione in cui va. Il nodo ha bisogno di un controller di `playerNew`, altrimenti non si muove nulla. Con `false` l'agente torna a posizionare il nodo da sé.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playernew,playerNew>>, <<playermove,playerMove>>, <<navagentnew,navAgentNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un compagno che segue l'eroe per tutto il livello e può essere sballottato.
|
||
local companion = nodeNew()
|
||
nodeSetPosition(companion, 2, 0.05, 0)
|
||
playerNew(companion, 0.3, 1.0)
|
||
local companionFox = modelInstance(foxModel)
|
||
nodeSetParent(companionFox, companion)
|
||
nodeSetScale(companionFox, 0.01)
|
||
local companionAgent = navAgentNew(nav, companion, 0.3, 1.0, 2.0)
|
||
navAgentSetPlayer(companionAgent, true)
|
||
----
|
||
|
||
[#navagentstop]
|
||
==== navAgentStop
|
||
|
||
[source,text]
|
||
----
|
||
navAgentStop(agent)
|
||
----
|
||
|
||
Dimentica la destinazione dell'agente: la folla smette di guidarlo verso qualcosa, l'agente rallenta fino a fermarsi dove si trova, e `onNavArrived` non scatta. Gli altri agenti continuano a girargli attorno. Mandalo da qualche parte con `navAgentMoveTo` per farlo ripartire.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navagentmoveto,navAgentMoveTo>>, <<navagentisarrived,navAgentIsArrived>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Le guardie si immobilizzano quando suona l'allarme e si girano verso il rumore.
|
||
function raiseAlarm(x, y, z)
|
||
for _, guard in ipairs(guards) do
|
||
navAgentStop(guard.agent)
|
||
nodeLookAt(guard.node, x, y, z)
|
||
animationPlay(guard.node, "Survey", true, 1, 0.3)
|
||
end
|
||
soundPlay(alarm)
|
||
end
|
||
----
|
||
|
||
[#navbuild]
|
||
==== navBuild
|
||
|
||
[source,text]
|
||
----
|
||
navBuild(nav)
|
||
----
|
||
|
||
Precalcola la navmesh percorribile da tutto ciò che `navAddNode` ha aggiunto: pavimenti e pendenze fino all'angolo della navmesh, gradini fino alla sua altezza, con il raggio dell'agente tolto attorno a ogni muro e dislivello, ed escluso tutto ciò che ha sopra di sé meno spazio dell'altezza dell'agente. Il precalcolo lavora su celle larghe un terzo del raggio dell'agente e ne copre al massimo 4096 per 4096, quindi un raggio piccolo su un livello grande è lento e può fallire; un decimo di secondo per Sponza su un desktop, qualche secondo su un Raspberry Pi. La geometria aggiunta viene poi liberata, e non si può aggiungere altro; precalcola invece un'altra navmesh. `navSave` conserva il risultato. Termina lo script quando non è stato aggiunto nulla, la navmesh è già costruita o il precalcolo fallisce.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navnew,navNew>>, <<navaddnode,navAddNode>>, <<navsave,navSave>>, <<navdraw,navDraw>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il precalcolo avviene una volta, mentre è visibile la schermata del titolo, e viene conservato per la volta dopo.
|
||
local sponza = modelInstance(modelLoad("Models/Sponza.glb"))
|
||
local nav = navNew(0.4, 1.6, 45, 0.3)
|
||
navAddNode(nav, sponza)
|
||
navBuild(nav)
|
||
navSave(nav, "sponza.nav")
|
||
if debugging then
|
||
navDraw(nav) -- Un fotogramma, per controllare a occhio il precalcolo
|
||
end
|
||
----
|
||
|
||
[#navdelete]
|
||
==== navDelete
|
||
|
||
[source,text]
|
||
----
|
||
navDelete(nav)
|
||
----
|
||
|
||
Libera la navmesh e ogni agente che vi si trova; i loro nodi restano dove sono. L'handle della navmesh e gli handle degli agenti non sono più validi e terminano lo script se usati. Il posto torna libero per un nuovo `navNew` o `navLoad`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navnew,navNew>>, <<navagentdelete,navAgentDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function loadLevel(index)
|
||
if levelNav then
|
||
navDelete(levelNav) -- Si porta via gli agenti del vecchio livello
|
||
end
|
||
nodeDelete(levelRoot)
|
||
levelRoot = modelInstance(levels[index].model)
|
||
levelNav = navNew(0.4, 1.6, 45, 0.3)
|
||
navAddNode(levelNav, levelRoot)
|
||
navBuild(levelNav)
|
||
end
|
||
----
|
||
|
||
[#navdraw]
|
||
==== navDraw
|
||
|
||
[source,text]
|
||
----
|
||
navDraw(nav)
|
||
navDraw(nav, r, g, b)
|
||
----
|
||
|
||
Disegna i poligoni della navmesh precalcolata come linee sopra la scena per questo fotogramma, in ciano se non si indica un colore, ogni poligono come un ventaglio di triangoli, così un poligono con più di tre lati mostra le sue diagonali. Chiamala a ogni fotogramma mentre guardi; costa tre linee per triangolo, e una navmesh non ancora costruita non disegna nulla. Serve per controllare che un precalcolo copra il pavimento e stia lontano dai mobili, insieme a `physicsSetDebug`.
|
||
|
||
*Parametri:*
|
||
|
||
* `nav` -- la navmesh.
|
||
* `r`, `g`, `b` -- il colore delle linee, da `0` a `255` ciascuno; predefinito `0, 255, 255`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<linedraw,lineDraw>>, <<navbuild,navBuild>>, <<physicssetdebug,physicsSetDebug>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
if showNavMesh then
|
||
navDraw(nav) -- Ciano sopra il pavimento
|
||
local path = navPath(nav, heroX, heroY, heroZ, goalX, goalY, goalZ)
|
||
if path then
|
||
for i = 2, #path do
|
||
lineDraw(path[i - 1][1], path[i - 1][2], path[i - 1][3], path[i][1], path[i][2], path[i][3], 255, 60, 40)
|
||
end
|
||
end
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#navload]
|
||
==== navLoad
|
||
|
||
[source,text]
|
||
----
|
||
nav = navLoad(name, agentRadius, agentHeight)
|
||
----
|
||
|
||
Carica una navmesh scritta da `navSave`, cercata prima tra i file del gioco (così una navmesh precalcolata può essere distribuita con il gioco, impacchettato o no) e poi nella cartella dei dati del gioco. La navmesh torna già costruita, pronta per agenti e interrogazioni, e non accetta altri `navAddNode`. La pendenza e il gradino con cui è stata precalcolata sono nel file; il raggio e l'altezza dell'agente sono quelli con cui la folla guida, quindi passa i valori per cui è stata precalcolata. Termina lo script quando il file non si può leggere, non è una navmesh, è più corto della navmesh descritta dalla sua intestazione (troncato, o modificato a mano) o non c'è un posto libero per una navmesh.
|
||
|
||
*Parametri:*
|
||
|
||
* `name` -- il nome del file, relativo alla cartella dei giochi o alla cartella dei dati.
|
||
* `agentRadius` -- il raggio dell'agente per cui è stata precalcolata la navmesh, in unità del mondo.
|
||
* `agentHeight` -- l'altezza dell'agente per cui è stata precalcolata, in unità del mondo.
|
||
|
||
*Restituisce:* L'handle della navmesh, un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navsave,navSave>>, <<navnew,navNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La navmesh precalcolata distribuita per il museo; agenti larghi 0.8 e alti 1.7.
|
||
local museum = modelInstance(modelLoad("museum.glb"))
|
||
local nav = navLoad("museum.nav", 0.4, 1.7)
|
||
for _, visitor in ipairs(visitors) do
|
||
visitor.agent = navAgentNew(nav, visitor.node, 0.35, 1.7, 1.2)
|
||
navAgentMoveTo(visitor.agent, navRandomPoint(nav))
|
||
end
|
||
----
|
||
|
||
[#navnearest]
|
||
==== navNearest
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z = navNearest(nav, x, y, z)
|
||
----
|
||
|
||
Il punto della navmesh più vicino a un punto dato, cercando fino a 2 unità di lato e 4 sopra o sotto, o `nil` quando non c'è nulla così vicino. Serve per agganciare al pavimento un punto di comparsa, un oggetto lasciato cadere o un clic prima di passarlo a un agente, e per trovare l'altezza del pavimento sotto un punto.
|
||
|
||
*Parametri:*
|
||
|
||
* `nav` -- la navmesh.
|
||
* `x`, `y`, `z` -- il punto da agganciare, in unità del mondo.
|
||
|
||
*Restituisce:* Tre numeri, o `nil`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navpath,navPath>>, <<navagentmoveto,navAgentMoveTo>>, <<sceneunproject,sceneUnproject>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fa comparire il nemico sul pavimento più vicino al generatore, o per niente.
|
||
function spawnEnemy(spawner)
|
||
local sx, sy, sz = nodeGetWorldPosition(spawner)
|
||
local x, y, z = navNearest(nav, sx, sy, sz)
|
||
if x then
|
||
local enemy = modelInstance(enemyModel)
|
||
nodeSetPosition(enemy, x, y, z)
|
||
enemies[#enemies + 1] = { node = enemy, agent = navAgentNew(nav, enemy, 0.4, 1.8, 2.5) }
|
||
end
|
||
end
|
||
----
|
||
|
||
[#navnew]
|
||
==== navNew
|
||
|
||
[source,text]
|
||
----
|
||
nav = navNew(agentRadius, agentHeight, maxSlope, maxStep)
|
||
----
|
||
|
||
Avvia una navmesh per agenti di quel raggio e di quell'altezza che possono salire gradini fino a `maxStep` unità e pendenze fino a `maxSlope` gradi. Il raggio e l'altezza vengono mantenuti a `0.01` o più, la pendenza tra `0` e `89` e il gradino a `0` o più. Il raggio stabilisce anche la risoluzione del precalcolo, dato che una cella ne è un terzo, quindi decide quanto ci mette `navBuild` e quanto grande può essere il livello che copre. Aggiungi la geometria con `navAddNode`, poi chiama `navBuild`. Termina lo script quando esistono già otto navmesh.
|
||
|
||
*Parametri:*
|
||
|
||
* `agentRadius` -- il raggio degli agenti, in unità del mondo; i muri vengono allargati di questo margine.
|
||
* `agentHeight` -- quanto sono alti; sotto i soffitti più bassi non si cammina.
|
||
* `maxSlope` -- la pendenza percorribile più ripida, in gradi.
|
||
* `maxStep` -- il gradino più alto che salgono, in unità del mondo.
|
||
|
||
*Restituisce:* L'handle della navmesh, un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navaddnode,navAddNode>>, <<navbuild,navBuild>>, <<navload,navLoad>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Sponza per un agente largo 0.8 e alto 1.6 che sale 0.3 e percorre pendenze fino a 45 gradi.
|
||
local sponza = modelInstance(modelLoad("Models/Sponza.glb"))
|
||
local nav = navNew(0.4, 1.6, 45, 0.3)
|
||
navAddNode(nav, sponza)
|
||
navBuild(nav)
|
||
----
|
||
|
||
[#navpath]
|
||
==== navPath
|
||
|
||
[source,text]
|
||
----
|
||
points = navPath(nav, x0, y0, z0, x1, y1, z1)
|
||
----
|
||
|
||
Gli angoli del percorso a piedi più breve da un punto a un altro, entrambi prima agganciati alla navmesh, come tabella di tabelle `{ x, y, z }` con la prima alla partenza e l'ultima all'arrivo; fino a 256 angoli. Quando l'arrivo non è raggiungibile il percorso si ferma al punto raggiungibile più vicino lungo la strada, quindi l'ultimo angolo è sempre un posto in cui un agente può stare. `nil` quando uno dei due estremi è lontano dalla navmesh o non si trova alcun percorso. Serve per disegnare un itinerario, misurare quanto dista qualcosa a piedi o muovere tu stesso qualcosa lungo il percorso; gli agenti lo fanno da soli.
|
||
|
||
*Parametri:*
|
||
|
||
* `nav` -- la navmesh.
|
||
* `x0`, `y0`, `z0` -- dove parte il percorso.
|
||
* `x1`, `y1`, `z1` -- dove finisce.
|
||
|
||
*Restituisce:* Una tabella di tabelle `{ x, y, z }`, o `nil`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navagentmoveto,navAgentMoveTo>>, <<navraycast,navRaycast>>, <<navnearest,navNearest>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Quanto dista l'uscita a piedi, per l'indicatore di distanza della mappa.
|
||
function walkingDistance(fromX, fromY, fromZ, toX, toY, toZ)
|
||
local path = navPath(nav, fromX, fromY, fromZ, toX, toY, toZ)
|
||
local total = 0
|
||
for i = 2, #(path or {}) do
|
||
local dx = path[i][1] - path[i - 1][1]
|
||
local dz = path[i][3] - path[i - 1][3]
|
||
total = total + math.sqrt(dx * dx + dz * dz)
|
||
end
|
||
return path and total or nil
|
||
end
|
||
----
|
||
|
||
[#navrandompoint]
|
||
==== navRandomPoint
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z = navRandomPoint(nav)
|
||
----
|
||
|
||
Un punto a caso sulla navmesh percorribile, ovunque arrivi, per vagare, spargere oggetti da raccogliere o piazzare un punto di comparsa. `nil` solo quando la navmesh non è costruita.
|
||
|
||
*Restituisce:* Tre numeri, o `nil`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navagentmoveto,navAgentMoveTo>>, <<navnearest,navNearest>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Sparge venti monete sul pavimento, ciascuna poco sopra di esso.
|
||
for i = 1, 20 do
|
||
local x, y, z = navRandomPoint(nav)
|
||
local coin = nodeNew()
|
||
nodeSetMesh(coin, coinMesh, gold)
|
||
nodeSetPosition(coin, x, y + 0.3, z)
|
||
coins[#coins + 1] = coin
|
||
end
|
||
----
|
||
|
||
[#navraycast]
|
||
==== navRaycast
|
||
|
||
[source,text]
|
||
----
|
||
blocked, x, y, z = navRaycast(nav, x0, y0, z0, x1, y1, z1)
|
||
----
|
||
|
||
Se una camminata in linea retta da un punto a un altro esce dalla navmesh, contro un muro o su un dislivello, e dove lo farebbe. La partenza viene agganciata alla navmesh; `false` da solo significa che la strada è libera, o che la partenza è lontana dalla navmesh. Quando la strada è bloccata, il punto è dove la linea attraversa il bordo della navmesh, con l'altezza interpolata tra i due estremi. Un controllo economico per sapere se esiste una scorciatoia prima di chiedere a `navPath`.
|
||
|
||
*Parametri:*
|
||
|
||
* `nav` -- la navmesh.
|
||
* `x0`, `y0`, `z0` -- dove parte la camminata.
|
||
* `x1`, `y1`, `z1` -- dove finirebbe.
|
||
|
||
*Restituisce:* `false`, oppure `true` e il punto in cui la camminata esce dalla navmesh.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navpath,navPath>>, <<physicsraycast,physicsRaycast>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una granata lanciata rotola solo fin dove il pavimento glielo permette.
|
||
function landingSpot(fromX, fromY, fromZ, toX, toY, toZ)
|
||
local blocked, hx, hy, hz = navRaycast(nav, fromX, fromY, fromZ, toX, toY, toZ)
|
||
if blocked then
|
||
return hx, hy, hz
|
||
end
|
||
return toX, toY, toZ
|
||
end
|
||
----
|
||
|
||
[#navsave]
|
||
==== navSave
|
||
|
||
[source,text]
|
||
----
|
||
navSave(nav, name)
|
||
----
|
||
|
||
Scrive la navmesh precalcolata nella cartella dei dati del gioco con il nome `name`, perché `navLoad` la usi la volta successiva, o da copiare nella cartella del gioco per distribuirla. La navmesh deve essere costruita. Termina lo script quando non lo è, o quando il file non si può scrivere.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navload,navLoad>>, <<navbuild,navBuild>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Precalcolata una volta su questa macchina, poi caricata dalla cartella dei dati nelle esecuzioni successive.
|
||
local nav = navNew(0.4, 1.6, 45, 0.3)
|
||
navAddNode(nav, level)
|
||
navBuild(nav)
|
||
navSave(nav, "level1.nav")
|
||
debugPrint("Navigation mesh baked and saved.")
|
||
----
|
||
|
||
[#net]
|
||
=== Rete
|
||
|
||
Ciò che il motore aggiunge sotto `Singe/Net.singe`, il client HTTP del menu.
|
||
La maggior parte dei giochi non lo tocca mai: inviare un punteggio o scaricare un
|
||
catalogo passa per `Net.singe`, che lo chiama al posto tuo.
|
||
|
||
[#nettrustsystem]
|
||
==== netTrustSystem
|
||
|
||
[source,text]
|
||
----
|
||
lists, report = netTrustSystem(context)
|
||
----
|
||
|
||
Aggiunge i certificati radice fidati della macchina stessa a un contesto client di
|
||
LuaSec, oltre a ciò che il contesto contiene già (di solito il `Singe/cacerts.pem`
|
||
distribuito con Singe). Una connessione fatta con quel contesto viene poi verificata
|
||
quando una delle due liste conosce la radice del server. La lista della macchina è,
|
||
in ordine: il file o la directory indicati da `SSL_CERT_FILE` o da `SSL_CERT_DIR`,
|
||
quando sono impostati; altrimenti il bundle della distribuzione su Linux (il primo
|
||
dei soliti sei posti che esiste), l'archivio di sistema root su Windows o i trust
|
||
anchor di sistema su macOS. Viene aggiunto anche `cacerts.local.pem` nella cartella
|
||
dei dati, quando esiste; il motore non lo scrive mai.
|
||
|
||
`lists` è il numero di liste che hanno aggiunto qualcosa. `report` indica cosa è
|
||
stato consultato, un `what path` per riga (`what` è `system` o `local`), per un
|
||
messaggio di errore su cui una persona possa intervenire.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<utilsha256,utilSha256>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un solo contesto per ogni richiesta: il bundle distribuito, poi sopra le radici di questa macchina.
|
||
local ssl = require("ssl")
|
||
local context = ssl.newcontext({ mode = "client", protocol = "any", verify = "peer", cafile = "Singe/cacerts.pem" })
|
||
local lists, report = netTrustSystem(context)
|
||
|
||
debugPrint("Trusting the shipped bundle and " .. lists .. " more:\n" .. report)
|
||
----
|
||
|
||
[#node]
|
||
=== Nodo
|
||
|
||
Ogni oggetto in una scena è un nodo di un unico albero: il nodo `0` è la radice,
|
||
e ogni altro nodo è appeso sotto un genitore. Un nodo ha una posizione, una
|
||
rotazione e una scala relative a quel genitore, e può disegnare una mesh con un
|
||
materiale, mostrare uno sprite o una riga di testo, o risplendere come luce; i figli
|
||
ereditano la trasformazione di tutto ciò che sta sopra di loro. Gli handle sono interi
|
||
restituiti da `nodeNew`, `modelInstance` e `lightNew`; un handle il cui nodo è stato
|
||
eliminato termina lo script quando viene ripassato, e un handle liberato può
|
||
essere riusato da un `nodeNew` successivo. Posizioni e dimensioni sono in unità
|
||
della scena e le rotazioni in gradi, con +Y verso l'alto e -Z in avanti, come
|
||
descritto in <<scenes3d,Scene 3D>>.
|
||
|
||
[#nodenew]
|
||
==== nodeNew
|
||
|
||
[source,text]
|
||
----
|
||
node = nodeNew()
|
||
node = nodeNew(parent)
|
||
----
|
||
|
||
Crea un nodo vuoto nell'origine del suo genitore, senza rotazione, in scala `1`,
|
||
visibile, che proietta ombre, senza mesh né materiale. Senza `parent` il nodo va
|
||
sotto la radice; con un genitore diventa l'ultimo figlio di quel nodo e da quel
|
||
momento si muove con esso. I nodi vuoti servono da perni e da gruppi: piazzane
|
||
uno, poi appendici sotto mesh, luci o una telecamera e muovi il gruppo tutto
|
||
insieme.
|
||
|
||
*Restituisce:* L'handle del nodo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetmesh,nodeSetMesh>>, <<nodesetparent,nodeSetParent>>, <<nodedelete,nodeDelete>>, <<lightnew,lightNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una torretta la cui canna ruota sulla torretta, che a sua volta gira sulla base.
|
||
local steel = materialNew()
|
||
materialSetColor(steel, 140, 145, 150)
|
||
local base = nodeNew()
|
||
local turret = nodeNew(base)
|
||
local barrel = nodeNew(turret)
|
||
nodeSetMesh(base, meshCylinder(1, 0.3), steel)
|
||
nodeSetMesh(turret, meshSphere(0.6), steel)
|
||
nodeSetMesh(barrel, meshCylinder(0.1, 1.2), steel)
|
||
nodeSetPosition(barrel, 0, 0, -0.8)
|
||
nodeSetRotation(barrel, 90, 0, 0)
|
||
----
|
||
|
||
[#nodedelete]
|
||
==== nodeDelete
|
||
|
||
[source,text]
|
||
----
|
||
nodeDelete(node)
|
||
----
|
||
|
||
Rimuove il nodo e tutto ciò che sta sotto di esso, liberando ogni handle di quel
|
||
sottoalbero; viene rilasciato anche lo sprite o il testo su ciascuno di essi. Le
|
||
mesh e i materiali che i nodi disegnavano restano intatti, perché altri nodi possono
|
||
condividerli. Se il nodo eliminato era la telecamera, la scena torna alla vista
|
||
predefinita. Il nodo radice non si può eliminare; chiederlo termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodenew,nodeNew>>, <<nodesetvisible,nodeSetVisible>>, <<meshdelete,meshDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- I colpi vivono due secondi, poi i loro nodi spariscono.
|
||
function fireShot(ship)
|
||
local shot = nodeNew()
|
||
nodeSetMesh(shot, shotMesh, tracer)
|
||
nodeSetPosition(shot, nodeGetWorldPosition(ship))
|
||
shots[#shots + 1] = { node = shot, born = os.clock() }
|
||
end
|
||
|
||
function reapShots()
|
||
for i = #shots, 1, -1 do
|
||
if os.clock() - shots[i].born > 2 then
|
||
nodeDelete(shots[i].node)
|
||
table.remove(shots, i)
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
[#nodefind]
|
||
==== nodeFind
|
||
|
||
[source,text]
|
||
----
|
||
node = nodeFind(name)
|
||
node = nodeFind(name, root)
|
||
----
|
||
|
||
Cerca in profondità il primo nodo chiamato `name`, partendo da `root` (la radice
|
||
della scena se omesso) e controllando `root` stesso prima dei suoi figli, poi il
|
||
sottoalbero di ciascun figlio a turno. Restituisce `nil` quando nulla corrisponde.
|
||
I nodi di un modello portano i nomi del loro file glTF, quindi un osso, una mesh o
|
||
un empty usato come segnaposto in Blender si possono trovare con quel nome sotto la
|
||
radice dell'istanza. I nomi impostati con `nodeSetName` si trovano allo stesso
|
||
modo.
|
||
|
||
*Restituisce:* Un handle di nodo, o `nil`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetname,nodeSetName>>, <<nodegetname,nodeGetName>>, <<modelinstance,modelInstance>>, <<nodegetchildren,nodeGetChildren>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Appende una torcia alla mano destra dell'eroe, trovata con il nome del suo osso.
|
||
local hero = modelInstance(heroModel)
|
||
local hand = nodeFind("hand_R", hero)
|
||
if hand then
|
||
local torch = modelInstance(torchModel)
|
||
nodeSetParent(torch, hand)
|
||
nodeSetPosition(torch, 0, 0.05, 0)
|
||
nodeSetRotation(torch, 0, 0, -90)
|
||
end
|
||
----
|
||
|
||
[#nodegetchildren]
|
||
==== nodeGetChildren
|
||
|
||
[source,text]
|
||
----
|
||
children = nodeGetChildren(node)
|
||
----
|
||
|
||
I figli diretti del nodo come tabella di handle, nell'ordine in cui sono stati
|
||
attaccati; cambiare il genitore di un nodo lo mette in fondo all'elenco del nuovo
|
||
genitore. I nipoti non sono inclusi; usa la ricorsione per percorrere un intero
|
||
sottoalbero. Una tabella vuota significa che il nodo non ha figli.
|
||
|
||
*Restituisce:* Una tabella di handle di nodo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodegetparent,nodeGetParent>>, <<nodefind,nodeFind>>, <<nodesetparent,nodeSetParent>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Dà a ogni parte di un oggetto di scena caricato lo stesso materiale consumato.
|
||
local function paint(node, material)
|
||
nodeSetMaterial(node, material)
|
||
for _, child in ipairs(nodeGetChildren(node)) do
|
||
paint(child, material)
|
||
end
|
||
end
|
||
|
||
local crate = modelInstance(crateModel)
|
||
paint(crate, wornWood)
|
||
----
|
||
|
||
[#nodegetmorph]
|
||
==== nodeGetMorph
|
||
|
||
[source,text]
|
||
----
|
||
weight = nodeGetMorph(node, nameOrIndex)
|
||
----
|
||
|
||
Il peso corrente di un morph target della mesh del nodo, indicato per nome o per
|
||
numero a partire da `1` nell'ordine in cui li elenca `nodeGetMorphs`. Il nodo è
|
||
quello che porta la mesh, non la radice di un'istanza di modello. Un nome o un
|
||
numero che la mesh non ha termina lo script.
|
||
|
||
*Restituisce:* Un numero, `0` finché qualcosa non imposta il target.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetmorph,nodeSetMorph>>, <<nodegetmorphs,nodeGetMorphs>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fa entrare il sorriso gradualmente in più fotogrammi invece di farlo scattare.
|
||
function onOverlayUpdate()
|
||
local now = nodeGetMorph(face, "smile")
|
||
local target = happy and 1 or 0
|
||
nodeSetMorph(face, "smile", now + (target - now) * 0.1)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#nodegetmorphs]
|
||
==== nodeGetMorphs
|
||
|
||
[source,text]
|
||
----
|
||
names = nodeGetMorphs(node)
|
||
----
|
||
|
||
I morph target della mesh del nodo come tabella di nomi, nell'ordine in cui li
|
||
memorizza il file; quell'ordine è la numerazione che accettano `nodeSetMorph` e
|
||
`nodeGetMorph`. Gli esportatori che non scrivono nomi danno stringhe vuote, e quei
|
||
target si possono comunque impostare per numero. Un nodo senza mesh, o una mesh
|
||
senza target, dà una tabella vuota.
|
||
|
||
*Restituisce:* Una tabella di stringhe.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetmorph,nodeSetMorph>>, <<nodegetmorph,nodeGetMorph>>, <<modelload,modelLoad>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Associa una volta sola i nomi dei target del volto ai numeri, così la sincronizzazione labiale può indicizzarli.
|
||
local head = nodeFind("Head", hero)
|
||
local visemes = {}
|
||
for index, name in ipairs(nodeGetMorphs(head)) do
|
||
if name ~= "" then
|
||
visemes[name] = index
|
||
end
|
||
end
|
||
debugPrint(string.format("%d morph targets on the head", #nodeGetMorphs(head)))
|
||
----
|
||
|
||
[#nodegetname]
|
||
==== nodeGetName
|
||
|
||
[source,text]
|
||
----
|
||
name = nodeGetName(node)
|
||
----
|
||
|
||
Il nome del nodo: quello che gli ha dato `nodeSetName` o, per un nodo di un
|
||
modello, il suo nome glTF; una stringa vuota quando non ha né l'uno né l'altro. Il
|
||
nodo radice si chiama `root`.
|
||
|
||
*Restituisce:* Una stringa.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetname,nodeSetName>>, <<nodefind,nodeFind>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Stampa nella console l'albero dei nodi di un modello mentre cerchi di capire i nomi delle ossa.
|
||
local function dump(node, depth)
|
||
debugPrint(string.rep(" ", depth) .. nodeGetName(node) .. " (" .. node .. ")")
|
||
for _, child in ipairs(nodeGetChildren(node)) do
|
||
dump(child, depth + 1)
|
||
end
|
||
end
|
||
|
||
dump(modelInstance(heroModel), 0)
|
||
----
|
||
|
||
[#nodegetparent]
|
||
==== nodeGetParent
|
||
|
||
[source,text]
|
||
----
|
||
parent = nodeGetParent(node)
|
||
----
|
||
|
||
L'handle del genitore del nodo, o `nil` per la radice. Ogni altro nodo ha un
|
||
genitore, anche se è solo la radice.
|
||
|
||
*Restituisce:* Un handle di nodo, o `nil`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetparent,nodeSetParent>>, <<nodegetchildren,nodeGetChildren>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Risale da un nodo di mesh selezionato fino alla cima della sua istanza di modello.
|
||
local function instanceRoot(node)
|
||
local parent = nodeGetParent(node)
|
||
while parent and parent ~= 0 do
|
||
node = parent
|
||
parent = nodeGetParent(node)
|
||
end
|
||
return node
|
||
end
|
||
----
|
||
|
||
[#nodegetposition]
|
||
==== nodeGetPosition
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z = nodeGetPosition(node)
|
||
----
|
||
|
||
La posizione del nodo relativa al suo genitore, come impostata per ultimo da
|
||
`nodeSetPosition`, da `nodeMove` o dal motore (un corpo, un veicolo o
|
||
un'animazione che lo muove). Per sapere dove si trova il nodo nel mondo, usa
|
||
`nodeGetWorldPosition`.
|
||
|
||
*Restituisce:* Tre numeri.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetposition,nodeSetPosition>>, <<nodegetworldposition,nodeGetWorldPosition>>, <<nodemove,nodeMove>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La telecamera segue l'auto, tre unità sopra, tre a destra e nove dietro.
|
||
function onOverlayUpdate()
|
||
local cx, cy, cz = nodeGetPosition(car)
|
||
nodeSetPosition(camera, cx + 3, cy + 3, cz + 9)
|
||
nodeLookAt(camera, cx, cy, cz - 3)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#nodegetquaternion]
|
||
==== nodeGetQuaternion
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z, w = nodeGetQuaternion(node)
|
||
----
|
||
|
||
La rotazione del nodo relativa al suo genitore come quaternione unitario, la forma
|
||
in cui la memorizzano modelli e animazioni. Usalo per copiare una rotazione tra
|
||
nodi o per conservare una posa senza l'ambiguità degli angoli di Eulero.
|
||
|
||
*Restituisce:* Quattro numeri.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetquaternion,nodeSetQuaternion>>, <<nodegetrotation,nodeGetRotation>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ricorda la posa della porta prima della prova del ragdoll, poi la rimette com'era.
|
||
local px, py, pz = nodeGetPosition(door)
|
||
local qx, qy, qz, qw = nodeGetQuaternion(door)
|
||
|
||
function resetDoor()
|
||
nodeSetPosition(door, px, py, pz)
|
||
nodeSetQuaternion(door, qx, qy, qz, qw)
|
||
end
|
||
----
|
||
|
||
[#nodegetrotation]
|
||
==== nodeGetRotation
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z = nodeGetRotation(node)
|
||
----
|
||
|
||
La rotazione del nodo relativa al suo genitore come angoli di Eulero in gradi,
|
||
scomposta nello stesso modo in cui la compone `nodeSetRotation`: girato attorno a
|
||
Y, poi inclinato attorno a X, poi ruotato in rollio attorno a Z. Gli angoli
|
||
descrivono la stessa rotazione che è stata impostata ma non sono per forza gli
|
||
stessi numeri: `X` torna tra `-90` e `90`, e quando raggiunge `90` o `-90` la
|
||
torsione viene data a `Y` e `Z` viene riportato come `0`.
|
||
|
||
*Restituisce:* Tre numeri.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetrotation,nodeSetRotation>>, <<nodegetquaternion,nodeGetQuaternion>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una bussola nell'overlay a partire dalla rotta della nave.
|
||
function onOverlayUpdate()
|
||
local _, heading = nodeGetRotation(ship)
|
||
overlayClear()
|
||
fontPrint(20, 20, string.format("Heading %03d", (360 - heading) % 360))
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#nodegetscale]
|
||
==== nodeGetScale
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z = nodeGetScale(node)
|
||
----
|
||
|
||
La scala propria del nodo per ciascun asse, come impostata da `nodeSetScale`
|
||
(`1, 1, 1` per un nodo nuovo). Le scale dei genitori non vengono incluse.
|
||
|
||
*Restituisce:* Tre numeri.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetscale,nodeSetScale>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fa crescere il power-up verso la dimensione piena, un po' a ogni fotogramma.
|
||
function onOverlayUpdate()
|
||
local size = nodeGetScale(powerUp)
|
||
if size < 1 then
|
||
nodeSetScale(powerUp, math.min(1, size + 0.05))
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#nodegetworldposition]
|
||
==== nodeGetWorldPosition
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z = nodeGetWorldPosition(node)
|
||
----
|
||
|
||
Dove si trova l'origine del nodo nello spazio del mondo, con applicata la
|
||
trasformazione di ogni genitore, all'ultimo fotogramma renderizzato. Le modifiche
|
||
fatte in questo fotogramma (le tue, o quelle di un corpo) si vedono dopo il
|
||
rendering successivo, e un nodo creato in questo fotogramma riporta `0, 0, 0` fino
|
||
ad allora. Questa è la posizione da passare a `sceneProject` o a un controllo di
|
||
distanza.
|
||
|
||
*Restituisce:* Tre numeri.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<sceneproject,sceneProject>>, <<nodelookat,nodeLookAt>>, <<nodegetposition,nodeGetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un'etichetta con il nome nell'overlay sopra l'anatra, ovunque l'abbia portata la fisica.
|
||
function onOverlayUpdate()
|
||
local dx, dy, dz = nodeGetWorldPosition(duck)
|
||
local sx, sy = sceneProject(dx, dy + 0.8, dz)
|
||
overlayClear()
|
||
if sx then
|
||
fontPrint(sx - 20, sy, "DUCK")
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#nodelookat]
|
||
==== nodeLookAt
|
||
|
||
[source,text]
|
||
----
|
||
nodeLookAt(node, x, y, z)
|
||
----
|
||
|
||
Ruota il nodo in modo che il suo -Z punti verso una posizione nel mondo, mantenendo
|
||
il suo +Y il più vicino all'alto del mondo che la direzione permette. È quello che
|
||
vogliono telecamere e luci, e va bene anche per una torretta o una testa che segue
|
||
un bersaglio. Cambia solo la rotazione del nodo; la sua posizione resta. Se il
|
||
bersaglio coincide con la posizione del nodo non cambia nulla.
|
||
|
||
*Note:* Il punto del mondo viene convertito nello spazio del genitore usando la
|
||
trasformazione del genitore dall'ultimo fotogramma renderizzato; un nodo il cui
|
||
genitore si è mosso in questo stesso fotogramma mira da dove si trovava il genitore.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<cameraset,cameraSet>>, <<lightnew,lightNew>>, <<nodesetrotation,nodeSetRotation>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||
nodeSetPosition(sun, -8, 3, 6)
|
||
nodeLookAt(sun, 0, 0, 0)
|
||
lightSetShadow(sun, true)
|
||
|
||
local camera = nodeNew()
|
||
nodeSetPosition(camera, 0, 2.2, 7)
|
||
nodeLookAt(camera, 0, 1.3, 0)
|
||
cameraSet(camera)
|
||
----
|
||
|
||
[#nodemove]
|
||
==== nodeMove
|
||
|
||
[source,text]
|
||
----
|
||
nodeMove(node, dx, dy, dz)
|
||
----
|
||
|
||
Muove il nodo lungo i suoi assi: lo spostamento viene ruotato secondo la rotazione
|
||
corrente del nodo e sommato alla sua posizione, quindi `nodeMove(ship, 0, 0, -0.1)`
|
||
porta avanti la nave ovunque stia puntando e `nodeMove(ship, 0.1, 0, 0)` la sposta
|
||
lateralmente a destra. Usa `nodeSetPosition` per un punto assoluto nello spazio del
|
||
genitore.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetposition,nodeSetPosition>>, <<noderotate,nodeRotate>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Controlli da carro armato: gira con sinistra e destra, avanza con su e giù.
|
||
function onOverlayUpdate()
|
||
if keyboardIsDown(SCANCODE_LEFT) then nodeRotate(tank, 0, 2, 0) end
|
||
if keyboardIsDown(SCANCODE_RIGHT) then nodeRotate(tank, 0, -2, 0) end
|
||
if keyboardIsDown(SCANCODE_UP) then nodeMove(tank, 0, 0, -0.08) end
|
||
if keyboardIsDown(SCANCODE_DOWN) then nodeMove(tank, 0, 0, 0.04) end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#noderotate]
|
||
==== nodeRotate
|
||
|
||
[source,text]
|
||
----
|
||
nodeRotate(node, dx, dy, dz)
|
||
----
|
||
|
||
Gira il nodo dei gradi indicati attorno ai suoi assi, in aggiunta alla sua rotazione
|
||
corrente: `nodeRotate(wheel, 5, 0, 0)` a ogni fotogramma fa girare la ruota attorno
|
||
al proprio X comunque sia inclinato l'assale. I tre angoli vengono combinati come li
|
||
combina `nodeSetRotation` (Y, poi X, poi Z) prima di essere applicati. Gli angoli
|
||
positivi ruotano in senso antiorario guardando lungo l'asse verso l'origine.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetrotation,nodeSetRotation>>, <<nodemove,nodeMove>>, <<nodesetquaternion,nodeSetQuaternion>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il logo gira di mezzo grado a fotogramma attorno al proprio asse verticale.
|
||
local logo = modelInstance(logoModel)
|
||
nodeSetPosition(logo, 0, -2.5, 0)
|
||
nodeSetRotation(logo, 0, -30, 0)
|
||
|
||
function onOverlayUpdate()
|
||
nodeRotate(logo, 0, 0.5, 0)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#nodesetbillboard]
|
||
==== nodeSetBillboard
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetBillboard(node, mode)
|
||
----
|
||
|
||
Gira il nodo verso la telecamera a ogni fotogramma. `BILLBOARD_ALL` la guarda di
|
||
fronte, prendendo tutti e tre gli assi dalla telecamera; `BILLBOARD_Y` gira solo
|
||
attorno all'alto del mondo, così il nodo resta dritto (alberi, barre della salute);
|
||
`BILLBOARD_NONE`, il valore predefinito, non lo gira affatto. La posizione nel mondo
|
||
e la scala del nodo vengono mantenute e la sua rotazione propria viene ignorata
|
||
finché è impostata una modalità. Qualsiasi altro valore termina lo script.
|
||
Funziona sulle mesh come sugli sprite e sul testo, e si gira verso qualunque
|
||
telecamera stia disegnando: anche una vista vede il billboard di fronte, mentre la
|
||
sua ombra segue la telecamera della finestra.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetsprite,nodeSetSprite>>, <<nodesettext,nodeSetText>>, <<cameraset,cameraSet>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un nome sopra la volpe rivolto di fronte alla telecamera, e un'icona di cassa che
|
||
-- gira solo attorno al proprio asse verticale.
|
||
local tag = nodeNew()
|
||
nodeSetParent(tag, fox)
|
||
nodeSetPosition(tag, 0, 110, 0)
|
||
nodeSetScale(tag, 50)
|
||
nodeSetText(tag, "FOX", 0.6)
|
||
nodeSetBillboard(tag, BILLBOARD_ALL)
|
||
nodeSetShadow(tag, false)
|
||
local icon = nodeNew()
|
||
nodeSetParent(icon, fox)
|
||
nodeSetPosition(icon, 0, 150, 0)
|
||
nodeSetScale(icon, 50)
|
||
nodeSetSprite(icon, spriteLoad("crate.png"), 0.5)
|
||
nodeSetBillboard(icon, BILLBOARD_Y)
|
||
----
|
||
|
||
[#nodesetlayer]
|
||
==== nodeSetLayer
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetLayer(node, layer)
|
||
----
|
||
|
||
Sceglie in quale punto della finestra viene disegnato il nodo. `LAYER_SCENE`, il
|
||
valore predefinito, lo disegna nella scena sotto l'overlay; `LAYER_FRONT` lo disegna
|
||
dopo l'overlay, con la stessa telecamera e le stesse luci, in un livello trasparente
|
||
tutto suo. Un nodo nel livello anteriore si porta dietro i suoi figli, qualunque sia
|
||
la loro impostazione. Qualsiasi altro valore termina lo script. Vedi
|
||
<<scenefront,Davanti all'overlay>>.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<sceneenable,sceneEnable>>, <<nodesetvisible,nodeSetVisible>>, <<emittersetlayer,emitterSetLayer>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La nave del giocatore sta sulla schermata 2D della mappa finché è aperta, e torna
|
||
-- nel mondo quando si chiude.
|
||
function showMap(open)
|
||
nodeSetLayer(ship, open and LAYER_FRONT or LAYER_SCENE)
|
||
end
|
||
----
|
||
|
||
[#nodesetmaterial]
|
||
==== nodeSetMaterial
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetMaterial(node, material)
|
||
nodeSetMaterial(node, nil)
|
||
----
|
||
|
||
Mette un materiale su qualunque mesh abbia il nodo, mantenendo la mesh. `nil` lo
|
||
rimuove, lasciando l'aspetto predefinito: bianco, ruvido a metà, senza texture.
|
||
Usalo per cambiare stile a una parte di un modello, per mettere e togliere un
|
||
materiale di evidenziazione, o per vestire una mesh che il motore ha fatto per te,
|
||
come il tubo di una corda creato da `softNew`. Un handle di materiale che non esiste
|
||
termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetmesh,nodeSetMesh>>, <<materialnew,materialNew>>, <<softnew,softNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La corda pende dalla gru; il motore ne fa il tubo, noi lo vestiamo.
|
||
local hemp = materialNew()
|
||
materialSetColor(hemp, 170, 140, 90)
|
||
materialSetRoughness(hemp, 1)
|
||
local rope = nodeNew()
|
||
nodeSetPosition(rope, 4, 4, -1)
|
||
softNew(rope, SOFT_ROPE, 6.5, 4, -1, 16, 0.04)
|
||
nodeSetMaterial(rope, hemp)
|
||
softSetMass(rope, 1.5)
|
||
softPin(rope, 4, 4, -1, crane)
|
||
----
|
||
|
||
[#nodesetmesh]
|
||
==== nodeSetMesh
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetMesh(node, mesh)
|
||
nodeSetMesh(node, mesh, material)
|
||
----
|
||
|
||
Dà al nodo una mesh da disegnare, con un materiale o, se omesso, con l'aspetto
|
||
predefinito (bianco, ruvido a metà). Entrambi gli argomenti devono essere handle
|
||
ancora validi; una mesh o un materiale che non esiste termina lo script.
|
||
Qualsiasi nodo può disegnare, compreso un nodo dentro un'istanza di modello, e un
|
||
numero qualsiasi di nodi può condividere una mesh. I pesi dei morph del nodo vengono
|
||
dimensionati sui target della nuova mesh e partono da `0`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<meshbox,meshBox>>, <<materialnew,materialNew>>, <<nodesetmaterial,nodeSetMaterial>>, <<nodesetvisible,nodeSetVisible>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La carrozzeria di un'auto con quattro ruote, ognuna un cilindro girato in modo che il suo X sia l'assale.
|
||
local car = nodeNew()
|
||
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
|
||
nodeSetPosition(car, 0, 0.9, 0)
|
||
local tireMesh = meshCylinder(0.35, 0.25, 16)
|
||
for _, p in ipairs({ { -0.95, -0.2, -1.4 }, { 0.95, -0.2, -1.4 }, { -0.95, -0.2, 1.4 }, { 0.95, -0.2, 1.4 } }) do
|
||
local wheel = nodeNew(car)
|
||
nodeSetPosition(wheel, p[1], p[2], p[3])
|
||
local tire = nodeNew(wheel)
|
||
nodeSetMesh(tire, tireMesh, rubber)
|
||
nodeSetRotation(tire, 0, 0, 90)
|
||
end
|
||
----
|
||
|
||
[#nodesetmorph]
|
||
==== nodeSetMorph
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetMorph(node, nameOrIndex, weight)
|
||
----
|
||
|
||
Imposta quanto di un morph target (blend shape) mostra la mesh del nodo, per nome o
|
||
per numero a partire da `1`; i pesi vanno di solito da `0` a `1` e più target si
|
||
sommano. Il nodo è quello che porta la mesh, che per un modello è un figlio della
|
||
radice dell'istanza (`nodeFind` con il nome della mesh, o `nodeGetChildren`). Un
|
||
target che la mesh non ha termina lo script. Un'animazione con un canale dei pesi
|
||
scrive i pesi a ogni fotogramma mentre è in riproduzione, sovrascrivendo ciò che hai
|
||
impostato.
|
||
|
||
*Note:* In un fotogramma si applicano fino a otto target con peso diverso da zero per
|
||
mesh; se sono di più, vengono usati gli otto più forti per peso assoluto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodegetmorph,nodeGetMorph>>, <<nodegetmorphs,nodeGetMorphs>>, <<animationplay,animationPlay>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Sincronizzazione labiale da una tabella di aperture della bocca per fotogramma del disco.
|
||
local face = nodeFind("Head", hero)
|
||
|
||
function onOverlayUpdate()
|
||
nodeSetMorph(face, "mouthOpen", mouthByFrame[discGetFrame()] or 0)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#nodesetname]
|
||
==== nodeSetName
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetName(node, name)
|
||
----
|
||
|
||
Dà un nome al nodo perché `nodeFind` possa trovarlo in seguito, sostituendo
|
||
qualsiasi nome avesse (compreso il nome glTF di un nodo di un modello). I nomi non
|
||
devono essere per forza unici; `nodeFind` restituisce la prima corrispondenza
|
||
nell'ordine della ricerca in profondità.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodefind,nodeFind>>, <<nodegetname,nodeGetName>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Le casse generate ricevono un nome perché lo script del livello possa trovarle per posizione.
|
||
for slot = 1, 6 do
|
||
local crate = nodeNew()
|
||
nodeSetMesh(crate, crateMesh, wood)
|
||
nodeSetPosition(crate, slot * 1.5 - 5, 0.5, -3)
|
||
nodeSetName(crate, "crate" .. slot)
|
||
end
|
||
nodeSetVisible(nodeFind("crate4"), false)
|
||
----
|
||
|
||
[#nodesetparent]
|
||
==== nodeSetParent
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetParent(node, parent)
|
||
----
|
||
|
||
Sposta il nodo, con tutto il suo sottoalbero, sotto un altro genitore, di cui
|
||
diventa l'ultimo figlio. La posizione, la rotazione e la scala locali del nodo
|
||
restano come sono, quindi ora si trova a quella distanza dal nuovo genitore e si
|
||
muove con esso; passa `0` per riportarlo sotto la radice. La radice non si può
|
||
spostare, e un nodo non può andare sotto se stesso o sotto uno dei suoi discendenti;
|
||
entrambe le cose terminano lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodegetparent,nodeGetParent>>, <<nodenew,nodeNew>>, <<nodesetposition,nodeSetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Raccogliere una cassa: da ora viaggia nella mano, e più tardi viene posata.
|
||
function pickUp(crate)
|
||
nodeSetParent(crate, nodeFind("hand_R", hero))
|
||
nodeSetPosition(crate, 0, 0.3, 0)
|
||
nodeSetRotation(crate, 0, 0, 0)
|
||
end
|
||
|
||
function putDown(crate, x, y, z)
|
||
nodeSetParent(crate, 0)
|
||
nodeSetPosition(crate, x, y, z)
|
||
end
|
||
----
|
||
|
||
[#nodesetposition]
|
||
==== nodeSetPosition
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetPosition(node, x, y, z)
|
||
----
|
||
|
||
Mette il nodo in `x, y, z` nello spazio del suo genitore; per un nodo sotto la
|
||
radice è lo spazio del mondo. I figli lo seguono. Un nodo con un corpo dinamico
|
||
viene poi mosso dalla fisica; imposta la sua posizione prima di `bodyNew`, o
|
||
teletrasportalo con le chiamate dei corpi.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodemove,nodeMove>>, <<nodegetposition,nodeGetPosition>>, <<nodesetrotation,nodeSetRotation>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Casse lasciate cadere lungo un crinale, ognuna sei unità sopra il terreno in quel punto.
|
||
for i = 1, 10 do
|
||
local crate = nodeNew()
|
||
local x, z = -20 + i * 4, -12
|
||
nodeSetMesh(crate, meshBox(1, 1, 1), wood)
|
||
nodeSetPosition(crate, x, (terrainGetHeight(hills, x, z) or 0) + 6, z)
|
||
nodeSetRotation(crate, i * 17, i * 31, 0)
|
||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 1, 1, 1)
|
||
end
|
||
----
|
||
|
||
[#nodesetquaternion]
|
||
==== nodeSetQuaternion
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetQuaternion(node, x, y, z, w)
|
||
----
|
||
|
||
Imposta la rotazione del nodo relativa al suo genitore come quaternione, per valori
|
||
venuti da `nodeGetQuaternion`, da un modello o dai tuoi calcoli. Il quaternione
|
||
viene normalizzato in ingresso, quindi non deve per forza avere lunghezza unitaria.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetrotation,nodeSetRotation>>, <<nodegetquaternion,nodeGetQuaternion>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il fantasma copia esattamente la posa dell'eroe, con un attimo di ritardo.
|
||
local qx, qy, qz, qw = nodeGetQuaternion(hero)
|
||
local px, py, pz = nodeGetPosition(hero)
|
||
history[#history + 1] = { px, py, pz, qx, qy, qz, qw }
|
||
if #history > 30 then
|
||
local old = table.remove(history, 1)
|
||
nodeSetPosition(ghost, old[1], old[2], old[3])
|
||
nodeSetQuaternion(ghost, old[4], old[5], old[6], old[7])
|
||
end
|
||
----
|
||
|
||
[#nodesetrotation]
|
||
==== nodeSetRotation
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetRotation(node, x, y, z)
|
||
----
|
||
|
||
Imposta la rotazione del nodo relativa al suo genitore come angoli di Eulero in
|
||
gradi, sostituendo quella che aveva: il nodo viene prima girato attorno al suo Y
|
||
(imbardata), poi inclinato attorno al suo X (beccheggio), poi ruotato in rollio
|
||
attorno al suo Z. Gli angoli positivi ruotano in senso antiorario guardando lungo
|
||
l'asse verso l'origine, quindi `nodeSetRotation(plane, 90, 0, 0)` solleva un piano
|
||
orizzontale perché guardi verso +Z.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<noderotate,nodeRotate>>, <<nodesetquaternion,nodeSetQuaternion>>, <<nodelookat,nodeLookAt>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Lo schermo di un monitor su un supporto, rivolto verso la telecamera a +Z.
|
||
local monitor = nodeNew()
|
||
nodeSetMesh(monitor, meshPlane(2.0, 1.5), screen)
|
||
nodeSetPosition(monitor, -3, 2.1, 1.06)
|
||
nodeSetRotation(monitor, 90, 0, 0)
|
||
-- Una rampa inclinata di 25 gradi attorno a Z.
|
||
local ramp = nodeNew()
|
||
nodeSetMesh(ramp, meshBox(6, 0.2, 3), asphalt)
|
||
nodeSetRotation(ramp, 0, 0, 25)
|
||
----
|
||
|
||
[#nodesetscale]
|
||
==== nodeSetScale
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetScale(node, scale)
|
||
nodeSetScale(node, x, y, z)
|
||
----
|
||
|
||
Scala il nodo e tutto ciò che sta sotto di esso, in modo uniforme con un fattore o
|
||
per asse con tre. I modelli caricati sono spesso in centimetri o in metri; scala una
|
||
volta la radice dell'istanza per adattarla alla tua scena e piazza le cose sotto di
|
||
essa nelle unità proprie del modello. Anche gli sprite e il testo sul nodo vengono
|
||
scalati, oltre alla loro `height`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodegetscale,nodeGetScale>>, <<modelinstance,modelInstance>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La volpe è modellata in centimetri: un centesimo la porta in metri.
|
||
local fox = modelInstance(foxModel)
|
||
nodeSetParent(fox, player)
|
||
nodeSetScale(fox, 0.01)
|
||
nodeSetRotation(fox, 0, 90, 0)
|
||
-- Una sfera schiacciata per una pozzanghera.
|
||
local puddle = nodeNew()
|
||
nodeSetMesh(puddle, meshSphere(0.5, 24), water)
|
||
nodeSetScale(puddle, 1.5, 0.05, 1.2)
|
||
----
|
||
|
||
[#nodesetshadow]
|
||
==== nodeSetShadow
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetShadow(node, casts)
|
||
----
|
||
|
||
Se la mesh del nodo viene disegnata nelle mappe delle ombre; `true` per ogni nodo
|
||
nuovo. Disattivalo per la mesh attorno a una luce (una lampadina, una fiamma), così
|
||
non fa ombra alla propria luce, e per qualsiasi cosa che brilla. Il nodo viene
|
||
comunque disegnato e riceve comunque le ombre degli altri. Si applica solo al nodo,
|
||
non ai suoi figli.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<lightsetshadow,lightSetShadow>>, <<nodesetvisible,nodeSetVisible>>, <<materialsetunlit,materialSetUnlit>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una lampadina dentro il suo paralume: il vetro non proietta nulla, il paralume sì.
|
||
local shade = nodeNew(stand)
|
||
nodeSetMesh(shade, meshCone(0.45, 0.5, 24), brass)
|
||
nodeSetPosition(shade, 0, 0.9, 0)
|
||
local bulbMesh = nodeNew(stand)
|
||
nodeSetMesh(bulbMesh, meshSphere(0.08, 12), glow)
|
||
nodeSetPosition(bulbMesh, 0, 0.8, 0)
|
||
nodeSetShadow(bulbMesh, false)
|
||
local bulb = lightNew(LIGHT_POINT)
|
||
nodeSetParent(bulb, bulbMesh)
|
||
lightSetShadow(bulb, true)
|
||
----
|
||
|
||
[#nodesetsprite]
|
||
==== nodeSetSprite
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetSprite(node, sprite)
|
||
nodeSetSprite(node, sprite, height)
|
||
nodeSetSprite(node, sprite, height, lit)
|
||
nodeSetSprite(node, nil)
|
||
nodeSetSprite(node)
|
||
----
|
||
|
||
Mostra uno sprite caricato sul nodo: un quad alto `height` unità del mondo
|
||
(predefinito `1`) e largo quanto lo rendono le proporzioni dell'immagine, miscelato
|
||
secondo l'alfa dell'immagine, disegnato da entrambi i lati e illuminato dalla scena,
|
||
a meno che `lit` non sia `false`. Ogni fotogramma di uno sprite animato viene
|
||
caricato sulla GPU e si vede il primo; `nodeSetSpriteFrame` ne sceglie un altro. Il
|
||
quad diventa la mesh del nodo con un materiale privato, sostituendo qualsiasi mesh,
|
||
sprite o testo avesse; `nil` o nessun secondo argomento toglie l'immagine e lascia
|
||
il nodo vuoto. Termina lo script quando il renderer 3D non è disponibile.
|
||
|
||
*Parametri:*
|
||
|
||
* `sprite` -- un handle di `spriteLoad`, o `nil` per toglierlo.
|
||
* `height` -- l'altezza del quad in unità del mondo, predefinita `1`; la scala del nodo la moltiplica.
|
||
* `lit` -- `true` (predefinito) ombreggia l'immagine con le luci e le ombre della scena; `false` la mostra così com'è.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetspriteframe,nodeSetSpriteFrame>>, <<nodesetbillboard,nodeSetBillboard>>, <<spriteload,spriteLoad>>, <<nodesettext,nodeSetText>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un'icona di cassa che fluttua sopra la volpe, girandosi verso il giocatore.
|
||
local icon = nodeNew()
|
||
nodeSetParent(icon, fox)
|
||
nodeSetPosition(icon, 0, 150, 0)
|
||
nodeSetScale(icon, 50)
|
||
nodeSetSprite(icon, spriteLoad("crate.png"), 0.5)
|
||
nodeSetBillboard(icon, BILLBOARD_Y)
|
||
-- Un bagliore non illuminato che appare uguale anche in ombra.
|
||
local flare = nodeNew(torchTip)
|
||
nodeSetSprite(flare, spriteLoad("flare.png"), 0.3, false)
|
||
nodeSetBillboard(flare, BILLBOARD_ALL)
|
||
----
|
||
|
||
[#nodesetspriteframe]
|
||
==== nodeSetSpriteFrame
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetSpriteFrame(node, frame)
|
||
----
|
||
|
||
Quale fotogramma dello sprite animato del nodo si vede, contando da `0`. Lo sprite
|
||
del nodo non avanza da solo; fallo avanzare da `onOverlayUpdate`. Un nodo senza
|
||
sprite, o un fotogramma che lo sprite non ha, termina lo script; il testo e uno
|
||
sprite fisso hanno solo il fotogramma `0`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetsprite,nodeSetSprite>>, <<spriteload,spriteLoad>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Lo sprite di un fuoco da campo che scorre i suoi fotogrammi a dieci al secondo.
|
||
local fire = nodeNew()
|
||
nodeSetPosition(fire, 2, 0.4, -1)
|
||
nodeSetSprite(fire, spriteLoad("campfire.gif"), 0.8, false)
|
||
nodeSetBillboard(fire, BILLBOARD_Y)
|
||
|
||
function onOverlayUpdate()
|
||
nodeSetSpriteFrame(fire, math.floor(os.clock() * 10) % fireFrames)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#nodesettext]
|
||
==== nodeSetText
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetText(node, text)
|
||
nodeSetText(node, text, height)
|
||
nodeSetText(node, nil)
|
||
nodeSetText(node)
|
||
----
|
||
|
||
Mostra una riga di testo sul nodo, resa nel font selezionato (`fontSelect`) alla
|
||
qualità corrente (`fontQuality`) nel colore di primo piano (`colorForeground`), su
|
||
un quad alto `height` unità del mondo (predefinito `1`) e largo quanto lo rende il
|
||
testo renderizzato. Non è illuminato, è miscelato e viene disegnato da entrambi i
|
||
lati: etichette con il nome, insegne, un punteggio sopra un tavolo. Come uno sprite
|
||
diventa la mesh del nodo, sostituendo qualsiasi mesh, sprite o testo ci fosse;
|
||
richiamala per cambiare il testo, con `nil` o senza secondo argomento per toglierlo.
|
||
Senza un font selezionato, o senza il renderer 3D, termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetsprite,nodeSetSprite>>, <<nodesetbillboard,nodeSetBillboard>>, <<fontselect,fontSelect>>, <<colorforeground,colorForeground>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un'insegna fissa vicino alla lampada, e un punteggio che si aggiorna quando cambia.
|
||
fontSelect(signFont)
|
||
colorForeground(255, 240, 200)
|
||
local sign = nodeNew()
|
||
nodeSetPosition(sign, 4.2, 1.2, -1.2)
|
||
nodeSetRotation(sign, 0, -30, 0)
|
||
nodeSetText(sign, "LAMP", 0.5)
|
||
|
||
function setScore(points)
|
||
nodeSetText(scoreboard, string.format("%06d", points), 0.4)
|
||
end
|
||
----
|
||
|
||
[#nodesetvisible]
|
||
==== nodeSetVisible
|
||
|
||
[source,text]
|
||
----
|
||
nodeSetVisible(node, visible)
|
||
----
|
||
|
||
Nasconde o mostra il nodo e tutto ciò che sta sotto di esso; l'impostazione propria
|
||
di un figlio vale ancora quando il genitore torna visibile. Le mesh nascoste non
|
||
vengono disegnate e le luci nascoste smettono di risplendere. Usalo per tenere
|
||
pronto un indicatore o un oggetto di scena di riserva senza eliminarlo e
|
||
ricostruirlo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<nodesetmesh,nodeSetMesh>>, <<nodedelete,nodeDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un indicatore che compare dove la palla ha colpito la racchetta l'ultima volta.
|
||
local mark = nodeNew()
|
||
nodeSetMesh(mark, meshSphere(0.08, 12), markLook)
|
||
nodeSetVisible(mark, false)
|
||
|
||
function onCollision(a, b, x, y, z)
|
||
if (a == ball and b == paddle) or (a == paddle and b == ball) then
|
||
nodeSetPosition(mark, x, y, z)
|
||
nodeSetVisible(mark, true)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#overlay]
|
||
=== Overlay
|
||
|
||
L'overlay è una superficie RGBA a 32 bit composta sopra il video a ogni fotogramma; tutto il disegno 2D finisce lì. Le sue coordinate sono la risoluzione dell'overlay impostata da `overlaySetResolution`, che per impostazione predefinita è metà della larghezza e dell'altezza del video, e vengono scalate alla finestra al momento della visualizzazione. Disegna da `onOverlayUpdate` (o da `singeMain` nel modello <<threaded,a thread>>) e restituisci `OVERLAY_UPDATED` quando qualcosa è cambiato. Le primitive `overlay*` disegnano contorni di un pixel nel colore di primo piano corrente e tagliano senza avvisare tutto ciò che cade fuori dalla superficie. `overlayPrint` è l'eccezione: accetta coordinate in celle di carattere e copia il font della console incorporato così com'è, ignorando le impostazioni del colore.
|
||
|
||
[#overlaybanner]
|
||
==== overlayBanner
|
||
|
||
[source,text]
|
||
----
|
||
overlayBanner(text [, height])
|
||
----
|
||
|
||
Estensione di Hypseus. Mostra un breve messaggio sopra l'immagine per circa trenta fotogrammi disegnati e poi lo toglie di nuovo. `height` è il punto in cui si trova, come percentuale dall'alto dell'immagine, da 1 a 95; il valore predefinito è 47, quello di Hypseus stesso, e un numero fuori da quell'intervallo viene sostituito dal valore predefinito invece di essere rifiutato.
|
||
|
||
Un messaggio più lungo di 60 caratteri viene ignorato, esattamente come lo ignora Hypseus. Il banner viene disegnato dallo stesso documento distribuito con Singe che usano i sottotitoli (`Singe/subtitle.rml`, con lo stile di `Singe/subtitle.rcss`), quindi sembra un sottotitolo e prende lo stile insieme a loro; finché è visibile prende il posto di qualsiasi sottotitolo, e il sottotitolo ritorna quando il banner scade. Il testo viene disegnato come testo, quindi `<`, `>` e `&` arrivano sullo schermo così come sono.
|
||
|
||
*Parametri:*
|
||
|
||
* `text` -- stringa; fino a 60 caratteri.
|
||
* `height` -- numero facoltativo; da 1 a 95, una percentuale dall'alto dell'immagine. Predefinito 47.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<srtload,srtLoad>>, <<subtitles,Sottotitoli>>, <<overlayprint,overlayPrint>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Dice al giocatore cos'è appena successo senza costruire un HUD apposta.
|
||
overlayBanner("Extra life!", 20)
|
||
----
|
||
|
||
[#overlaybox]
|
||
==== overlayBox
|
||
|
||
[source,text]
|
||
----
|
||
overlayBox(x1, y1, x2, y2)
|
||
----
|
||
|
||
Disegna il contorno del rettangolo allineato agli assi con angoli opposti `(x1, y1)` e `(x2, y2)`, come quattro linee di un pixel nel colore di primo piano. Entrambi gli angoli sono inclusi e si possono dare in qualsiasi ordine. Non esiste una variante piena; riempi tutto l'overlay con `overlayClear` su un colore di sfondo, o un'area con uno sprite stirato.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<overlayline,overlayLine>>, <<colorforeground,colorForeground>>, <<overlayclear,overlayClear>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Incornicia l'overlay di rosso e disegna il contorno di ogni blocco delle piattaforme.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
colorForeground(255, 0, 0, 255)
|
||
overlayBox(0, 0, overlayGetWidth() - 1, overlayGetHeight() - 1)
|
||
colorForeground(90, 90, 110, 255)
|
||
for _, b in ipairs(blocks) do
|
||
overlayBox(b.x - b.w / 2, b.y - b.h / 2, b.x + b.w / 2, b.y + b.h / 2)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#overlaycircle]
|
||
==== overlayCircle
|
||
|
||
[source,text]
|
||
----
|
||
overlayCircle(x, y, radius)
|
||
----
|
||
|
||
Disegna il contorno di un cerchio di un pixel nel colore di primo piano, centrato in `(x, y)`, con l'algoritmo del punto medio. Un raggio di `0` o meno non disegna nulla. Coordinate e raggio sono interi in unità dell'overlay; le parti frazionarie vengono troncate.
|
||
|
||
*Parametri:*
|
||
|
||
* `x`, `y` -- il centro in coordinate dell'overlay.
|
||
* `radius` -- il raggio in unità dell'overlay.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<overlayellipse,overlayEllipse>>, <<overlayplot,overlayPlot>>, <<colorforeground,colorForeground>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un mirino a croce che diventa rosso mentre il giocatore viene colpito.
|
||
function onOverlayUpdate()
|
||
if hitTimer > 0 then
|
||
colorForeground(255, 0, 0, 255)
|
||
hitTimer = hitTimer - 1
|
||
else
|
||
colorForeground(255, 255, 255, 255)
|
||
end
|
||
overlayCircle(aimX, aimY, 8)
|
||
overlayLine(aimX - 12, aimY, aimX + 12, aimY)
|
||
overlayLine(aimX, aimY - 12, aimX, aimY + 12)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#overlayclear]
|
||
==== overlayClear
|
||
|
||
[source,text]
|
||
----
|
||
overlayClear()
|
||
----
|
||
|
||
Riempie l'intero overlay con il colore di sfondo corrente. Quando il colore è lo stesso dell'ultima pulizia, viene riempito di nuovo solo ciò che è stato disegnato da allora, quindi pulire un overlay che contiene poche righe di testo non costa quasi nulla, qualunque sia la sua dimensione. Lo sfondo parte come nero trasparente, quindi senza una chiamata a `colorBackground` questa funzione riporta l'overlay all'invisibile e il video si vede attraverso. Uno sfondo parzialmente opaco scurisce il video; uno del tutto opaco lo nasconde. La maggior parte dei giochi la chiama come prima cosa in `onOverlayUpdate`. Gli argomenti eventuali vengono ignorati.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<colorbackground,colorBackground>>, <<onoverlayupdate,onOverlayUpdate>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Scurisce a metà luminosità il video in pausa dietro il menu di servizio.
|
||
function onOverlayUpdate()
|
||
colorBackground(0, 0, 0, 127)
|
||
overlayClear()
|
||
spriteDraw(serviceTitle, (overlayGetWidth() - spriteGetWidth(serviceTitle)) / 2, 25)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#overlayellipse]
|
||
==== overlayEllipse
|
||
|
||
[source,text]
|
||
----
|
||
overlayEllipse(x1, y1, x2, y2)
|
||
----
|
||
|
||
Disegna il contorno di un'ellisse di un pixel nel colore di primo piano, inscritta nel rettangolo allineato agli assi con angoli opposti `(x1, y1)` e `(x2, y2)`. Gli angoli si possono dare in qualsiasi ordine. Un rettangolo di larghezza e altezza uguali dà un cerchio, quindi questo è anche il modo di disegnare un cerchio a partire dal rettangolo che lo contiene invece che dal suo centro.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<overlaycircle,overlayCircle>>, <<overlaybox,overlayBox>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Evidenzia il cabinato selezionato con un ovale che si adatta al suo riquadro.
|
||
function onOverlayUpdate()
|
||
spriteDraw(cabinetSprite, CABINET_X, CABINET_Y)
|
||
if selected == CABINET then
|
||
colorForeground(255, 220, 0, 255)
|
||
overlayEllipse(CABINET_X - 6, CABINET_Y - 6, CABINET_X + CABINET_W + 6, CABINET_Y + CABINET_H + 6)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#overlaygetfontheight]
|
||
==== overlayGetFontHeight
|
||
|
||
[source,text]
|
||
----
|
||
result = overlayGetFontHeight()
|
||
----
|
||
|
||
Restituisce l'altezza in pixel di una cella di carattere del font della console incorporato, che è l'unità in cui conta `overlayPrint`. Il font della console è un'unica immagine incorporata di 256 glifi, caricata una volta prima che parta qualsiasi script, quindi questo valore non cambia mai mentre il programma è in esecuzione. Gli argomenti eventuali vengono ignorati.
|
||
|
||
*Restituisce:* l'altezza intera di una cella in pixel.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<overlaygetfontwidth,overlayGetFontWidth>>, <<overlayprint,overlayPrint>>, <<overlaygetheight,overlayGetHeight>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Quante righe di testo della console ci stanno, qualunque sia la dimensione dell'overlay.
|
||
ROWS = overlayGetHeight() // overlayGetFontHeight()
|
||
overlayPrint(0, ROWS - 1, "along the bottom")
|
||
----
|
||
|
||
[#overlaygetfontwidth]
|
||
==== overlayGetFontWidth
|
||
|
||
[source,text]
|
||
----
|
||
result = overlayGetFontWidth()
|
||
----
|
||
|
||
Restituisce la larghezza in pixel di una cella di carattere del font della console incorporato, che è l'unità in cui conta `overlayPrint`. Usala con `overlayGetWidth` per calcolare quante colonne ci stanno, o per posizionare il testo della console rispetto a qualcosa disegnato in pixel. Dividi con `//` invece che con `/`: la divisione normale di Lua dà un numero frazionario, e una posizione in celle deve essere intera. Gli argomenti eventuali vengono ignorati.
|
||
|
||
*Restituisce:* la larghezza intera di una cella in pixel.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<overlaygetfontheight,overlayGetFontHeight>>, <<overlayprint,overlayPrint>>, <<overlaygetwidth,overlayGetWidth>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Centra una riga di testo della console senza scrivere nel codice il numero di colonne.
|
||
local columns = overlayGetWidth() // overlayGetFontWidth()
|
||
local text = "GAME OVER"
|
||
|
||
overlayPrint((columns - #text) // 2, 10, text)
|
||
----
|
||
|
||
[#overlaygetheight]
|
||
==== overlayGetHeight
|
||
|
||
[source,text]
|
||
----
|
||
result = overlayGetHeight()
|
||
----
|
||
|
||
Restituisce l'altezza della superficie dell'overlay in unità dell'overlay: l'ultimo valore dato a `overlaySetResolution`, o metà dell'altezza del video prima di qualsiasi chiamata. Leggila all'avvio, dopo aver impostato la risoluzione, per disporre le posizioni in base all'area di disegno effettiva. Gli argomenti eventuali vengono ignorati.
|
||
|
||
*Restituisce:* l'altezza, un intero.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<overlaygetwidth,overlayGetWidth>>, <<overlaysetresolution,overlaySetResolution>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ancora il riquadro del testo al margine inferiore, qualunque sia la dimensione dell'overlay.
|
||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||
TEXT_Y = 200
|
||
TEXT_H = overlayGetHeight() - MARGIN_Y - TEXT_Y
|
||
TEXT_LINE_LIMIT = math.floor(TEXT_H / LINE_HEIGHT)
|
||
----
|
||
|
||
[#overlaygetwidth]
|
||
==== overlayGetWidth
|
||
|
||
[source,text]
|
||
----
|
||
result = overlayGetWidth()
|
||
----
|
||
|
||
Restituisce la larghezza della superficie dell'overlay in unità dell'overlay: l'ultimo valore dato a `overlaySetResolution`, o metà della larghezza del video prima di qualsiasi chiamata. Usala per allineare a destra o centrare gli elementi invece di scrivere una larghezza fissa nel codice. Gli argomenti eventuali vengono ignorati.
|
||
|
||
*Restituisce:* la larghezza, un intero.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<overlaygetheight,overlayGetHeight>>, <<overlaysetresolution,overlaySetResolution>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Allinea a destra il marquee e centra il titolo, indipendentemente dalla risoluzione.
|
||
function onOverlayUpdate()
|
||
local marqueeX = overlayGetWidth() - MARGIN_X - spriteGetWidth(marqueeSprite)
|
||
spriteDraw(marqueeSprite, marqueeX, MARGIN_Y)
|
||
spriteDraw(titleSprite, (overlayGetWidth() - spriteGetWidth(titleSprite)) / 2, 25)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#overlayline]
|
||
==== overlayLine
|
||
|
||
[source,text]
|
||
----
|
||
overlayLine(x1, y1, x2, y2)
|
||
----
|
||
|
||
Disegna una linea di Bresenham di un pixel nel colore di primo piano da `(x1, y1)` a `(x2, y2)`, entrambi gli estremi inclusi. I pixel fuori dall'overlay vengono scartati, quindi gli estremi possono trovarsi fuori dalla superficie. Le coordinate vengono troncate a interi.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<overlayplot,overlayPlot>>, <<overlaybox,overlayBox>>, <<colorforeground,colorForeground>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Disegna il laser dall'arma al punto di mira, e un semplice reticolo di mira.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
if firing then
|
||
colorForeground(255, 64, 64, 255)
|
||
overlayLine(GUN_X, GUN_Y, aimX, aimY)
|
||
end
|
||
colorForeground(255, 255, 255, 255)
|
||
overlayCircle(aimX, aimY, 6)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#overlayplot]
|
||
==== overlayPlot
|
||
|
||
[source,text]
|
||
----
|
||
overlayPlot(x, y)
|
||
----
|
||
|
||
Imposta un pixel dell'overlay al colore di primo piano. Un punto fuori dalla superficie viene ignorato. Ogni chiamata blocca e sblocca l'overlay, quindi disegnare migliaia di punti per fotogramma da Lua è lento; usa uno sprite per qualsiasi cosa abbia una struttura e tieni questa funzione per stelle, scintille e segni di debug.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<overlayline,overlayLine>>, <<colorforeground,colorForeground>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un campo di stelle che scintillano lentamente dietro il titolo.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
for _, star in ipairs(stars) do
|
||
local shade = 128 + math.floor(127 * math.sin(frameCount * star.speed))
|
||
colorForeground(shade, shade, shade, 255)
|
||
overlayPlot(star.x, star.y)
|
||
end
|
||
frameCount = frameCount + 1
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#overlayprint]
|
||
==== overlayPrint
|
||
|
||
[source,text]
|
||
----
|
||
overlayPrint(column, row, text)
|
||
----
|
||
|
||
Stampa una stringa con il font della console a larghezza fissa incorporato nel motore. A differenza di ogni altra chiamata di disegno, la posizione è una cella di carattere: il motore moltiplica `column` e `row` per la larghezza e l'altezza del glifo, quindi `(0, 0)` è la cella in alto a sinistra e `(1, 0)` è un glifo più a destra. I glifi vengono copiati dall'immagine del font così come sono; i colori di primo piano e di sfondo correnti non vengono applicati, e solo lo sfondo del font stesso è trasparente. Ogni byte della stringa seleziona uno dei 256 glifi del font, quindi il testo viene trattato come byte, non come UTF-8. I caratteri che andrebbero oltre il bordo destro dell'overlay vengono scartati, e il testo che inizia oltre il bordo non disegna nulla.
|
||
|
||
*Parametri:*
|
||
|
||
* `column`, `row` -- la cella di carattere, `0` in alto a sinistra.
|
||
* `text` -- la stringa da stampare.
|
||
|
||
*Note:* Il font della console è pensato per la diagnostica e le schermate di servizio. Usa `fontPrint` o `fontToSprite` per il testo colorato o proporzionale. La dimensione di una cella in pixel è `overlayGetFontWidth` per `overlayGetFontHeight`, quindi `overlayGetWidth() // overlayGetFontWidth()` è il numero di colonne che ci stanno.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<fontprint,fontPrint>>, <<overlayclear,overlayClear>>, <<colorbackground,colorBackground>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Diagnostica nell'angolo mentre un download si completa.
|
||
function onOverlayUpdate()
|
||
colorBackground(0, 0, 0, 127)
|
||
overlayClear()
|
||
overlayPrint(1, 1, "FRAME " .. discGetFrame())
|
||
overlayPrint(1, 2, download.url .. " " .. tostring(download.finished))
|
||
if download.finished then
|
||
overlayPrint(1, 3, download.result)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#overlaysetmonochrome]
|
||
==== overlaySetMonochrome
|
||
|
||
[source,text]
|
||
----
|
||
overlaySetMonochrome(enabled)
|
||
----
|
||
|
||
Estensione di Hypseus. Mostra l'overlay in grigio: prima di ogni caricamento sulla GPU il motore ne converte una copia in luma con i pesi BT.601, lasciando il canale alfa com'è (Hypseus gli applica una soglia). I pixel dell'overlay stesso restano a colori, quindi nulla di ciò che lo script rilegge cambia, e il disegno continua come al solito. Abbinala a `vldpSetMonochrome` per una modalità in bianco e nero. Un argomento non booleano termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<vldpsetmonochrome,vldpSetMonochrome>>, <<overlayclear,overlayClear>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una sola impostazione comanda sia il disco sia l'overlay.
|
||
function applyChroma(color)
|
||
vldpSetMonochrome(not color)
|
||
overlaySetMonochrome(not color)
|
||
end
|
||
----
|
||
|
||
[#overlaysetresolution]
|
||
==== overlaySetResolution
|
||
|
||
[source,text]
|
||
----
|
||
overlaySetResolution(width, height)
|
||
----
|
||
|
||
Sostituisce l'overlay con una nuova superficie della dimensione data e scarta tutto ciò che è stato disegnato finora. Da quel momento ogni coordinata dell'overlay, ogni posizione di sprite e ogni risultato di `overlayGetWidth` o `overlayGetHeight` è nel nuovo spazio, che il motore scala alla finestra al momento della visualizzazione. Chiamala una volta durante l'avvio, prima di disegnare; il valore predefinito è metà della risoluzione del video in ciascuna dimensione. Una larghezza o un'altezza di `0` o meno termina lo script con un errore. Risoluzioni più alte danno un posizionamento più fine e un testo più nitido, al prezzo di più lavoro di composizione per fotogramma; la dimensione nativa del disco, da `discGetWidth` e `discGetHeight`, è la scelta comune.
|
||
|
||
*Parametri:*
|
||
|
||
* `width` -- la nuova larghezza dell'overlay in pixel, maggiore di `0`.
|
||
* `height` -- la nuova altezza dell'overlay in pixel, maggiore di `0`.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<overlaygetwidth,overlayGetWidth>>, <<overlaygetheight,overlayGetHeight>>, <<discgetwidth,discGetWidth>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Si adegua al disco, poi dispone il menu in quello spazio di coordinate.
|
||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||
MARGIN_X = 16
|
||
MARGIN_Y = 16
|
||
MARQUEE_W = 200
|
||
MARQUEE_X = overlayGetWidth() - MARGIN_X - MARQUEE_W
|
||
CABINET_H = overlayGetHeight() - MARGIN_Y - CABINET_Y
|
||
----
|
||
|
||
[#setoverlayfullalpha]
|
||
==== setOverlayFullAlpha
|
||
|
||
[source,text]
|
||
----
|
||
setOverlayFullAlpha()
|
||
----
|
||
|
||
Estensione di Hypseus, accettata e ignorata. Hypseus copia il suo overlay con una soglia di `128` sul canale alfa, quindi un pixel semitrasparente diventa del tutto opaco o sparisce, e questa chiamata gli chiede invece di caricare la superficie così com'è e di miscelarla correttamente. L'overlay di Singe è sempre stato RGBA completo con un vero alfa per pixel, quindi ciò che questa chiamata chiede è già in vigore.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00
|
||
|
||
[#setoverlaylinearscale]
|
||
==== setOverlayLinearScale
|
||
|
||
[source,text]
|
||
----
|
||
setOverlayLinearScale(enabled)
|
||
----
|
||
|
||
Estensione di Hypseus. Sceglie come viene filtrato l'overlay nel suo percorso verso la finestra. Con `true`, il valore predefinito e quello che un ricaricamento ripristina, viene campionato linearmente, quindi un overlay a bassa risoluzione si ammorbidisce invece di sgranarsi a blocchi; con `false` viene preso il pixel più vicino, il che mantiene nitidi la pixel art, i contorni di un pixel e il testo piccolo. Il lineare è il valore predefinito perché è così che ogni versione di Singe ha disegnato l'overlay; Hypseus invece usa per impostazione predefinita il pixel più vicino, quindi un gioco portato da Hypseus che non chiama mai questa funzione apparirà leggermente più morbido finché non lo fa. Nel disegno non cambia nulla, solo la scalatura finale dalla risoluzione dell'overlay alla finestra, quindi tutto ciò che sta sull'overlay -- le primitive, i font, gli sprite e il livello 3D -- passa insieme per lo stesso filtro, e l'immagine del disco dietro di esso resta intatta. L'impostazione sopravvive a `overlaySetResolution`, che sostituisce l'overlay. Un argomento non booleano termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<overlaysetresolution,overlaySetResolution>>, <<spritequality,spriteQuality>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un overlay 320x240 su uno schermo 4K; il titolo dipinto morbido, la pixel art del gioco nitida.
|
||
overlaySetResolution(320, 240)
|
||
|
||
function showTitle()
|
||
setOverlayLinearScale(true)
|
||
mode = "title"
|
||
end
|
||
|
||
function startGame()
|
||
setOverlayLinearScale(false)
|
||
mode = "play"
|
||
end
|
||
----
|
||
|
||
[#setoverlayontop]
|
||
==== setOverlayOnTop
|
||
|
||
[source,text]
|
||
----
|
||
loaded, id = setOverlayOnTop(enabled)
|
||
----
|
||
|
||
Estensione di Hypseus. Sceglie se l'overlay e le GUI disegnate sopra l'immagine stanno sopra la grafica del bezel o sotto. Solo la grafica disegnata davanti all'immagine (`--bezelflip`) può trovarsi sopra di loro, quindi con la grafica dietro -- che è l'impostazione predefinita -- questa chiamata non cambia nulla. `false` è l'impostazione con cui parte un gioco, ed è quella predefinita di Hypseus stesso. Viene ricordata finché lo script non si ricarica.
|
||
|
||
L'impostazione viene registrata solo mentre è caricata una grafica; senza, la chiamata non fa nulla e risponde `false` e `"0"`, esattamente come Hypseus. Un argomento non booleano termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano; `true` mette l'overlay e le GUI sopra la grafica.
|
||
|
||
*Restituisce:* un booleano, che dice se è caricata una grafica, e il suo identificatore come stringa (`"0"` quando non ce n'è nessuna).
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<mainbezelloaded,mainBezelLoaded>>, <<guidrawscreen,guiDrawScreen>>, xref:Manual.adoc#bezels[Bezel]
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La cornice del cabinato copre in parte l'immagine, ma la riga del punteggio deve restare leggibile.
|
||
if mainBezelLoaded() then
|
||
setOverlayOnTop(true)
|
||
end
|
||
----
|
||
|
||
[#setoverlayopacity]
|
||
==== setOverlayOpacity
|
||
|
||
[source,text]
|
||
----
|
||
setOverlayOpacity(alpha)
|
||
----
|
||
|
||
Estensione di Hypseus. Quanto è opaco l'intero overlay, da `0` (invisibile) a `255` (come lo ha disegnato il gioco). Moltiplica l'alfa che i pixel dell'overlay hanno già, quindi uno sprite semitrasparente resta opaco la metà del resto; non cambia il contenuto dell'overlay, e non c'è nulla da ridisegnare.
|
||
|
||
Il livello viene limitato all'intervallo da `0` a `255` invece di essere rifiutato, e uno script parte da `255`. Viene ricordato finché lo script non si ricarica, e sopravvive a un cambio di risoluzione dell'overlay.
|
||
|
||
Le GUI, la scena 3D e la grafica del bezel vengono disegnate a parte e non ne sono toccate.
|
||
|
||
*Parametri:*
|
||
|
||
* `alpha` -- numero; da `0` a `255`.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<setoverlaylinearscale,setOverlayLinearScale>>, <<overlaysetresolution,overlaySetResolution>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fa sparire in dissolvenza l'intero HUD in mezzo secondo.
|
||
local fade = 255
|
||
function onOverlayUpdate()
|
||
fade = math.max(0, fade - 8)
|
||
setOverlayOpacity(fade)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#physicsref]
|
||
=== Fisica
|
||
|
||
Chiamate a livello del mondo: l'interrogazione con un raggio, la gravità, la modalità 2D, la pausa e il disegno di debug. La fisica viene inizializzata insieme al motore, quindi non c'è nulla da attivare prima del primo `bodyNew`; il mondo avanza di un passo sessanta volte al secondo, scandito dall'orologio, dopo le animazioni e prima del rendering della scena. Posizioni e distanze sono in unità del mondo e la gravità in unità al secondo quadrato. Vedi <<physics,Fisica>> in Sviluppo di giochi per come si combinano corpi, nodi e fotogramma.
|
||
|
||
[#physicsraycast]
|
||
==== physicsRaycast
|
||
|
||
[source,text]
|
||
----
|
||
node, hx, hy, hz, nx, ny, nz = physicsRaycast(x, y, z, dx, dy, dz)
|
||
node, hx, hy, hz, nx, ny, nz = physicsRaycast(x, y, z, dx, dy, dz, maxDistance)
|
||
----
|
||
|
||
Trova il primo corpo lungo un raggio che parte da `(x, y, z)` nella direzione `(dx, dy, dz)`, entro `maxDistance` unità del mondo. La direzione viene normalizzata, quindi va bene qualsiasi lunghezza, ma una direzione nulla non colpisce niente. I trigger e i giocatori vengono colpiti come qualsiasi altro corpo; un giocatore viene riportato tramite il suo nodo. Con `sceneUnproject` che fornisce due punti lungo la linea del mouse, questa è la selezione degli oggetti (picking); dritto verso il basso da un punto, è una sonda d'altezza.
|
||
|
||
*Parametri:*
|
||
|
||
* `x`, `y`, `z` -- l'inizio del raggio nello spazio del mondo.
|
||
* `dx`, `dy`, `dz` -- la sua direzione; conta solo la direzione.
|
||
* `maxDistance` -- fin dove cercare; predefinito `1000`, e `0` o meno significa il valore predefinito.
|
||
|
||
*Restituisce:* Il nodo del corpo, il punto colpito e la normale della superficie in quel punto, o un solo `nil` quando non viene colpito nulla.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<sceneunproject,sceneUnproject>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<linedraw,lineDraw>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Posa l'indicatore su qualunque terreno piano ci sia sotto il cursore.
|
||
function onOverlayUpdate()
|
||
local mx, my = mouseGetPosition(0)
|
||
local ox, oy, oz = sceneUnproject(mx, my, 0)
|
||
local fx, fy, fz = sceneUnproject(mx, my, 10)
|
||
local hit, hx, hy, hz, nx, ny, nz = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz, 60)
|
||
nodeSetVisible(marker, hit ~= nil and ny > 0.7)
|
||
if hit then
|
||
nodeSetPosition(marker, hx, hy + 0.05, hz)
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#physicsset2d]
|
||
==== physicsSet2D
|
||
|
||
[source,text]
|
||
----
|
||
physicsSet2D(planar)
|
||
----
|
||
|
||
Con `true`, i corpi creati da quel momento si muovono solo in X e Y e ruotano solo attorno a Z: il mondo di un gioco 2D, di solito in coordinate dell'overlay con la gravità che punta verso +Y, così le cose cadono verso il basso dello schermo. I corpi già creati mantengono la loro libertà, quindi chiamala prima di qualsiasi `bodyNew`; `false` torna al 3D completo per i corpi creati dopo. I giocatori sono diversi: finché la modalità è attiva, ogni giocatore, in qualunque momento sia stato creato, corre lungo X con la sua Z tenuta a `0` e `playerMove` accetta un solo valore. Il mondo 2D funziona senza GPU, Raspberry Pi 3 compreso.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<physicssetgravity,physicsSetGravity>>, <<bodynew,bodyNew>>, <<playermove,playerMove>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il mondo di un platform in pixel dell'overlay: gravità verso il basso dello schermo, un pavimento e dei muri.
|
||
physicsSet2D(true)
|
||
physicsSetGravity(0, 900, 0)
|
||
local width, height = overlayGetWidth(), overlayGetHeight()
|
||
floor = nodeNew()
|
||
nodeSetPosition(floor, width / 2, height - 10, 0)
|
||
bodyNew(floor, BODY_STATIC, SHAPE_BOX, width, 20, 50)
|
||
leftWall = nodeNew()
|
||
nodeSetPosition(leftWall, -10, height / 2, 0)
|
||
bodyNew(leftWall, BODY_STATIC, SHAPE_BOX, 20, height, 50)
|
||
----
|
||
|
||
[#physicssetdebug]
|
||
==== physicsSetDebug
|
||
|
||
[source,text]
|
||
----
|
||
physicsSetDebug(mask)
|
||
----
|
||
|
||
Disegna ciò che contiene la simulazione come linee sopra la scena a ogni fotogramma, finché la maschera è impostata: `DEBUG_SHAPES` per la forma in wireframe di ogni corpo in movimento (compresi i corpi morbidi, e i giocatori attivi in giallo), `DEBUG_CONSTRAINTS` per giunti, giunti dei ragdoll e vincoli dei veicoli con i loro limiti, `DEBUG_CONTACTS` per una croce rossa dove i corpi si sono toccati in questo passo, e `DEBUG_VELOCITIES` per una linea lungo la velocità di ogni corpo in movimento. Somma i flag; `DEBUG_ALL` li comprende tutti e quattro. Le forme dei corpi statici restano nascoste a meno di aggiungere `DEBUG_STATIC`, perché la mesh di un livello sono tantissime linee. `DEBUG_NONE` lo disattiva, e quando è disattivato non si disegna nulla e non costa nulla.
|
||
|
||
*Note:* Una build di Singe compilata senza il renderer di debug di Jolt stampa un messaggio nella console e non disegna nulla.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<linedraw,lineDraw>>, <<navdraw,navDraw>>, <<physicssetenabled,physicsSetEnabled>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Attiva e disattiva la visualizzazione della fisica con un tasto di debug; anche le forme statiche, perché il livello è piccolo.
|
||
function onInputPressed(switch)
|
||
if switch == SWITCH_BUTTON3 then
|
||
showPhysics = not showPhysics
|
||
if showPhysics then
|
||
physicsSetDebug(DEBUG_ALL + DEBUG_STATIC)
|
||
else
|
||
physicsSetDebug(DEBUG_NONE)
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
[#physicssetenabled]
|
||
==== physicsSetEnabled
|
||
|
||
[source,text]
|
||
----
|
||
physicsSetEnabled(enabled)
|
||
----
|
||
|
||
Mette in pausa la simulazione con `false` e la riprende con `true`. Durante la pausa non viene eseguito alcun passo: i corpi dinamici restano fermi e conservano le loro velocità, i corpi cinematici smettono di seguire i loro nodi, i giocatori smettono di muoversi, e non scatta nessun `onCollision` o `onTrigger`. Il tempo non si accumula durante la pausa, quindi riprendere non rigioca i passi persi. La pausa del gioco stesso fa lo stesso senza questa chiamata; usala per un menu, una scena d'intermezzo o un fermo immagine che lascia girare il resto del gioco.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<physicssetgravity,physicsSetGravity>>, <<bodyisresting,bodyIsResting>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Congela il mondo mentre l'inventario è aperto.
|
||
function openInventory()
|
||
inventoryOpen = true
|
||
physicsSetEnabled(false)
|
||
end
|
||
|
||
function closeInventory()
|
||
inventoryOpen = false
|
||
physicsSetEnabled(true)
|
||
end
|
||
----
|
||
|
||
[#physicssetgravity]
|
||
==== physicsSetGravity
|
||
|
||
[source,text]
|
||
----
|
||
physicsSetGravity(x, y, z)
|
||
----
|
||
|
||
Imposta l'accelerazione che sente ogni corpo dinamico e ogni giocatore, in unità del mondo al secondo quadrato lungo ciascun asse del mondo. Il valore predefinito è `0, -9.81, 0`, la gravità terrestre in metri; `0, 0, 0` è lo spazio, e un gioco 2D in pixel dell'overlay vuole qualcosa come `0, 900, 0`, perché la Y dell'overlay scende lungo lo schermo. La direzione opposta alla gravità è «su» per i giocatori: il verso in cui stanno in piedi, saltano e salgono i gradini.
|
||
|
||
Un cambio di gravità sveglia ogni corpo, compresi i corpi morbidi, quindi una cassa che si era fermata cade quando la gravità gira di lato, e la stoffa si muove quando cambia un vento fatto di gravità. Impostare di nuovo la stessa gravità non sveglia nulla.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodynew,bodyNew>>, <<physicsset2d,physicsSet2D>>, <<playersetgravityscale,playerSetGravityScale>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il livello lunare: un sesto dell'attrazione terrestre, e un salto più lungo in proporzione.
|
||
function startLevel(name)
|
||
if name == "moon" then
|
||
physicsSetGravity(0, -1.62, 0)
|
||
jumpSpeed = 3.0
|
||
else
|
||
physicsSetGravity(0, -9.81, 0)
|
||
jumpSpeed = 4.5
|
||
end
|
||
end
|
||
----
|
||
|
||
[#player]
|
||
=== Giocatore
|
||
|
||
Un giocatore è un controller del personaggio su un nodo: mentre un corpo va dove lo mandano le forze, un giocatore va dove dice lo script, camminando sui pavimenti, salendo i gradini, scivolando lungo i muri, viaggiando sulle piattaforme e spingendo da parte i corpi leggeri. I giocatori si indicano tramite il loro nodo, e un nodo porta un corpo o un giocatore, non entrambi. Dimensioni e velocità sono in unità del mondo (metri in una scena 3D, pixel dell'overlay in un mondo 2D) e le tolleranze del controller scalano con la forma, quindi una capsula di 30 pixel si comporta come una di 0,3 metri. Il giocatore guida la posizione del suo nodo a ogni passo e lascia la rotazione allo script. Ogni chiamata di questa sezione termina lo script quando il nodo non ha un giocatore. Vedi <<physics,Fisica>> in Sviluppo di giochi, Giocatori.
|
||
|
||
[#playernew]
|
||
==== playerNew
|
||
|
||
[source,text]
|
||
----
|
||
playerNew(node, radius, height)
|
||
playerNew(node, SHAPE_CAPSULE, radius, height)
|
||
playerNew(node, SHAPE_CYLINDER, radius, height)
|
||
playerNew(node, SHAPE_SPHERE, radius, 0)
|
||
playerNew(node, SHAPE_BOX, width, height, depth)
|
||
playerNew(node, SHAPE_HULL)
|
||
----
|
||
|
||
Mette un giocatore sul nodo, nel punto del mondo in cui il nodo si trova in questo momento: una capsula di quel raggio e di quell'altezza totale, o qualsiasi forma convessa di `bodyNew` con le sue misure, scalata secondo la scala del nodo nel mondo. La forma poggia sull'origine del nodo, quindi il nodo sono i piedi. Parte con una massa di `70` chilogrammi, una spinta di `300` newton, un limite di pendenza di `45` gradi, un'altezza del gradino di `0.3` scalata secondo la forma, una scala della gravità di `1` e le velocità di nuoto predefinite. Sostituisce qualsiasi corpo o giocatore avesse il nodo. Termina lo script per `SHAPE_MESH` (un giocatore ha bisogno di una forma convessa; usa `SHAPE_HULL`), quando la fisica non è disponibile su questa macchina, o quando il giocatore non si può creare (un inviluppo con troppo poca mesh, o già sedici giocatori).
|
||
|
||
*Parametri:*
|
||
|
||
* `radius`, `height` -- una capsula, raggio e altezza totale lungo l'alto, calotte comprese.
|
||
* `SHAPE_*` -- una costante di forma seguita dalle sue misure di `bodyNew`; `SHAPE_HULL` non ne prende e avvolge la mesh del nodo e tutto ciò che sta sotto di esso.
|
||
|
||
*Note:* Una chiamata con esattamente tre argomenti è sempre una capsula, quindi una sfera ha bisogno di un quarto argomento (un numero qualsiasi) dopo il raggio per essere letta come `SHAPE_SPHERE`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playermove,playerMove>>, <<playerdelete,playerDelete>>, <<bodynew,bodyNew>>, <<nodesetparent,nodeSetParent>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- L'eroe: una capsula con il modello Fox appeso al nodo, piedi a terra.
|
||
hero = nodeNew()
|
||
nodeSetPosition(hero, 0, 0.05, 0)
|
||
playerNew(hero, 0.3, 1.0)
|
||
playerSetStep(hero, 0.35)
|
||
fox = modelInstance(modelLoad(DIR .. "Models/Fox.glb"))
|
||
nodeSetParent(fox, hero)
|
||
nodeSetScale(fox, 0.01)
|
||
animationPlay(fox, "Walk", true)
|
||
-- Un robot è una scatola; gira gli angoli ad angolo retto e non può rotolare.
|
||
robot = nodeNew()
|
||
playerNew(robot, SHAPE_BOX, 0.8, 1.6, 0.6)
|
||
----
|
||
|
||
[#playerdelete]
|
||
==== playerDelete
|
||
|
||
[source,text]
|
||
----
|
||
playerDelete(node)
|
||
----
|
||
|
||
Rimuove il giocatore; il nodo e tutto ciò che vi è appeso sotto restano dove sono. Eliminare il nodo rimuove anche il suo giocatore, e lo stesso fa dare al nodo un corpo con `bodyNew`. Termina lo script quando il nodo non ha un giocatore.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playernew,playerNew>>, <<playersetenabled,playerSetEnabled>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- L'eroe sale in auto: il controller se ne va, il modello viaggia con lei.
|
||
function boardCar()
|
||
playerDelete(hero)
|
||
nodeSetParent(hero, car)
|
||
nodeSetPosition(hero, 0, 0.4, 0)
|
||
driving = true
|
||
end
|
||
----
|
||
|
||
[#playermove]
|
||
==== playerMove
|
||
|
||
[source,text]
|
||
----
|
||
playerMove(node, vx, vz)
|
||
playerMove(node, vx, vy, vz)
|
||
playerMove(node, vx)
|
||
----
|
||
|
||
Dice al giocatore la velocità che vuole in questo fotogramma, in unità del mondo al secondo. A terra o in aria i due valori orizzontali vengono usati così come sono, in aggiunta al moto della piattaforma su cui si trova; la parte verticale viene dalla gravità, dal terreno e da `playerJump`, e `vy` viene ignorato. Quando il giocatore nuota tutti e tre i valori lo guidano, e l'acqua porta gradualmente la sua velocità verso di essi con la resistenza di `playerSetSwim`, affondando alla velocità di affondamento quando `vy` è `0`. In un mondo 2D un solo `vx` corre lungo X e qualsiasi `vz` viene ignorato. Chiamala a ogni fotogramma da `onOverlayUpdate`: la richiesta viene consumata dai passi di quel fotogramma, e in un fotogramma senza richiesta il giocatore sta fermo.
|
||
|
||
*Parametri:*
|
||
|
||
* `vx`, `vz` -- la velocità orizzontale voluta, in unità del mondo al secondo (2D: pixel dell'overlay lungo X).
|
||
* `vy` -- la velocità verticale voluta mentre nuota; positiva verso l'alto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playerjump,playerJump>>, <<playersetvelocity,playerSetVelocity>>, <<playerisswimming,playerIsSwimming>>, <<physicsset2d,physicsSet2D>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Cammina con la levetta sinistra, nuota verso l'alto mentre il pulsante è premuto, guarda nella direzione di marcia.
|
||
function onOverlayUpdate()
|
||
local vx = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) / 32767 * 3
|
||
local vz = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_Y) / 32767 * 3
|
||
if playerIsSwimming(hero) then
|
||
playerMove(hero, vx, surfacing and 1.5 or 0, vz)
|
||
else
|
||
playerMove(hero, vx, vz)
|
||
end
|
||
nodeSetRotation(hero, 0, math.deg(math.atan(-vz, vx)), 0)
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#playerisswimming]
|
||
==== playerIsSwimming
|
||
|
||
[source,text]
|
||
----
|
||
swimming = playerIsSwimming(node)
|
||
----
|
||
|
||
Se il giocatore era in acqua all'ultimo passo: il centro della sua forma sotto la superficie di un volume `bodySetWater`. Mentre nuota, `playerMove` lo guida su tre assi, la gravità lascia il posto alla velocità di affondamento e la corrente dell'acqua trascina il giocatore.
|
||
|
||
*Restituisce:* Booleano.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playersetswim,playerSetSwim>>, <<bodysetwater,bodySetWater>>, <<playermove,playerMove>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Cambia l'animazione quando l'eroe entra in acqua.
|
||
function onOverlayUpdate()
|
||
local swimming = playerIsSwimming(hero)
|
||
if swimming ~= wasSwimming then
|
||
animationPlay(fox, swimming and "Swim" or "Walk", true)
|
||
wasSwimming = swimming
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#playerjump]
|
||
==== playerJump
|
||
|
||
[source,text]
|
||
----
|
||
jumped = playerJump(node, speed)
|
||
----
|
||
|
||
Chiede al giocatore di staccarsi da terra a quella velocità verso l'alto, in unità del mondo al secondo, al passo successivo. Viene concesso solo quando in questo momento c'è terreno percorribile sotto i piedi, quindi tenere premuto un pulsante non fa rimbalzare il giocatore e un salto a mezz'aria non fa nulla. Una velocità negativa viene trattata come `0`. Il giocatore mantiene la sua velocità orizzontale durante il salto e `playerMove` lo guida comunque in aria.
|
||
|
||
*Restituisce:* `true` quando il salto è stato concesso.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playerisonground,playerIsOnGround>>, <<playermove,playerMove>>, <<playersetgravityscale,playerSetGravityScale>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Salta con il pulsante d'azione, con un suono solo quando il salto è avvenuto davvero.
|
||
function onInputPressed(switch)
|
||
if switch == SWITCH_BUTTON1 then
|
||
if playerJump(hero, 4.5) then
|
||
soundPlay(jumpSound)
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
[#playersetswim]
|
||
==== playerSetSwim
|
||
|
||
[source,text]
|
||
----
|
||
playerSetSwim(node, sinkSpeed, drag)
|
||
----
|
||
|
||
Imposta come si comporta il giocatore in acqua: quanto velocemente affonda, in unità del mondo al secondo, quando `playerMove` non chiede movimento verticale (predefinito `0.3`), e quanto in fretta l'acqua porta la sua velocità verso quella che chiede `playerMove`, come tasso al secondo (predefinito `2`, quindi la maggior parte della differenza sparisce entro un secondo; `0` non cambia mai la velocità). Entrambi vengono limitati ad almeno `0`. Un palombaro pesante affonda in fretta con una resistenza alta; un tappo non affonda affatto.
|
||
|
||
*Parametri:*
|
||
|
||
* `sinkSpeed` -- la deriva verso il basso senza input verticale, in unità al secondo.
|
||
* `drag` -- il tasso con cui l'acqua porta la velocità verso quella richiesta, al secondo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playerisswimming,playerIsSwimming>>, <<playermove,playerMove>>, <<bodysetwater,bodySetWater>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- L'armatura trascina a fondo l'eroe; toglila per galleggiare.
|
||
function wearArmor(on)
|
||
armored = on
|
||
if on then
|
||
playerSetSwim(hero, 1.2, 4)
|
||
else
|
||
playerSetSwim(hero, 0.3, 2)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#playersetvelocity]
|
||
==== playerSetVelocity
|
||
|
||
[source,text]
|
||
----
|
||
playerSetVelocity(node, x, y, z)
|
||
----
|
||
|
||
Imposta l'intera velocità del giocatore, in unità del mondo al secondo lungo ciascun asse del mondo, e scarta qualsiasi richiesta di `playerMove` fatta in questo fotogramma. La parte verticale è quella che dura: una velocità verso l'alto stacca il giocatore da terra e poi lui ricade per gravità, il che fa da trampolino di lancio o da rimbalzo. La parte orizzontale vale solo fino al passo successivo, quando viene sostituita da ciò che chiede `playerMove` (niente, se non viene chiamata), perché un giocatore non ha una quantità di moto orizzontale propria. Un contraccolpo o uno scatto è quindi una spinta verso l'alto qui più una velocità di `playerMove` fornita nei fotogrammi successivi.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playergetvelocity,playerGetVelocity>>, <<playermove,playerMove>>, <<playerjump,playerJump>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un colpo dell'orco lancia in alto l'eroe e, nei fotogrammi successivi, lontano da lui.
|
||
function onCollision(a, b, x, y, z, speed)
|
||
if (a == hero and b == ogre) or (a == ogre and b == hero) then
|
||
local hx, hy, hz = nodeGetPosition(hero)
|
||
local ox, oy, oz = nodeGetPosition(ogre)
|
||
local dx, dz = hx - ox, hz - oz
|
||
local len = math.sqrt(dx * dx + dz * dz)
|
||
playerSetVelocity(hero, 0, 3, 0)
|
||
knockX, knockZ, knockFrames = dx / len * 6, dz / len * 6, 20
|
||
health = health - 10
|
||
end
|
||
end
|
||
----
|
||
|
||
[#playergetvelocity]
|
||
==== playerGetVelocity
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z = playerGetVelocity(node)
|
||
----
|
||
|
||
La velocità del giocatore dopo l'ultimo passo, in unità del mondo al secondo lungo ciascun asse del mondo, compresa quella che gli ha dato il terreno su cui viaggia. Leggi la parte verticale per distinguere la salita dalla caduta, o la velocità intera per il tonfo di un atterraggio.
|
||
|
||
*Restituisce:* Tre numeri.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playersetvelocity,playerSetVelocity>>, <<playerisonground,playerIsOnGround>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Danno da caduta in base alla velocità con cui l'eroe ha toccato terra.
|
||
function onOverlayUpdate()
|
||
local _, vy = playerGetVelocity(hero)
|
||
local onGround = playerIsOnGround(hero)
|
||
if onGround and not wasOnGround and lastVy < -8 then
|
||
health = health - math.floor((-lastVy - 8) * 5)
|
||
end
|
||
wasOnGround = onGround
|
||
lastVy = vy
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#playerisonground]
|
||
==== playerIsOnGround
|
||
|
||
[source,text]
|
||
----
|
||
onGround = playerIsOnGround(node)
|
||
----
|
||
|
||
Se il giocatore sta su terreno percorribile all'ultimo passo: una superficie non più ripida di `playerSetSlope`. In aria o su una pendenza troppo ripida da salire è `false`. È lo stesso controllo che fa `playerJump`.
|
||
|
||
*Restituisce:* Booleano.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playergetground,playerGetGround>>, <<playerjump,playerJump>>, <<playersetslope,playerSetSlope>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Coyote time: un salto è ancora permesso per qualche fotogramma dopo essere usciti da un bordo.
|
||
function onOverlayUpdate()
|
||
if playerIsOnGround(hero) then
|
||
framesSinceGround = 0
|
||
else
|
||
framesSinceGround = framesSinceGround + 1
|
||
end
|
||
if jumpPressed and framesSinceGround < 6 then
|
||
playerSetVelocity(hero, vx, 4.5, vz)
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#playergetground]
|
||
==== playerGetGround
|
||
|
||
[source,text]
|
||
----
|
||
ground, nx, ny, nz = playerGetGround(node)
|
||
----
|
||
|
||
Su cosa sta il giocatore e la normale della superficie del terreno in quel punto. Il nodo è il corpo sotto i piedi, percorribile o no; è `nil` in aria, e la normale allora è `0, 1, 0`. Usa il nodo per distinguere dal pavimento una piattaforma mobile, il ghiaccio o una piastra a pressione, e la normale per inclinare un modello sul pendio.
|
||
|
||
*Restituisce:* Il nodo su cui sta o `nil`, poi la normale.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playerisonground,playerIsOnGround>>, <<bodysetfriction,bodySetFriction>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- I passi suonano diversi sul ponte di legno, e la piastra apre il cancello.
|
||
function onOverlayUpdate()
|
||
local ground = playerGetGround(hero)
|
||
if ground == bridge then
|
||
footstep = woodStep
|
||
elseif ground == plate then
|
||
openGate()
|
||
else
|
||
footstep = stoneStep
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#playersetstep]
|
||
==== playerSetStep
|
||
|
||
[source,text]
|
||
----
|
||
playerSetStep(node, height)
|
||
----
|
||
|
||
Il gradino più alto che il giocatore sale senza saltare, in unità del mondo; qualsiasi cosa più alta è un muro lungo cui scivola. Il valore predefinito è `0.3` per una forma la cui semi-dimensione più piccola è `0.3` (una capsula di raggio `0.3`), e scala con la forma, quindi un giocatore di 30 pixel in un mondo 2D parte da 30 pixel. La stessa distanza è quanto il giocatore resta attaccato al pavimento scendendo oltre il bordo di un gradino, quindi un gradino più grande lo tiene anche incollato ai pendii in discesa. I valori negativi vengono portati a `0`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playersetslope,playerSetSlope>>, <<playernew,playerNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Le scale di questo livello salgono di 0.35 a gradino; assicurati che l'eroe le salga.
|
||
playerNew(hero, 0.3, 1.0)
|
||
playerSetStep(hero, 0.4)
|
||
-- Le sporgenze del platform 2D sono di 26 pixel.
|
||
playerNew(runner, 16, 48)
|
||
playerSetStep(runner, 26)
|
||
----
|
||
|
||
[#playersetslope]
|
||
==== playerSetSlope
|
||
|
||
[source,text]
|
||
----
|
||
playerSetSlope(node, degrees)
|
||
----
|
||
|
||
La pendenza più ripida che il giocatore risale camminando, in gradi dall'orizzontale; un terreno più ripido non conta come appoggio, quindi il giocatore ci scivola giù e non può saltare da lì. Il valore predefinito è `45`, e il valore viene limitato tra `0` e `89`. Un valore basso trasforma le colline in ostacoli, uno alto permette al giocatore di salire quasi su tutto, tranne un muro.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playersetstep,playerSetStep>>, <<playerisonground,playerIsOnGround>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Lo stambecco sale dove l'eroe scivola.
|
||
playerNew(goat, 0.25, 0.8)
|
||
playerSetSlope(goat, 70)
|
||
playerNew(hero, 0.3, 1.0)
|
||
playerSetSlope(hero, 40)
|
||
----
|
||
|
||
[#playersetmass]
|
||
==== playerSetMass
|
||
|
||
[source,text]
|
||
----
|
||
playerSetMass(node, kilograms)
|
||
----
|
||
|
||
La massa del giocatore, usata quando spinge i corpi dinamici e come suo peso su quelli su cui sta. Il valore predefinito è `70` chilogrammi; i valori sotto `0.001` vengono portati a quel minimo. La massa non influisce su come il giocatore cade o cammina, solo sulle spinte, quindi abbinala a `playerSetPush`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playersetpush,playerSetPush>>, <<bodysetmass,bodySetMass>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il troll sfonda ciò che l'eroe deve aggirare.
|
||
troll = nodeNew()
|
||
nodeSetPosition(troll, 6, 0, -3)
|
||
playerNew(troll, 0.6, 2.4)
|
||
playerSetMass(troll, 400)
|
||
playerSetPush(troll, 2000)
|
||
playerSetStep(troll, 0.8)
|
||
----
|
||
|
||
[#playersetpush]
|
||
==== playerSetPush
|
||
|
||
[source,text]
|
||
----
|
||
playerSetPush(node, strength)
|
||
----
|
||
|
||
Con quanta forza il giocatore spinge i corpi dinamici contro cui cammina, in newton. Il valore predefinito `300` sposta casse e palle ma non i cabinati; con `0` il giocatore aggira tutto come se fosse statico. I valori negativi vengono portati a `0`. È il `bodySetMass` del corpo stesso a decidere cosa può spostare una data forza.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playersetmass,playerSetMass>>, <<bodysetmass,bodySetMass>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La pozione della forza permette all'eroe di spostare il macigno per dieci secondi.
|
||
function drinkStrength()
|
||
playerSetPush(hero, 3000)
|
||
strengthUntil = frames + 600
|
||
end
|
||
function onOverlayUpdate()
|
||
if strengthUntil and frames > strengthUntil then
|
||
playerSetPush(hero, 300)
|
||
strengthUntil = nil
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#playersetgravityscale]
|
||
==== playerSetGravityScale
|
||
|
||
[source,text]
|
||
----
|
||
playerSetGravityScale(node, scale)
|
||
----
|
||
|
||
Moltiplica la gravità del mondo solo per questo giocatore: con `1` (il valore predefinito) cade come tutto il resto, con `0` fluttua dove si trova (una scala a pioli, una stanza a gravità zero, una cavalcatura volante), con di più cade più in fretta per una sensazione di pesantezza, e con di meno fa un salto fluttuante. Non cambia ciò che fa `playerJump` allo stacco, solo quanto in fretta il giocatore torna giù. Mentre nuota, la velocità di affondamento dell'acqua prende il posto della gravità.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<physicssetgravity,physicsSetGravity>>, <<playerjump,playerJump>>, <<playersetswim,playerSetSwim>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Sulla scala a pioli l'eroe sale con la levetta invece di cadere.
|
||
function onTrigger(trigger, other, entered)
|
||
if trigger == ladder and other == hero then
|
||
onLadder = entered
|
||
playerSetGravityScale(hero, entered and 0 or 1)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#playersetposition]
|
||
==== playerSetPosition
|
||
|
||
[source,text]
|
||
----
|
||
playerSetPosition(node, x, y, z)
|
||
----
|
||
|
||
Teletrasporta il giocatore in una posizione del mondo, senza toccare nulla lungo la strada, e lo ferma di colpo. Il nodo si sposta con lui e mantiene la sua rotazione. Usala per la comparsa, la ricomparsa e i passaggi di livello; spostare invece il nodo con `nodeSetPosition` lascia il giocatore dov'era e il nodo viene riportato indietro al passo successivo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playersetvelocity,playerSetVelocity>>, <<nodesetposition,nodeSetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Di nuovo all'ultimo checkpoint dopo una caduta nella fossa.
|
||
function onTrigger(trigger, other, entered)
|
||
if trigger == pit and other == hero and entered then
|
||
lives = lives - 1
|
||
playerSetPosition(hero, checkpointX, checkpointY, checkpointZ)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#playersetenabled]
|
||
==== playerSetEnabled
|
||
|
||
[source,text]
|
||
----
|
||
playerSetEnabled(node, enabled)
|
||
----
|
||
|
||
Con `false` il giocatore esce dalla simulazione: il suo corpo viene rimosso dal mondo, quindi non si muove, non cade, non guida il suo nodo e non blocca nulla, `onTrigger` segnala la sua uscita da ogni trigger in cui si trovava, e viene escluso dal disegno di debug. `true` rimette il corpo e lo riprende da dovunque si trovi il nodo. Disattiva un giocatore mentre il personaggio è in mano a un ragdoll, a una scena d'intermezzo o a un veicolo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<playerdelete,playerDelete>>, <<ragdollnew,ragdollNew>>, <<bodysetenabled,bodySetEnabled>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un colpo forte consegna lo scheletro dell'eroe al ragdoll.
|
||
function onCollision(a, b, x, y, z, speed)
|
||
if (a == hero or b == hero) and speed > 6 then
|
||
playerSetEnabled(hero, false)
|
||
ragdollActivate(fox)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#ragdoll]
|
||
=== Ragdoll
|
||
|
||
Lo scheletro di un modello con skin affidato alla fisica (vedi <<physics,Fisica>>, Ragdoll). Un ragdoll si indica tramite il nodo dato a `ragdollNew`, di solito la radice di un'istanza di modello, e un nodo senza ragdoll termina lo script in ogni chiamata dopo `ragdollNew`. Le ossa si chiamano con i nomi dei loro nodi dei giunti, i nomi che vede `nodeFind`, confrontati senza distinguere tra maiuscole e minuscole; le distanze sono in unità del mondo, gli angoli in gradi, gli impulsi in newton secondo e la forza dei motori in newton metro. Fino a otto ragdoll alla volta, di non più di 48 ossa ciascuno.
|
||
|
||
[#ragdollnew]
|
||
==== ragdollNew
|
||
|
||
[source,text]
|
||
----
|
||
ragdollNew(node)
|
||
----
|
||
|
||
Cerca nel nodo e sotto di esso la prima mesh con skin e ne costruisce una ricetta di ragdoll dallo scheletro: ogni giunto che ha un giunto figlio diventa un osso, una capsula che va dal giunto verso i suoi figli, collegata all'osso più vicino sopra di essa da un giunto swing-twist limitato a 45 gradi di oscillazione e 30 di torsione. I giunti senza giunti figli, come le punte delle dita e la punta di una coda, seguono il loro osso genitore e non si possono nominare nelle altre chiamate. Non viene ancora simulato nulla: il modello continua ad animarsi fino a `ragdollActivate`. Chiamarla di nuovo sullo stesso nodo scarta prima il vecchio ragdoll. Termina lo script quando la fisica non è disponibile, quando sotto il nodo non c'è una mesh con skin dotata di ossa, o quando esistono già otto ragdoll.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<ragdollactivate,ragdollActivate>>, <<ragdollsetjoint,ragdollSetJoint>>, <<modelinstance,modelInstance>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una guardia che corre verso il giocatore; il ragdoll è pronto prima del placcaggio.
|
||
local guard = nodeNew()
|
||
nodeSetPosition(guard, -1, 0.05, -1)
|
||
playerNew(guard, 0.3, 0.9)
|
||
local guardFox = modelInstance(foxModel)
|
||
nodeSetParent(guardFox, guard)
|
||
nodeSetScale(guardFox, 0.01)
|
||
nodeSetRotation(guardFox, 0, 90, 0)
|
||
animationPlay(guardFox, "Run", true)
|
||
ragdollNew(guardFox) -- Solo la ricetta; corre ancora
|
||
----
|
||
|
||
[#ragdolldelete]
|
||
==== ragdollDelete
|
||
|
||
[source,text]
|
||
----
|
||
ragdollDelete(node)
|
||
----
|
||
|
||
Rimuove il ragdoll: i suoi corpi e i suoi giunti se è attivo, poi la ricetta. I nodi dei giunti restano dove li ha lasciati la fisica, quindi il modello giace com'è caduto finché un'animazione non lo muove, e il nodo è libero per un nuovo `ragdollNew`. Un nodo senza ragdoll termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<ragdollnew,ragdollNew>>, <<ragdolldeactivate,ragdollDeactivate>>, <<nodedelete,nodeDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
for i = #fallen, 1, -1 do
|
||
local body = fallen[i]
|
||
body.restingFrames = ragdollIsResting(body.node) and body.restingFrames + 1 or 0
|
||
if body.restingFrames > 300 then
|
||
ragdollDelete(body.node) -- Fermo da cinque secondi: ripuliscilo
|
||
nodeDelete(body.node)
|
||
table.remove(fallen, i)
|
||
end
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#ragdollsetjoint]
|
||
==== ragdollSetJoint
|
||
|
||
[source,text]
|
||
----
|
||
ragdollSetJoint(node, jointName, radius, swingDegrees, twistDegrees)
|
||
----
|
||
|
||
Regola la ricetta di un osso. I valori vengono letti quando si creano i corpi, quindi chiamala prima di `ragdollActivate`; un ragdoll già attivo mantiene i suoi corpi finché non viene disattivato e riattivato. Il raggio `0` chiede la capsula automatica, 0,22 volte la lunghezza dell'osso e mai più sottile di `0.02` unità. L'oscillazione è il semiangolo del cono entro cui l'osso può muoversi rispetto al genitore, la torsione quanto può girare attorno al proprio asse; entrambe vengono limitate tra `0` e `179`. Si possono nominare solo le ossa, non i giunti foglia; il nome viene confrontato senza distinguere tra maiuscole e minuscole, e un nome senza osso termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo del ragdoll.
|
||
* `jointName` -- il nome del nodo del giunto, come nel modello.
|
||
* `radius` -- il raggio della capsula in unità del mondo, o `0` per quello automatico.
|
||
* `swingDegrees` -- quanto l'osso può oscillare rispetto al genitore; predefinito `45`.
|
||
* `twistDegrees` -- quanto può torcersi attorno al proprio asse; predefinito `30`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<ragdollnew,ragdollNew>>, <<ragdollactivate,ragdollActivate>>, <<nodefind,nodeFind>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
ragdollNew(fox)
|
||
ragdollSetJoint(fox, "b_Neck_04", 0, 30, 20) -- Un collo più rigido
|
||
ragdollSetJoint(fox, "b_Tail01_012", 0.03, 80, 60) -- Una coda a frusta
|
||
for _, leg in ipairs({ "b_LeftLeg01_015", "b_RightLeg01_019" }) do
|
||
ragdollSetJoint(fox, leg, 0, 60, 15) -- Le anche oscillano molto, si torcono appena
|
||
end
|
||
ragdollActivate(fox)
|
||
----
|
||
|
||
[#ragdollactivate]
|
||
==== ragdollActivate
|
||
|
||
[source,text]
|
||
----
|
||
ragdollActivate(node)
|
||
----
|
||
|
||
Affida lo scheletro alla fisica a partire dalla sua posa corrente: ogni livello di animazione sul nodo si ferma, viene creata una capsula dinamica per ogni osso dove si trova il suo giunto in questo momento, vengono creati i giunti swing-twist tra di esse, e da quel momento ogni passo della fisica guida i nodi dei giunti a partire dai corpi, così la mesh con skin li segue. Le ossa collidono con il mondo e tra loro, tranne ciascun osso con il proprio genitore. La posa di questo momento è quella verso cui tira `ragdollSetStrength`. Un ragdoll già attivo resta com'è. Termina lo script quando non si può creare il corpo di un osso.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<ragdolldeactivate,ragdollDeactivate>>, <<ragdollapplyimpulse,ragdollApplyImpulse>>, <<ragdollsetstrength,ragdollSetStrength>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
local crashed = false
|
||
|
||
function onCollision(a, b, x, y, z, speed)
|
||
if not crashed and (a == guard or b == guard) and speed > 1.5 then
|
||
crashed = true
|
||
playerSetEnabled(guard, false) -- Il controller molla la presa
|
||
ragdollActivate(guardFox) -- Da qui lo scheletro passa alla fisica
|
||
ragdollApplyImpulse(guardFox, "b_Spine02_03", 40, 30, 0)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#ragdolldeactivate]
|
||
==== ragdollDeactivate
|
||
|
||
[source,text]
|
||
----
|
||
ragdollDeactivate(node)
|
||
----
|
||
|
||
Rimuove i corpi e i giunti e restituisce lo scheletro. I nodi dei giunti mantengono la posa in cui li ha lasciati il ragdoll, e nulla li muove finché non parte un'animazione; un `animationPlay` con una dissolvenza parte da quella posa fondendosi gradualmente, così un personaggio a terra può rialzarsi. La ricetta resta, quindi `ragdollActivate` può riprendere il controllo più tardi da dovunque l'animazione abbia portato lo scheletro. Non fa nulla a un ragdoll che non è attivo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<ragdollactivate,ragdollActivate>>, <<animationplay,animationPlay>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Quando il corpo si è assestato, l'eroe si rialza in mezzo secondo.
|
||
function onOverlayUpdate()
|
||
if downed and ragdollIsResting(hero) then
|
||
downed = false
|
||
ragdollDeactivate(hero)
|
||
animationPlay(hero, "GetUp", false, 1, 0.5)
|
||
playerSetEnabled(heroController, true)
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#ragdollsetstrength]
|
||
==== ragdollSetStrength
|
||
|
||
[source,text]
|
||
----
|
||
ragdollSetStrength(node, strength)
|
||
----
|
||
|
||
Dà a ogni giunto un motore che tira verso la posa in cui il ragdoll è stato attivato, limitato a quel numero di newton metro; `0` (predefinito) spegne i motori e il corpo si affloscia. I motori sono molle a 4 Hz, con smorzamento critico. Si può impostare prima dell'attivazione o mentre il ragdoll si muove, e ha effetto al passo successivo. Non rimette in piedi un corpo, perché niente fa leva sul pavimento: qualche decina di newton metro trasforma una caduta in un barcollamento e permette a un personaggio a terra di raccogliere gli arti.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<ragdollactivate,ragdollActivate>>, <<ragdollisresting,ragdollIsResting>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Atterrato e inerte; un attimo dopo il lottatore si irrigidisce e si raggomitola.
|
||
ragdollActivate(fighter)
|
||
ragdollSetStrength(fighter, 0)
|
||
downFrames = 0
|
||
|
||
function onOverlayUpdate()
|
||
downFrames = downFrames + 1
|
||
if downFrames == 60 then
|
||
ragdollSetStrength(fighter, 60)
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#ragdollapplyimpulse]
|
||
==== ragdollApplyImpulse
|
||
|
||
[source,text]
|
||
----
|
||
hit = ragdollApplyImpulse(node, jointName, x, y, z)
|
||
----
|
||
|
||
Aggiunge un impulso in newton secondo, lungo gli assi del mondo, al corpo dell'osso che parte dal giunto indicato: il pugno, lo sparo, l'esplosione. Il ragdoll deve essere attivo, quindi chiama prima `ragdollActivate` e poi spingi. Restituisce `false`, e non fa nulla, quando il ragdoll non è attivo o quando nessun osso ha quel nome (i giunti foglia non sono ossa); il nome viene confrontato senza distinguere tra maiuscole e minuscole.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo del ragdoll.
|
||
* `jointName` -- il nome del nodo del giunto, come nel modello.
|
||
* `x`, `y`, `z` -- l'impulso in newton secondo, sugli assi del mondo.
|
||
|
||
*Restituisce:* Vero quando l'osso è stato trovato e il ragdoll era attivo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<ragdollactivate,ragdollActivate>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<nodefind,nodeFind>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un colpo dalla posizione del tiratore: l'impulso punta lontano dall'arma.
|
||
function shoot(target, gunX, gunY, gunZ, boneName)
|
||
local tx, ty, tz = nodeGetWorldPosition(target)
|
||
local dx = tx - gunX
|
||
local dz = tz - gunZ
|
||
local length = math.sqrt(dx * dx + dz * dz)
|
||
if not ragdollIsActive(target) then
|
||
ragdollActivate(target)
|
||
end
|
||
ragdollApplyImpulse(target, boneName, dx / length * 30, 10, dz / length * 30)
|
||
end
|
||
----
|
||
|
||
[#ragdollisactive]
|
||
==== ragdollIsActive
|
||
|
||
[source,text]
|
||
----
|
||
active = ragdollIsActive(node)
|
||
----
|
||
|
||
Se in questo momento è la fisica a guidare lo scheletro: `true` da `ragdollActivate` fino a `ragdollDeactivate` o `ragdollDelete`. Un nodo senza ragdoll termina lo script.
|
||
|
||
*Restituisce:* Vero o falso.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<ragdollactivate,ragdollActivate>>, <<ragdollisresting,ragdollIsResting>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La barra della salute sta sopra il modello solo finché è in piedi.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
for _, enemy in ipairs(enemies) do
|
||
if not ragdollIsActive(enemy.node) then
|
||
local x, y, z = nodeGetWorldPosition(enemy.node)
|
||
local sx, sy = sceneProject(x, y + 1.2, z)
|
||
drawHealthBar(sx, sy, enemy.health)
|
||
end
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#ragdollisresting]
|
||
==== ragdollIsResting
|
||
|
||
[source,text]
|
||
----
|
||
resting = ragdollIsResting(node)
|
||
----
|
||
|
||
Se il corpo di ogni osso si è addormentato, cosa che la fisica fa quando un corpo è rimasto fermo per un momento. `false` mentre il ragdoll non è attivo, quindi non si può usare per distinguere un modello inattivo da uno che si è assestato; controlla anche `ragdollIsActive`.
|
||
|
||
*Restituisce:* Vero o falso.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<ragdollisactive,ragdollIsActive>>, <<ragdolldeactivate,ragdollDeactivate>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il round finisce quando il perdente ha smesso di muoversi.
|
||
function onOverlayUpdate()
|
||
if roundOver and ragdollIsResting(loser) then
|
||
restingFrames = restingFrames + 1
|
||
if restingFrames == 90 then
|
||
showResults()
|
||
end
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#save]
|
||
=== Salvataggio
|
||
|
||
Tutto ciò che il gioco deve ancora sapere la prossima volta che parte: i
|
||
punteggi record, le opzioni del giocatore, fin dove è arrivato. Una tabella per
|
||
gioco, conservata come JSON nella cartella dei dati del gioco stesso (vedi
|
||
<<singegetdatapath,`singeGetDataPath`>>), così un `.game` impacchettato resta di
|
||
sola lettura e due giochi non si pestano mai i piedi.
|
||
|
||
Le chiavi sono tue. I valori possono essere numeri, stringhe, booleani o tabelle
|
||
di questi, annidate alla profondità che vuoi -- un'intera tabella di punteggi
|
||
record entra come un solo valore. Una funzione, o un handle di qualcosa che
|
||
appartiene al motore, no: né l'una né l'altro significano qualcosa
|
||
all'esecuzione successiva, e salvarne uno è un errore subito anziché una
|
||
sorpresa più avanti.
|
||
|
||
**Ciò che imposti è ciò che viene salvato, e ciò che rileggi è tuo.** Le tabelle
|
||
vengono copiate in entrata e in uscita, quindi modificare una tabella riletta
|
||
non cambia nulla finché non la salvi di nuovo, e modificarne una che hai salvato
|
||
non tocca il salvataggio dopo. Tenere un'anteprima che poi il giocatore annulla
|
||
è quindi solo questione di non salvarla.
|
||
|
||
La scrittura avviene una volta alla fine di ogni fotogramma che ha cambiato
|
||
qualcosa, per quante chiavi tu vi abbia impostato, e di nuovo quando il gioco si
|
||
chiude -- compreso tutto ciò che viene impostato dentro
|
||
<<onshutdown,`onShutdown`>>. Per un cabinato che può perdere l'alimentazione in
|
||
qualsiasi momento, <<saveflush,`saveFlush`>> scrive subito. Il file viene
|
||
scritto accanto al vecchio e poi rinominato al suo posto, così perdere
|
||
l'alimentazione durante una scrittura lascia il salvataggio precedente anziché
|
||
metà di uno nuovo.
|
||
|
||
I numeri interi tornano come numeri interi. JSON ha un solo tipo di numero e Lua
|
||
ne ha due, quindi un punteggio salvato come `100` altrimenti tornerebbe come
|
||
`100.0` e verrebbe stampato con il punto decimale.
|
||
|
||
[#saveclear]
|
||
==== saveClear
|
||
|
||
[source,text]
|
||
----
|
||
saveClear()
|
||
----
|
||
|
||
Dimentica tutto e scrive il salvataggio vuoto. Serve per la voce «azzera i punteggi record» di un menu di servizio.
|
||
|
||
*Parametri:*
|
||
|
||
* Nessuno.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#savedelete]
|
||
==== saveDelete
|
||
|
||
[source,text]
|
||
----
|
||
saveDelete(key)
|
||
----
|
||
|
||
Rimuove una chiave. Eliminare una chiave che non c'è mai stata non è un errore.
|
||
|
||
*Parametri:*
|
||
|
||
* `key` -- la chiave da rimuovere.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#saveflush]
|
||
==== saveFlush
|
||
|
||
[source,text]
|
||
----
|
||
saveFlush()
|
||
----
|
||
|
||
Scrive il salvataggio subito anziché alla fine del fotogramma. Usalo dopo qualcosa che ti dispiacerebbe davvero perdere -- un nuovo punteggio record su un cabinato che si spegne staccando la corrente.
|
||
|
||
*Parametri:*
|
||
|
||
* Nessuno.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<saveset,saveSet>>
|
||
|
||
[#saveget]
|
||
==== saveGet
|
||
|
||
[source,text]
|
||
----
|
||
value = saveGet(key [, default])
|
||
----
|
||
|
||
Ciò che è stato salvato sotto quella chiave. Quando non c'è niente torna il valore predefinito, o `nil` se non c'è un valore predefinito -- ed è così che si distingue la prima esecuzione da quelle successive.
|
||
|
||
*Parametri:*
|
||
|
||
* `key` -- la chiave da leggere.
|
||
* `default` -- facoltativo; che cosa rispondere quando la chiave non c'è.
|
||
|
||
*Restituisce:* il valore salvato, il valore predefinito o `nil`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<saveset,saveSet>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Conta le esecuzioni e ricorda i dieci punteggi migliori.
|
||
local runs = saveGet("runs", 0) + 1
|
||
saveSet("runs", runs)
|
||
|
||
local best = saveGet("highScores", {})
|
||
best[#best + 1] = { name = initials, score = score }
|
||
table.sort(best, function(a, b) return a.score > b.score end)
|
||
while #best > 10 do table.remove(best) end
|
||
saveSet("highScores", best)
|
||
saveFlush()
|
||
----
|
||
|
||
[#savegetall]
|
||
==== saveGetAll
|
||
|
||
[source,text]
|
||
----
|
||
everything = saveGetAll()
|
||
----
|
||
|
||
Una copia dell'intero salvataggio, da leggere: per stamparlo mentre fai debug, o per scorrere chiavi i cui nomi il gioco non conosce in anticipo. Modificare ciò che torna non cambia nulla -- è <<savesetall,`saveSetAll`>> che scrive un'intera tabella.
|
||
|
||
*Parametri:*
|
||
|
||
* Nessuno.
|
||
|
||
*Restituisce:* una tabella; vuota quando non è stato salvato nulla.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<savesetall,saveSetAll>>, <<saveget,saveGet>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Che cosa c'è dentro? inspect è incluso proprio per questo.
|
||
debugPrint(require("inspect")(saveGetAll()))
|
||
----
|
||
|
||
[#saveset]
|
||
==== saveSet
|
||
|
||
[source,text]
|
||
----
|
||
saveSet(key, value)
|
||
----
|
||
|
||
Conserva un valore finché il gioco non viene disinstallato. La scrittura avviene alla fine del fotogramma, quindi impostare venti chiavi in un ciclo scrive un solo file.
|
||
|
||
*Parametri:*
|
||
|
||
* `key` -- la chiave da scrivere.
|
||
* `value` -- un numero, una stringa, un booleano o una tabella di questi.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<saveget,saveGet>>, <<saveflush,saveFlush>>
|
||
|
||
[#savesetall]
|
||
==== saveSetAll
|
||
|
||
[source,text]
|
||
----
|
||
saveSetAll(everything)
|
||
----
|
||
|
||
Sostituisce l'intero salvataggio con questa tabella, in un solo passo. Le chiavi che c'erano e non sono nella tabella che passi spariscono.
|
||
|
||
Esiste per portare un vecchio salvataggio a una nuova struttura. Farlo con <<saveclear,`saveClear`>> e un ciclo di <<saveset,`saveSet`>> lascia un momento in cui il salvataggio è vuoto, e una macchina spenta in quel momento perde tutto; questa chiamata non ha un momento del genere, perché il file viene scritto una sola volta alla fine del fotogramma a partire dalla tabella completa.
|
||
|
||
*Parametri:*
|
||
|
||
* `everything` -- tabella; ciò che il salvataggio deve contenere d'ora in poi. Gli stessi valori che accetta `saveSet`.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<savegetall,saveGetAll>>, <<saveclear,saveClear>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La versione 1 teneva un punteggio in due chiavi; la versione 2 ne tiene una tabella.
|
||
if saveGet("version", 1) < 2 then
|
||
local old = saveGetAll()
|
||
saveSetAll({
|
||
version = 2,
|
||
scores = { { name = old.hiName, score = old.hiScore } },
|
||
runs = old.runs,
|
||
})
|
||
saveFlush()
|
||
end
|
||
----
|
||
|
||
[#scene]
|
||
=== Scena
|
||
|
||
La scena è il livello 3D disegnato tra il video del disco e l'overlay; i nodi messi nel livello frontale con `nodeSetLayer` vengono invece disegnati dopo l'overlay. È disattivata finché non si chiama `sceneEnable(true)`, ha le dimensioni dell'overlay e a ogni fotogramma viene ripulita con il colore di `sceneSetBackground`. Le chiamate `sceneSet*` impostano l'aspetto dell'intero livello: luce ambiente, cielo, nebbia, esposizione e curva tonale, bloom, antialiasing e qualità delle ombre; i colori sono interi da `0` a `255` e le distanze sono in unità del mondo. `sceneProject` e `sceneUnproject` fanno da ponte tra lo spazio del mondo e le coordinate dell'overlay, e `sceneGetStats` riporta il lavoro dell'ultimo fotogramma. Vedi <<scenes3d,Scene 3D>>.
|
||
|
||
[#sceneenable]
|
||
==== sceneEnable
|
||
|
||
[source,text]
|
||
----
|
||
sceneEnable(enabled)
|
||
----
|
||
|
||
Attiva o disattiva il livello 3D. Nodi, mesh e materiali possono essere costruiti prima che sia attivo e sopravvivono alla sua disattivazione; si ferma solo il disegno. Termina lo script quando lo si attiva senza una GPU utilizzabile (vedi <<scenes3d,Scene 3D>>), quindi un gioco con un'alternativa 2D chiede prima `singeCapabilities().SCENE`. In entrambi i casi lo schermo si aggiorna al fotogramma successivo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<scenesetbackground,sceneSetBackground>>, <<scenesetantialias,sceneSetAntialias>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ripiega sul semplice overlay quando il 3D non è disponibile.
|
||
has3d = singeCapabilities().SCENE
|
||
if has3d then
|
||
sceneEnable(true)
|
||
buildArena()
|
||
else
|
||
debugPrint("No 3D here; using the 2D attract mode.")
|
||
end
|
||
|
||
function onPause()
|
||
sceneEnable(false)
|
||
end
|
||
----
|
||
|
||
[#scenegetsize]
|
||
==== sceneGetSize
|
||
|
||
[source,text]
|
||
----
|
||
width, height = sceneGetSize()
|
||
----
|
||
|
||
La dimensione del livello, che è la risoluzione dell'overlay: lo spazio in cui lavorano `sceneProject` e `sceneUnproject`, e la dimensione a cui viene renderizzata la scena. Si cambia con `overlaySetResolution`. Prima che il livello sia stato dimensionato entrambi i valori sono `0`.
|
||
|
||
*Restituisce:* Due interi, la larghezza e l'altezza in coordinate dell'overlay.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<overlaysetresolution,overlaySetResolution>>, <<sceneunproject,sceneUnproject>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un mirino al centro della vista, e la cosa che ci sta sotto.
|
||
function onOverlayUpdate()
|
||
local width, height = sceneGetSize()
|
||
local cx = width / 2
|
||
local cy = height / 2
|
||
local ox, oy, oz = sceneUnproject(cx, cy, 0)
|
||
local fx, fy, fz = sceneUnproject(cx, cy, 50)
|
||
target = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz)
|
||
fontPrint(cx - 4, cy - 8, "+")
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#scenegetstats]
|
||
==== sceneGetStats
|
||
|
||
[source,text]
|
||
----
|
||
total, drawn, calls, textureKB = sceneGetStats()
|
||
----
|
||
|
||
I numeri dell'ultimo fotogramma dalla telecamera della finestra: le mesh raccolte per il disegno, quante di esse erano dentro l'inquadratura della telecamera, le chiamate di disegno in cui si sono trasformate dopo l'instancing, e la memoria delle texture che la scena occupa, in kilobyte. Serve per la messa a punto: una scena con centinaia di `drawn` ma una manciata di `calls` condivide bene le sue mesh, e un `textureKB` che non sta nella memoria di un Raspberry Pi chiede texture KTX2. Le viste renderizzate non contano.
|
||
|
||
*Restituisce:* Quattro interi.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<sceneenable,sceneEnable>>, <<modelinstance,modelInstance>>, <<materialsettexture,materialSetTexture>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un indicatore di debug nell'angolo durante la messa a punto del livello.
|
||
function onOverlayUpdate()
|
||
if showStats then
|
||
local total, drawn, calls, textureKB = sceneGetStats()
|
||
fontPrint(10, 10, string.format("%d meshes, %d in view, %d calls, %d MB textures", total, drawn, calls, math.floor(textureKB / 1024)))
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#scenemeasure]
|
||
==== sceneMeasure
|
||
|
||
[source,text]
|
||
----
|
||
sceneMeasure(on)
|
||
ms = sceneMeasure(false)
|
||
----
|
||
|
||
Cronometra la scena 3D. `sceneMeasure(true)` avvia la misura: ogni fotogramma
|
||
successivo aspetta che la GPU finisca la scena e somma quel tempo, così ciò che
|
||
si misura è il lavoro e non il ritmo dello schermo. `sceneMeasure(false)` ferma
|
||
la misura e restituisce la media in millisecondi per fotogramma, o `nil` quando
|
||
non è stato disegnato alcun fotogramma. L'attesa costa frequenza dei
|
||
fotogrammi, quindi cronometra qualche secondo, non un'intera partita.
|
||
|
||
*Restituisce:* niente all'avvio; un numero o `nil` all'arresto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<singesavetier,singeSaveTier>>, <<scenegetstats,sceneGetStats>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Due secondi della schermata del titolo dicono se gli effetti completi ci stanno.
|
||
sceneMeasure(true)
|
||
-- ... due secondi dopo:
|
||
local ms = sceneMeasure(false)
|
||
|
||
if ms and (ms > 8) then
|
||
sceneSetBloom(1.0, 0.0)
|
||
end
|
||
----
|
||
|
||
[#sceneprobegui]
|
||
==== sceneProbeGui
|
||
|
||
[source,text]
|
||
----
|
||
gui, x, y = sceneProbeGui(sx, sy)
|
||
----
|
||
|
||
Quale GUI si trova sulla superficie sotto un punto dell'overlay, e in quale punto di essa: un raggio dalla telecamera attraverso il punto, con la telecamera dell'ultimo fotogramma renderizzato, contro i triangoli di ogni nodo visibile il cui materiale mostra una GUI (`materialSetGui`), prendendo le coordinate di texture dell'intersezione più vicina come posizione sulla GUI. Restituisce l'handle della GUI e il pixel nelle coordinate proprie di quella GUI (`0, 0` in alto a sinistra, `guiGetWidth`, `guiGetHeight` in basso a destra), oppure solo `nil` quando il punto non cade su nessuna di queste superfici, la scena è disattivata o non è ancora stato renderizzato nulla. Il motore si pone la stessa domanda quando il mouse o una pistola ottica puntano sulla scena mentre una GUI che accetta input si trova su una superficie e sotto il puntatore non c'è nulla disegnato in piano, quindi i clic su un monitor nella scena non richiedono script; la chiamata serve a un gioco che vuole la risposta per sé, per un mirino laser, un effetto al passaggio del puntatore o un test.
|
||
|
||
*Parametri:*
|
||
|
||
* `sx, sy` -- il punto, in coordinate dell'overlay (la posizione di un mouse o di una pistola ottica).
|
||
|
||
*Restituisce:* Un intero e due interi, o `nil`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<materialsetgui,materialSetGui>>, <<guisetinput,guiSetInput>>, <<sceneproject,sceneProject>>, <<sceneunproject,sceneUnproject>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il punto di una pistola ottica si illumina dove cade sullo schermo della cabina di pilotaggio.
|
||
function onMouseMoved(x, y)
|
||
local g, px, py = sceneProbeGui(x, y)
|
||
if g == screenGui then
|
||
guiSetValue(g, screenPage, "dot", string.format("%d,%d", px, py))
|
||
end
|
||
end
|
||
----
|
||
|
||
[#sceneproject]
|
||
==== sceneProject
|
||
|
||
[source,text]
|
||
----
|
||
x, y, depth, inFront = sceneProject(wx, wy, wz)
|
||
----
|
||
|
||
Dove cade un punto del mondo in coordinate dell'overlay, con la telecamera dell'ultimo fotogramma renderizzato. `depth` vale `0` al piano vicino e `1` al piano lontano, per ordinare le etichette o sfumarle con la distanza. `inFront` è `false` quando il punto è dietro la telecamera, e allora `x` e `y` non hanno senso; prima che sia stato renderizzato il primo fotogramma non c'è nulla rispetto a cui proiettare, e ogni valore torna `0` con `inFront` a `false`. I punti davanti ma fuori dall'inquadratura tornano con coordinate oltre i bordi dell'overlay, quindi confrontale con `sceneGetSize` prima di disegnare. L'uso tipico è un'etichetta con il nome o una barra della salute disegnata con `fontPrint` o `spriteDraw` sopra un oggetto 3D.
|
||
|
||
*Parametri:*
|
||
|
||
* `wx, wy, wz` -- il punto del mondo, in unità del mondo.
|
||
|
||
*Restituisce:* Due numeri, un numero e un booleano.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<sceneunproject,sceneUnproject>>, <<nodegetworldposition,nodeGetWorldPosition>>, <<scenegetsize,sceneGetSize>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un nome e una barra della salute che fluttuano sopra ogni personaggio.
|
||
function onOverlayUpdate()
|
||
for _, enemy in ipairs(enemies) do
|
||
local x, y, depth, visible = sceneProject(nodeGetWorldPosition(enemy.node))
|
||
if visible then
|
||
fontPrint(x - 30, y - 80, enemy.name)
|
||
overlayBox(x - 30, y - 60, x - 30 + enemy.health * 0.6, y - 54)
|
||
end
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#sceneunproject]
|
||
==== sceneUnproject
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z = sceneUnproject(sx, sy, distance)
|
||
----
|
||
|
||
Il punto del mondo a `distance` unità lungo il raggio che passa per un pixel dell'overlay, con la telecamera dell'ultimo fotogramma renderizzato. Due distanze danno un raggio per la selezione con `physicsRaycast`; una sola colloca un oggetto «dove il giocatore ha cliccato». Prima che sia stato disegnato il primo fotogramma restituisce l'origine.
|
||
|
||
*Parametri:*
|
||
|
||
* `sx, sy` -- il pixel dell'overlay, in coordinate dell'overlay.
|
||
* `distance` -- quanto lontano lungo il raggio, in unità del mondo; `0` è il piano vicino.
|
||
|
||
*Restituisce:* Tre numeri, il punto del mondo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<sceneproject,sceneProject>>, <<mousegetposition,mouseGetPosition>>, <<physicsraycast,physicsRaycast>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Posa un segnaposto su ciò che il giocatore ha cliccato, o a 8 unità di distanza se non c'è niente.
|
||
function onMouseDown(button)
|
||
local mx, my = mouseGetPosition(0)
|
||
local ox, oy, oz = sceneUnproject(mx, my, 0)
|
||
local fx, fy, fz = sceneUnproject(mx, my, 100)
|
||
local hit, hx, hy, hz = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz)
|
||
if hit then
|
||
nodeSetPosition(marker, hx, hy, hz)
|
||
else
|
||
nodeSetPosition(marker, sceneUnproject(mx, my, 8))
|
||
end
|
||
end
|
||
----
|
||
|
||
[#scenesetambient]
|
||
==== sceneSetAmbient
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetAmbient(r, g, b)
|
||
----
|
||
|
||
Luce da ogni direzione, da `0` a `255` per canale, trattata come sRGB; il valore predefinito è un grigio tenue di `26, 26, 26`. Schiarisce i lati che nessuna luce raggiunge perché non restino neri, e `materialSetOcclusionMap` la scurisce nelle fessure. Quando è impostato un cielo e `sceneSetEnvironment` è attivo, la luce del cielo sostituisce questo valore.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<lightnew,lightNew>>, <<scenesetenvironment,sceneSetEnvironment>>, <<scenesetsky,sceneSetSky>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un cortile al chiaro di luna: un blu tenue ovunque, una fredda luna direzionale.
|
||
sceneSetAmbient(20, 25, 45)
|
||
moon = lightNew(LIGHT_DIRECTIONAL)
|
||
nodeSetPosition(moon, -5, 10, 3)
|
||
nodeLookAt(moon, 0, 0, 0)
|
||
lightSetColor(moon, 170, 190, 255)
|
||
lightSetIntensity(moon, 0.4)
|
||
----
|
||
|
||
[#scenesetantialias]
|
||
==== sceneSetAntialias
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetAntialias(antialias)
|
||
----
|
||
|
||
Attiva o disattiva il multisampling 4x; è attivo in modo predefinito ovunque la GPU lo offra per i formati del livello, e manca senza avvisi dove non lo offre. Disattivarlo fa guadagnare velocità su un Raspberry Pi. I render target vengono ricostruiti subito, quindi chiamalo durante la preparazione anziché a ogni fotogramma.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<sceneenable,sceneEnable>>, <<scenesetshadowsize,sceneSetShadowSize>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un'impostazione di qualità dalla schermata delle opzioni del gioco.
|
||
function applyQuality(level)
|
||
if level == "low" then
|
||
sceneSetAntialias(false)
|
||
sceneSetShadowSize(512)
|
||
sceneSetShadowCascades(2)
|
||
else
|
||
sceneSetAntialias(true)
|
||
sceneSetShadowSize(2048)
|
||
sceneSetShadowCascades(3)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#scenesetbackground]
|
||
==== sceneSetBackground
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetBackground(r, g, b)
|
||
sceneSetBackground(r, g, b, a)
|
||
----
|
||
|
||
Il colore con cui il livello viene ripulito a ogni fotogramma, da `0` a `255` per canale. `a` vale `255` quando è omesso, uno sfondo opaco; sotto `255` lascia trasparire il video, e `0` (la scelta abituale per un gioco con un disco) rende lo sfondo del tutto trasparente. Finché non viene chiamata, il livello viene ripulito a nero e trasparente. Un cielo di `sceneSetSky` viene disegnato sopra lo sfondo, quindi finché ce n'è uno impostato il video non traspare più.
|
||
|
||
*Parametri:*
|
||
|
||
* `r, g, b` -- il colore con cui ripulire.
|
||
* `a` -- l'alfa con cui ripulire; `255` se omesso.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<sceneenable,sceneEnable>>, <<scenesetsky,sceneSetSky>>, <<scenesetfog,sceneSetFog>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il disco si vede dietro la cabina di pilotaggio 3D; un cielo notturno pieno per il menu.
|
||
function showCockpit()
|
||
sceneSetBackground(0, 0, 0, 0)
|
||
discPlay()
|
||
end
|
||
|
||
function showMenu()
|
||
discPause()
|
||
sceneSetBackground(10, 12, 30)
|
||
end
|
||
----
|
||
|
||
[#scenesetbloom]
|
||
==== sceneSetBloom
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetBloom(threshold, strength)
|
||
----
|
||
|
||
Un alone attorno a tutto ciò che è più luminoso di `threshold`, preso dalla luce lineare della scena prima dell'esposizione e della curva tonale (dove `1` è il bianco), e aggiunto di nuovo con intensità `strength`. I valori predefiniti sono una soglia di `1` e un'intensità di `0`, cioè nessun bloom; da `0.2` a `0.4` è discreto, `1` è una foschia. L'alone viene sfocato su una catena di passate a metà dimensione, quindi costa poco, e lo riceve solo la telecamera della finestra: le viste renderizzate lo saltano. I valori negativi vengono portati a `0`.
|
||
|
||
*Parametri:*
|
||
|
||
* `threshold` -- la luminosità lineare sopra la quale un pixel brilla; `1` è il bianco prima della curva tonale.
|
||
* `strength` -- quanto dell'alone viene aggiunto di nuovo; `0` per niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<scenesetexposure,sceneSetExposure>>, <<materialsetemissive,materialSetEmissive>>, <<scenesettonemap,sceneSetTonemap>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il neon e i fari brillano; l'asfalto no.
|
||
sceneSetBloom(1.0, 0.35)
|
||
neonLook = materialNew()
|
||
materialSetColor(neonLook, 255, 40, 120)
|
||
materialSetEmissive(neonLook, 255, 40, 120)
|
||
nodeSetMesh(sign, meshBox(2, 0.3, 0.05), neonLook)
|
||
----
|
||
|
||
[#scenesetenvironment]
|
||
==== sceneSetEnvironment
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetEnvironment(lit)
|
||
----
|
||
|
||
Se il cielo impostato da `sceneSetSky` illumina la scena: la sua luce diffusa da ogni direzione al posto di `sceneSetAmbient`, e i suoi riflessi su metalli e superfici lucide, nitidi su uno specchio e sfocati man mano che la rugosità aumenta. Attivo in modo predefinito, così un cielo illumina la scena appena viene impostato; `false` tiene il cielo solo come fondale e torna la luce ambiente uniforme. Senza un cielo non ha effetto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<scenesetsky,sceneSetSky>>, <<scenesetambient,sceneSetAmbient>>, <<scenesetskyintensity,sceneSetSkyIntensity>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un fondale dipinto che non deve tingere le cromature.
|
||
sceneSetSky(DIR .. "Skies/matteBackdrop.png")
|
||
sceneSetEnvironment(false)
|
||
sceneSetAmbient(40, 40, 45)
|
||
----
|
||
|
||
[#scenesetexposure]
|
||
==== sceneSetExposure
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetExposure(stops)
|
||
----
|
||
|
||
Schiarisce o scurisce l'intera scena in stop fotografici, da `-10` a `10` (i valori fuori intervallo vengono limitati): `+1` raddoppia la luce, `-1` la dimezza. Il valore predefinito è `0`. L'esposizione viene applicata dopo il bloom e prima della curva tonale, quindi una scena luminosa a `+2` si comprime verso il bianco anziché saturare, e una grotta e una spiaggia possono condividere le stesse luci. Animala per un adattamento lento quando si passa da un interno alla luce del giorno.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<scenesettonemap,sceneSetTonemap>>, <<lightsetintensity,lightSetIntensity>>, <<scenesetskyintensity,sceneSetSkyIntensity>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Occhi che si adattano: avvicina piano l'esposizione all'obiettivo della zona a ogni fotogramma.
|
||
targetStops = 0
|
||
currentStops = 0
|
||
|
||
function onEnterArea(area)
|
||
targetStops = area.indoors and 1.5 or -0.5
|
||
end
|
||
|
||
function onOverlayUpdate()
|
||
currentStops = currentStops + (targetStops - currentStops) * 0.05
|
||
sceneSetExposure(currentStops)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#scenesetfog]
|
||
==== sceneSetFog
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetFog(r, g, b, near, far)
|
||
sceneSetFog()
|
||
----
|
||
|
||
Nebbia di distanza: limpida fino a `near` unità dalla telecamera, del tutto del colore della nebbia a `far` e oltre, sulle mesh (anche quelle non illuminate) come sulle particelle; le particelle additive svaniscono invece di diventare grigie. Senza argomenti, o con un `far` non maggiore di `near`, non c'è nebbia. Il cielo non viene annebbiato, quindi scegli un colore vicino al suo orizzonte, o allo sfondo quando non c'è un cielo. Qualsiasi altro numero di argomenti termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `r, g, b` -- il colore della nebbia, da `0` a `255`.
|
||
* `near` -- la distanza a cui inizia la nebbia, in unità del mondo dalla telecamera.
|
||
* `far` -- la distanza a cui non resta altro che nebbia.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<scenesetsky,sceneSetSky>>, <<scenesetbackground,sceneSetBackground>>, <<camerasetperspective,cameraSetPerspective>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Foschia mattutina sulle colline, che si dissolve mentre il livello va avanti.
|
||
sceneSetSky(DIR .. "Skies/dawn.hdr")
|
||
sceneSetFog(190, 200, 215, 40, 160)
|
||
|
||
function onOverlayUpdate()
|
||
if levelTime > 60 then
|
||
sceneSetFog()
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#scenesetshadowcascades]
|
||
==== sceneSetShadowCascades
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetShadowCascades(count)
|
||
----
|
||
|
||
In quante mappe è divisa l'ombra di una luce direzionale lungo la vista, da `1` a `4` (i valori fuori intervallo vengono limitati; il predefinito è `3`). Ogni cascata copre una fetta dell'inquadratura della telecamera fino a `sceneSetShadowDistance`, e la fetta più vicina è la più piccola, così le ombre vicino alla telecamera sono nitide per quanto grande sia il livello. `1` adatta un'unica mappa all'intera scena, il che va bene per scene piccole, ed è sempre usato con una telecamera ortografica. Ogni cascata è una passata d'ombra in più per ogni luce direzionale che proietta ombre; `2` è una scelta ragionevole su un Raspberry Pi.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<scenesetshadowdistance,sceneSetShadowDistance>>, <<scenesetshadowsize,sceneSetShadowSize>>, <<lightsetshadow,lightSetShadow>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un grande livello all'aperto: ombre nitide vicino al giocatore, che arrivano lontano.
|
||
sun = lightNew(LIGHT_DIRECTIONAL)
|
||
nodeSetPosition(sun, 30, 40, 20)
|
||
nodeLookAt(sun, 0, 0, 0)
|
||
lightSetShadow(sun, true)
|
||
sceneSetShadowCascades(4)
|
||
sceneSetShadowDistance(120)
|
||
sceneSetShadowSize(2048)
|
||
----
|
||
|
||
[#scenesetshadowdistance]
|
||
==== sceneSetShadowDistance
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetShadowDistance(distance)
|
||
----
|
||
|
||
Fin dove arrivano, a partire dalla telecamera, le ombre a cascata, in unità del mondo; il valore predefinito è `60` e i valori sotto `1` vengono portati a `1`. Oltre questa distanza una luce direzionale non proietta ombre. Distanze più brevi rendono ogni cascata più nitida, perché le stesse mappe coprono meno terreno, quindi impostala più o meno alla distanza a cui il giocatore riesce a distinguere un'ombra. Conta solo con più di una cascata sotto una telecamera prospettica.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<scenesetshadowcascades,sceneSetShadowCascades>>, <<scenesetshadowsize,sceneSetShadowSize>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Le ombre devono arrivare solo fino alla parete in fondo all'arena.
|
||
sceneSetShadowCascades(2)
|
||
sceneSetShadowDistance(35)
|
||
sun = lightNew(LIGHT_DIRECTIONAL)
|
||
nodeSetPosition(sun, 10, 20, 8)
|
||
nodeLookAt(sun, 0, 0, 0)
|
||
lightSetShadow(sun, true)
|
||
----
|
||
|
||
[#scenesetshadowsize]
|
||
==== sceneSetShadowSize
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetShadowSize(size)
|
||
----
|
||
|
||
Texel per lato di ogni mappa delle ombre, cascata e faccia del cubo, da `256` a `4096` (i valori fuori intervallo vengono limitati; il predefinito è `1024`). Più grande è più nitido e più lento, in memoria oltre che in tempo, perché ogni luce che proietta ombre ha le proprie mappe di questa dimensione. `2048` va bene per un desktop, `512` per un Raspberry Pi. Le mappe vengono ricostruite al fotogramma successivo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<lightsetshadow,lightSetShadow>>, <<scenesetshadowcascades,sceneSetShadowCascades>>, <<scenesetantialias,sceneSetAntialias>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Dimensiona le mappe delle ombre in base alla macchina.
|
||
if singeGetPlatform() == "raspberrypi" then
|
||
sceneSetShadowSize(512)
|
||
else
|
||
sceneSetShadowSize(2048)
|
||
end
|
||
----
|
||
|
||
[#scenesetsky]
|
||
==== sceneSetSky
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetSky(file)
|
||
sceneSetSky(nil)
|
||
sceneSetSky()
|
||
----
|
||
|
||
Avvolge la scena in un panorama equirettangolare: un `.hdr` Radiance (come lo distribuiscono i siti di HDRI, mantenendone la gamma), o un PNG, un JPEG o un'altra immagine che SDL_image sa leggere, decodificata da sRGB. Il nome è relativo al gioco come per `spriteLoad`. Il cielo viene disegnato dietro tutto al posto del colore di sfondo (quindi il video non traspare più) e illumina la scena a meno di `sceneSetEnvironment(false)`. Il suo centro guarda verso -Z e la sua parte alta sta in alto. Viene ricampionato in un cubo di al massimo 1024 texel per faccia, quindi un'immagine di 2048 per 1024 basta e avanza, e quelle più grandi richiedono un momento per essere preparate su un Raspberry Pi. Senza argomento, o con `nil`, il cielo viene rimosso. Termina lo script quando il file non può essere letto o il 3D non è disponibile.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<scenesetskyintensity,sceneSetSkyIntensity>>, <<scenesetenvironment,sceneSetEnvironment>>, <<scenesetfog,sceneSetFog>>, <<scenesetbackground,sceneSetBackground>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un cielo coperto per il livello del porto, con la nebbia abbinata al suo orizzonte.
|
||
sceneEnable(true)
|
||
sceneSetSky(DIR .. "Skies/overcastHarbor.hdr")
|
||
sceneSetSkyIntensity(0.8)
|
||
sceneSetFog(190, 200, 215, 25, 90)
|
||
|
||
function onLevelEnd()
|
||
sceneSetSky()
|
||
sceneSetBackground(0, 0, 0, 0)
|
||
end
|
||
----
|
||
|
||
[#scenesetskyintensity]
|
||
==== sceneSetSkyIntensity
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetSkyIntensity(scale)
|
||
----
|
||
|
||
La luminosità del cielo e della luce che proietta, `1` com'è stato caricato; i valori negativi vengono portati a `0`. Utile per attenuare un cielo PNG troppo luminoso, per fare di un cielo `.hdr` la luce principale della scena, o per far comparire e scomparire il cielo in dissolvenza. Moltiplica il cielo stesso, a differenza di `sceneSetExposure`, che scala tutto.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<scenesetsky,sceneSetSky>>, <<scenesetexposure,sceneSetExposure>>, <<scenesetenvironment,sceneSetEnvironment>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Crepuscolo: il cielo e la sua luce si attenuano nell'arco di dieci secondi.
|
||
sceneSetSky(DIR .. "Skies/sunset.hdr")
|
||
duskStart = singeGetTicks()
|
||
|
||
function onOverlayUpdate()
|
||
local t = math.min((singeGetTicks() - duskStart) / 10000, 1)
|
||
sceneSetSkyIntensity(1 - 0.8 * t)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#scenesettonemap]
|
||
==== sceneSetTonemap
|
||
|
||
[source,text]
|
||
----
|
||
sceneSetTonemap(curve)
|
||
----
|
||
|
||
La curva che porta la luce lineare della scena sullo schermo, applicata dopo l'esposizione. `TONEMAP_NEUTRAL` (il valore predefinito) mantiene i colori fedeli e comprime solo la luce più intensa; `TONEMAP_ACES` è la curva cinematografica, con più contrasto e un passaggio alle alte luci più caldo; `TONEMAP_NONE` taglia al bianco. I materiali video, le superfici non illuminate e il cielo passano per la stessa curva, quindi un gioco il cui disco deve apparire esattamente come è stato girato vuole `TONEMAP_NONE` con un'esposizione di `0`. Qualsiasi altro valore termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<scenesetexposure,sceneSetExposure>>, <<scenesetbloom,sceneSetBloom>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un aspetto cinematografico per la scena d'intermezzo, colori fedeli per il gioco.
|
||
function onCutsceneStart()
|
||
sceneSetTonemap(TONEMAP_ACES)
|
||
sceneSetExposure(0.5)
|
||
end
|
||
|
||
function onCutsceneEnd()
|
||
sceneSetTonemap(TONEMAP_NEUTRAL)
|
||
sceneSetExposure(0)
|
||
end
|
||
----
|
||
|
||
[#script]
|
||
=== Script
|
||
|
||
WARNING: Queste funzioni permettono a uno script di Singe di passare il controllo a un altro script di Singe: è il meccanismo dietro il launcher incorporato `Menu.singe`. Chi sviluppa giochi non dovrebbe ricorrervi; costruisci il tuo gioco come un unico script e lascia che sia il sistema dei menu a gestire il concatenamento. Sono documentate qui per completezza e per chi si occupa del menu stesso.
|
||
|
||
Entrambe le chiamate prendono una tabella con la forma di una voce `GAMES[]` di `games.dat` e la mettono in coda per l'esecutore di script del motore. La nuova configurazione parte da quella in esecuzione, quindi le opzioni da riga di comando come la cartella dei dati e la traccia vengono mantenute, ma il disco, il video e il contenitore mai: ogni voce porta i propri. Lo script corrente termina alla fine del fotogramma, scatta `onShutdown` e lo script in coda parte con uno stato Lua nuovo (vedi <<enginecallbacks,Callback del motore>>). La cartella dei dati del nuovo script segue la stessa regola di un avvio da riga di comando (vedi `singeGetDataPath`); quando non può essere creata, lo script chiamante viene terminato.
|
||
|
||
[#scriptexecute]
|
||
==== scriptExecute
|
||
|
||
[source,text]
|
||
----
|
||
scriptExecute(config)
|
||
----
|
||
|
||
Sostituisce lo script in esecuzione con quello descritto dalla tabella. Lo script corrente si ferma alla fine di questo fotogramma e viene eseguita `onShutdown`; il controllo non gli torna mai, e quando il nuovo script esce il motore passa a qualunque altra cosa sia in coda, oppure esce. Le chiavi stringa che il motore conosce sono `SCRIPT`, `CONTAINER`, `VIDEO`, `STRETCH`, `NO_MOUSE`, `RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, `AUDIO_DELAY`, `LEGACY_SPRITE_ARGS`, `CANVAS_X` e `CANVAS_Y`; le altre chiavi vengono ignorate e quelle che non sono stringhe vengono saltate. Un `SCRIPT`, un `CONTAINER` o un `VIDEO` che non è una stringa termina lo script, e così un argomento che non è una tabella.
|
||
|
||
*Parametri:*
|
||
|
||
* `config` -- una tabella di campi di `games.dat`, come minimo `SCRIPT` o `CONTAINER`.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<scriptpush,scriptPush>>, <<singequit,singeQuit>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un launcher che non torna mai a se stesso.
|
||
function onInputPressed(what)
|
||
if what == SWITCH_START1 then
|
||
scriptExecute({
|
||
SCRIPT = DIR .. "../MyGame/MyGame.singe",
|
||
VIDEO = DIR .. "../MyGame/MyGame.mp4",
|
||
})
|
||
end
|
||
end
|
||
----
|
||
|
||
[#scriptpush]
|
||
==== scriptPush
|
||
|
||
[source,text]
|
||
----
|
||
scriptPush(config)
|
||
----
|
||
|
||
Avvia lo script descritto dalla tabella e mette in coda dietro di esso la configurazione corrente, così quando il nuovo script esce lo script corrente riparte dall'inizio. Non viene conservato nulla: `onShutdown` dello script corrente viene eseguita subito, il suo stato Lua viene scartato e l'esecuzione successiva è un avvio da zero, ed è per questo che `Menu.singe` salva la propria selezione in un file prima della chiamata. La tabella accetta le stesse chiavi di `scriptExecute`.
|
||
|
||
*Parametri:*
|
||
|
||
* `config` -- una tabella di campi di `games.dat`, come per `scriptExecute`.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<scriptexecute,scriptExecute>>, <<singequit,singeQuit>>, <<onshutdown,onShutdown>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Menu.singe: avvia il gioco selezionato e poi torna qui.
|
||
function onInputPressed(what)
|
||
if what == SWITCH_START1 or what == SWITCH_BUTTON1 then
|
||
SHUTDOWN_FROM_PUSH = true
|
||
scriptPush(GAME_LIST[GAME_SELECTED])
|
||
end
|
||
end
|
||
----
|
||
|
||
[#singe]
|
||
=== Singe
|
||
|
||
Le funzioni dello spazio dei nomi `singe*` controllano il motore stesso: la finestra, gli screenshot, il sistema della pausa, l'uscita e il ricaricamento, il ritardo audio usato per la sincronizzazione labiale, e alcuni percorsi e indicatori che vengono dalla riga di comando e da `games.dat`. Nessuna di queste prende un handle. Il comportamento della pausa è descritto in <<pausing,La pausa>> e i valori del ritardo audio in <<audiosync,Sincronizzazione dell'audio>>.
|
||
|
||
[#allowsocketcall]
|
||
==== allowSocketCall
|
||
|
||
[source,text]
|
||
----
|
||
allowed = allowSocketCall()
|
||
----
|
||
|
||
Estensione di Hypseus, sempre `false` in Singe. Hypseus risponde se l'utente gli ha permesso di inviare dati a un server di statistiche, cosa che il suo `-noserversend` disattiva; lì l'invio vero e proprio è commentato. Singe non invia nulla da nessuna parte, quindi niente è permesso e la risposta non cambia mai. Esiste perché un gioco che lo chiede non si fermi su una funzione mancante.
|
||
|
||
*Restituisce:* un booleano, sempre `false`.
|
||
|
||
*Disponibile da:* 3.00
|
||
|
||
[#getuserstring]
|
||
==== getUserString
|
||
|
||
[source,text]
|
||
----
|
||
id = getUserString()
|
||
----
|
||
|
||
Estensione di Hypseus, sempre una stringa vuota. Nonostante il nome, non è una stringa impostata dall'utente: Hypseus risponde con l'ID di installazione della macchina che invia insieme alle statistiche di rete, preso dal `MachineGuid` di Windows o dal machine ID di Linux. Singe non invia nulla da nessuna parte e non consegna a un gioco un'impronta identificativa della macchina su cui gira.
|
||
|
||
*Restituisce:* una stringa, sempre vuota.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<allowsocketcall,allowSocketCall>>
|
||
|
||
[#singedisablepausekey]
|
||
==== singeDisablePauseKey
|
||
|
||
[source,text]
|
||
----
|
||
singeDisablePauseKey()
|
||
----
|
||
|
||
Cede il tasto di pausa allo script, esattamente come fa `singeSetPauseKeyEnabled(false)`. È una funzione definita in `Framework.singe`, mantenuta perché i giochi più vecchi funzionino senza modifiche.
|
||
|
||
*Note:* Alias storico.
|
||
|
||
WARNING: Il codice nuovo dovrebbe chiamare <<singesetpausekeyenabled,singeSetPauseKeyEnabled>> con `false`.
|
||
|
||
*Disponibile da:* 1.18 (RDG)
|
||
*Vedi anche:* <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singeenablepausekey,singeEnablePauseKey>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Codice di un gioco più vecchio; la forma moderna è singeSetPauseKeyEnabled(false).
|
||
singeDisablePauseKey()
|
||
|
||
function onInputPressed(what)
|
||
if what == SWITCH_PAUSE then
|
||
togglePauseMenu()
|
||
end
|
||
end
|
||
----
|
||
|
||
[#singeenablepausekey]
|
||
==== singeEnablePauseKey
|
||
|
||
[source,text]
|
||
----
|
||
singeEnablePauseKey()
|
||
----
|
||
|
||
Ridà il tasto di pausa al motore, esattamente come fa `singeSetPauseKeyEnabled(true)`. È una funzione definita in `Framework.singe`, mantenuta perché i giochi più vecchi funzionino senza modifiche.
|
||
|
||
*Note:* Alias storico.
|
||
|
||
WARNING: Il codice nuovo dovrebbe chiamare <<singesetpausekeyenabled,singeSetPauseKeyEnabled>> con `true`.
|
||
|
||
*Disponibile da:* 1.18 (RDG)
|
||
*Vedi anche:* <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singedisablepausekey,singeDisablePauseKey>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Codice di un gioco più vecchio; la forma moderna è singeSetPauseKeyEnabled(true).
|
||
function pauseMenuClose()
|
||
singeSetPauseFlag(false)
|
||
singeEnablePauseKey()
|
||
end
|
||
----
|
||
|
||
[#utilsha256]
|
||
==== utilSha256
|
||
|
||
[source,text]
|
||
----
|
||
digest = utilSha256(data)
|
||
----
|
||
|
||
Lo SHA-256 di una stringa, come 64 caratteri esadecimali minuscoli. Funziona anche con i dati
|
||
binari, quindi lavora sui byte di un file con la stessa facilità che sul testo.
|
||
|
||
Serve a decidere se i byte che hai sono quelli che ti erano stati promessi: un gioco scaricato
|
||
confrontato con l'impronta che riporta la sua voce di catalogo, o la chiave pubblica di un server
|
||
confrontata con quella fissata in anticipo. Il modulo `md5` incluso c'è ancora per un checksum, ma
|
||
un'impronta usata per decidere se *fidarsi* di qualcosa deve essere una di cui valga ancora la pena
|
||
fidarsi.
|
||
|
||
*Parametri:*
|
||
|
||
* `data` -- una stringa di byte. Una stringa vuota termina lo script.
|
||
|
||
*Restituisce:* stringa, 64 caratteri esadecimali minuscoli.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Rifiuta un download che non è arrivato intatto.
|
||
local file = io.open("game.part", "rb")
|
||
local bytes = file:read("*a")
|
||
file:close()
|
||
if utilSha256(bytes) ~= expected then
|
||
os.remove("game.part")
|
||
end
|
||
----
|
||
|
||
[#singecapabilities]
|
||
==== singeCapabilities
|
||
|
||
[source,text]
|
||
----
|
||
capabilities = singeCapabilities()
|
||
----
|
||
|
||
Che cosa può far girare questa macchina. La tabella ha una voce per ogni nome di
|
||
capacità che può comparire in un elenco `REQUIRES` di `games.dat` (`SCENE`,
|
||
`GUI`, `SHADOWS`, `POST`, `SKIN`, `MORPH`), ciascuna `true` o `false`, e `tier`:
|
||
la fascia di prestazioni misurata della macchina, da 0 (niente 3D) a 3 (una GPU
|
||
da desktop), o `nil` prima che il menu l'abbia misurata. Un gioco lo chiede per
|
||
poter fare a meno di qualcosa invece di essere rifiutato perché gli manca.
|
||
|
||
*Restituisce:* una tabella.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<singehasgpu,singeHasGpu>>, <<singesavetier,singeSaveTier>>, <<scenemeasure,sceneMeasure>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ombre dove la macchina le regge, un sole semplice dove non le regge.
|
||
local machine = singeCapabilities()
|
||
|
||
lightSetShadow(sun, machine.SHADOWS)
|
||
if (machine.tier or 0) < 2 then
|
||
sceneSetBloom(1.0, 0.0)
|
||
end
|
||
----
|
||
|
||
[#singegetaudiocalibration]
|
||
==== singeGetAudioCalibration
|
||
|
||
[source,text]
|
||
----
|
||
milliseconds = singeGetAudioCalibration()
|
||
----
|
||
|
||
Restituisce il ritardo audio della macchina in vigore, in millisecondi: il valore che la schermata di calibrazione del menu ha salvato in `machine.cfg` nella radice dei dati, o uno impostato in seguito con `singeSetAudioCalibration`. Zero quando la macchina non è mai stata calibrata. Vale per ogni gioco della macchina, in aggiunta al ritardo del singolo gioco di `singeGetAudioDelay`.
|
||
|
||
*Restituisce:* millisecondi interi.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<singesetaudiocalibration,singeSetAudioCalibration>>, <<singegetaudiodelay,singeGetAudioDelay>>, <<singegetaudiolatency,singeGetAudioLatency>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Menu.singe: avvia la schermata di calibrazione dal valore salvato.
|
||
function calibrationBegin()
|
||
CALIBRATING = true
|
||
CAL_ORIGINAL = singeGetAudioCalibration()
|
||
CAL_VALUE = CAL_ORIGINAL
|
||
CAL_LATENCY = singeGetAudioLatency()
|
||
CAL_BEAT = singeGetTicks() - CAL_PERIOD
|
||
end
|
||
----
|
||
|
||
[#singegetaudiodelay]
|
||
==== singeGetAudioDelay
|
||
|
||
[source,text]
|
||
----
|
||
milliseconds = singeGetAudioDelay()
|
||
----
|
||
|
||
Restituisce il ritardo audio del gioco in vigore, in millisecondi, che venga da `--audiodelay`, dalla chiave `AUDIO_DELAY` della voce del gioco in `games.dat` o da `singeSetAudioDelay`. Zero quando non ne è stato impostato nessuno. Si somma alla calibrazione della macchina e alla latenza misurata del dispositivo quando il motore decide quando mostrare ciascun fotogramma del video.
|
||
|
||
*Restituisce:* millisecondi interi.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<singesetaudiodelay,singeSetAudioDelay>>, <<singegetaudiocalibration,singeGetAudioCalibration>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Menu di servizio: mostra lo scarto di sincronizzazione attuale accanto al suo cursore.
|
||
function drawSyncSetting(y)
|
||
local delay = singeGetAudioDelay()
|
||
fontPrint(40, y, string.format("A/V offset: %+d ms", delay))
|
||
end
|
||
----
|
||
|
||
[#singegetaudiolatency]
|
||
==== singeGetAudioLatency
|
||
|
||
[source,text]
|
||
----
|
||
milliseconds = singeGetAudioLatency()
|
||
----
|
||
|
||
Restituisce la coda del dispositivo audio che il motore ha misurato all'apertura del mixer, in millisecondi: il tempo tra la consegna dei campioni al mixer e il loro arrivo al dispositivo, prima di qualsiasi calibrazione o ritardo del gioco. Il motore la compensa già; una schermata di calibrazione ne ha bisogno per programmare il suo clic e il suo lampo, e vale la pena stamparla in una schermata di diagnostica.
|
||
|
||
*Restituisce:* millisecondi interi.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<singegetaudiocalibration,singeGetAudioCalibration>>, <<singegetaudiodelay,singeGetAudioDelay>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Menu.singe: il lampo segue il clic di un tempo pari alla coda del dispositivo più il valore candidato.
|
||
function calibrationUpdate()
|
||
local now = singeGetTicks()
|
||
local gap = CAL_LATENCY + CAL_VALUE
|
||
if now - CAL_BEAT >= CAL_PERIOD then
|
||
CAL_BEAT = now
|
||
CAL_CLICK_AT = now + math.max(0, -gap)
|
||
CAL_FLASH_AT = now + math.max(0, gap)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#singegetdatapath]
|
||
==== singeGetDataPath
|
||
|
||
[source,text]
|
||
----
|
||
path = singeGetDataPath()
|
||
----
|
||
|
||
Restituisce la cartella in cui Singe scrive per questo gioco, con un separatore di percorso finale così che vi si possa accodare direttamente il nome di un file. È una sottocartella della base di `--datadir` che prende il nome dal gioco (per un gioco impacchettato, il nome del database e poi la cartella dello script al suo interno); senza `--datadir` la base è `data` nella cartella dei giochi. Lì vanno gli screenshot, gli indici dei video e `trace.txt`, e lì dovrebbero andare anche i salvataggi, i punteggi record e le impostazioni; Singe 2.x li scriveva accanto al gioco, che può essere di sola lettura.
|
||
|
||
*Restituisce:* una stringa di percorso che termina con un separatore.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<singegetscriptpath,singeGetScriptPath>>, <<singescreenshot,singeScreenshot>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Menu.singe conserva la sua ultima selezione tra un'esecuzione e l'altra.
|
||
CONFIG_FILE = singeGetDataPath() .. "menu.dat"
|
||
|
||
function saveConfig()
|
||
local cfg = io.open(CONFIG_FILE, "w")
|
||
if cfg then
|
||
cfg:write("GAME_SELECTED = " .. GAME_SELECTED .. "\n")
|
||
cfg:close()
|
||
else
|
||
debugPrint("Unable to write " .. CONFIG_FILE)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#singegetgameid]
|
||
==== singeGetGameId
|
||
|
||
[source,text]
|
||
----
|
||
id = singeGetGameId()
|
||
----
|
||
|
||
Il `GAME_ID` della voce di questo gioco in `games.dat`, o `nil` quando non ne ha uno. È l'id con cui
|
||
il servizio online conosce il gioco, e la chiave sotto cui viene tenuta una classifica.
|
||
|
||
**Il formato è un UUID**, nella forma canonica di 36 caratteri -- otto cifre esadecimali, tre gruppi
|
||
di quattro, poi dodici, separati da trattini:
|
||
|
||
----
|
||
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10"
|
||
----
|
||
|
||
Nient'altro viene accettato. L'id è un segmento di percorso negli URL del servizio, quindi uno che
|
||
contenesse una barra o un punto interrogativo verrebbe pubblicato e poi non sarebbe mai
|
||
raggiungibile, e due id che differissero solo per maiuscole e minuscole sarebbero due giochi per il
|
||
database e un gioco solo per chiunque li legga. Il confronto non distingue maiuscole e minuscole e
|
||
la memorizzazione è in minuscolo, quindi un id scritto in maiuscolo è lo stesso gioco.
|
||
|
||
Va bene qualsiasi UUID -- generalo come preferisci, o lascia che `publish.lua` te ne stampi uno. Un
|
||
gioco senza id si può giocare ma non entra in classifica: non ci sarebbe niente di stabile a cui
|
||
legare una classifica, e un id inventato durante l'esecuzione creerebbe una classifica diversa ogni
|
||
volta che i file vengono spostati.
|
||
|
||
**Un `.game`, un id.** Un `.game` il cui `games.dat` ha più voci -- una raccolta, o un gioco in due
|
||
lingue -- è un solo download, e ogni voce al suo interno porta lo stesso `GAME_ID`. Il servizio
|
||
online elenca ogni voce come un gioco a sé, e non accetta un `.game` le cui voci non concordano sul
|
||
loro id o in cui una voce ne è priva. I loro punteggi restano comunque separati: vedi
|
||
<<onlinescores,Punteggi record online>>.
|
||
|
||
*Restituisce:* stringa, o `nil`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<onlinescores,Punteggi record online>>, <<gamesdat,games.dat>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Offri la classifica solo quando questo gioco ha un'identità sotto cui stilarla.
|
||
if singeGetGameId() then
|
||
showLeaderboardButton()
|
||
end
|
||
----
|
||
|
||
[#singegetheight]
|
||
==== singeGetHeight
|
||
|
||
[source,text]
|
||
----
|
||
height = singeGetHeight()
|
||
----
|
||
|
||
Restituisce l'altezza della finestra di Singe in pixel dello schermo, non in unità dell'overlay, com'è la finestra al momento della chiamata. Per disegnare usa `overlayGetHeight`; questa serve a una schermata delle impostazioni che descrive lo schermo, o a decidere quanto dettaglio può permettersi una scena.
|
||
|
||
*Restituisce:* pixel interi.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<singegetwidth,singeGetWidth>>, <<overlaygetheight,overlayGetHeight>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Descrivi lo schermo nel menu di servizio.
|
||
function drawDisplayInfo(y)
|
||
fontPrint(40, y, string.format("Window %dx%d, overlay %dx%d", singeGetWidth(), singeGetHeight(), overlayGetWidth(), overlayGetHeight()))
|
||
end
|
||
----
|
||
|
||
[#singegetpauseflag]
|
||
==== singeGetPauseFlag
|
||
|
||
[source,text]
|
||
----
|
||
paused = singeGetPauseFlag()
|
||
----
|
||
|
||
Restituisce l'indicatore di pausa del motore: `true` mentre il gioco è in pausa per il tasto di pausa o per `singeSetPauseFlag`. È una cosa distinta dal fatto che il disco sia fermo su un fotogramma in pausa (`discGetState`); leggilo per sapere se una pausa è stata chiesta deliberatamente. Mentre il tasto di pausa tiene il gioco congelato non viene eseguita nessuna callback, quindi dall'interno di una callback l'indicatore può risultare `true` solo dopo un `singeSetPauseFlag(true)` dello script stesso.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 1.x (RDG)
|
||
*Vedi anche:* <<singesetpauseflag,singeSetPauseFlag>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- I timer restano fermi mentre è attiva la pausa propria del gioco.
|
||
function onOverlayUpdate()
|
||
if not singeGetPauseFlag() then
|
||
elapsed = elapsed + 1
|
||
updateEnemies()
|
||
end
|
||
drawFrame()
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#singegetscriptpath]
|
||
==== singeGetScriptPath
|
||
|
||
[source,text]
|
||
----
|
||
path = singeGetScriptPath()
|
||
----
|
||
|
||
Restituisce il percorso del file dello script in esecuzione come lo ha risolto il motore. `Framework.singe` ricava da questo valore la globale `DIR`, la cartella dello script con il separatore finale, e `DIR` è ciò che la maggior parte dei giochi usa per costruire i percorsi delle risorse.
|
||
|
||
*Restituisce:* una stringa di percorso.
|
||
|
||
*Disponibile da:* 1.15 (RDG)
|
||
*Vedi anche:* <<singegetdatapath,singeGetDataPath>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Framework.singe ricava DIR così; poi i giochi caricano i file relativamente a DIR.
|
||
DIR = singeGetScriptPath():match("(.*[/\\])") or "./"
|
||
titleSprite = spriteLoad(DIR .. "images/title.png")
|
||
----
|
||
|
||
[#singegetsysteminfo]
|
||
==== singeGetSystemInfo
|
||
|
||
[source,text]
|
||
----
|
||
info = singeGetSystemInfo()
|
||
----
|
||
|
||
Tutto ciò che l'intestazione della traccia sa di questa macchina e di questa
|
||
esecuzione, in una sola tabella. È pensata per una schermata di servizio e per
|
||
le segnalazioni di bug: la risposta a «su che cosa girava» senza chiedere al
|
||
giocatore di trovare un log.
|
||
|
||
*Restituisce:* una tabella con questi campi, tutti stringhe tranne le quattro dimensioni:
|
||
|
||
* `version` -- la versione del motore, come `SINGE_VERSION_STRING`.
|
||
* `os` -- il nome e la versione del sistema operativo, e l'architettura.
|
||
* `cpu` -- il processore, il numero dei suoi core e la memoria in megabyte.
|
||
* `renderer` -- il nome del renderer 2D di SDL in uso.
|
||
* `gpu` -- il driver SDL_GPU dietro la scena 3D, o una nota che dice che il 3D
|
||
non è disponibile.
|
||
* `decoder` -- quali decodificatori video hardware la build proverà, a parole.
|
||
* `audio` -- i formati audio compilati, separati da virgole.
|
||
* `soundFont` -- il SoundFont che usa la riproduzione MIDI, o perché non ce n'è uno.
|
||
* `midi` -- le porte MIDI aperte, o una nota che dice che non ne è stata chiesta nessuna.
|
||
* `dataPath` -- la cartella dei dati di questo gioco.
|
||
* `dataRoot` -- la cartella dei dati data al motore, un livello sopra `dataPath`,
|
||
sotto la quale scrive ogni gioco della macchina.
|
||
* `platform` -- la parte del nome della versione che indica la piattaforma, come
|
||
`Linux-x86_64`: quale versione è questo eseguibile, e quale chiedere al servizio.
|
||
* `executable` -- il percorso assoluto dell'eseguibile in esecuzione.
|
||
* `windowWidth`, `windowHeight` -- interi, la finestra in pixel.
|
||
* `canvasWidth`, `canvasHeight` -- interi, il canvas in pixel.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<singegetdatapath,singeGetDataPath>>, xref:Manual.adoc#servicetools[Gli strumenti di servizio]
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una schermata di diagnostica a un tasto.
|
||
local info = singeGetSystemInfo()
|
||
fontPrint(10, 10, "Singe " .. info.version .. " on " .. info.os)
|
||
fontPrint(10, 30, info.cpu)
|
||
fontPrint(10, 50, info.renderer .. " / " .. info.gpu)
|
||
----
|
||
|
||
[#singegetticks]
|
||
==== singeGetTicks
|
||
|
||
[source,text]
|
||
----
|
||
milliseconds = singeGetTicks()
|
||
----
|
||
|
||
Restituisce l'orologio in millisecondi dall'avvio del motore. È l'orologio per i timer, l'antirimbalzo e le animazioni, e continua a scorrere mentre il gioco è congelato dal tasto di pausa, quindi un timer che non deve contare il tempo in pausa dovrebbe leggere `singeGetPauseFlag` o misurare dal momento dello scongelamento. Di norma `os.clock()` di Lua misura il tempo del processore, che si ferma mentre il motore è inattivo; Singe lo sostituisce con i secondi di orologio dall'avvio del motore, così i giochi esistenti che lo usavano come orologio continuano a funzionare.
|
||
|
||
*Restituisce:* millisecondi interi.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<singegetpauseflag,singeGetPauseFlag>>, <<debugprint,debugPrint>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fai lampeggiare la scritta «insert coin» due volte al secondo.
|
||
function drawAttractPrompt()
|
||
local phase = (singeGetTicks() // 250) % 2
|
||
if phase == 0 then
|
||
fontPrint(120, 200, "INSERT COIN")
|
||
end
|
||
end
|
||
----
|
||
|
||
[#singegetwidth]
|
||
==== singeGetWidth
|
||
|
||
[source,text]
|
||
----
|
||
width = singeGetWidth()
|
||
----
|
||
|
||
Restituisce la larghezza della finestra di Singe in pixel dello schermo, la controparte di `singeGetHeight`. Per disegnare sull'overlay si usa invece `overlayGetWidth`.
|
||
|
||
*Restituisce:* pixel interi.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<singegetheight,singeGetHeight>>, <<overlaygetwidth,overlayGetWidth>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Scegli un budget per le texture in base alle dimensioni della finestra.
|
||
local pixels = singeGetWidth() * singeGetHeight()
|
||
if pixels > 1920 * 1080 then
|
||
textureSet = "high"
|
||
else
|
||
textureSet = "low"
|
||
end
|
||
----
|
||
|
||
[#singehasgpu]
|
||
==== singeHasGpu
|
||
|
||
[source,text]
|
||
----
|
||
present = singeHasGpu()
|
||
----
|
||
|
||
Se il dispositivo GPU si è avviato. Senza di esso tutto ciò che viene disegnato
|
||
tramite la GPU non è disponibile: la scena 3D, le particelle 3D e ogni documento
|
||
GUI. Le particelle 2D vengono disegnate con l'overlay e non hanno bisogno del
|
||
dispositivo. Chiedilo prima di chiedere una qualsiasi di queste cose, perché
|
||
<<guinew,guiNew>> e le chiamate della scena terminano lo script invece di
|
||
rispondere `false` -- non c'è modo di tentare e poi rimediare.
|
||
|
||
Una macchina può non averlo perché non ha alcun driver Vulkan, Direct3D 12 o
|
||
Metal, o perché è stato dato `--nogpu`. Il menu incluso chiama questa funzione
|
||
all'avvio e sceglie un renderer in base alla risposta (vedi xref:Manual.adoc#menurenderers[I due renderer]).
|
||
|
||
*Restituisce:* booleano, true quando è presente un dispositivo GPU.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<guinew,guiNew>>, <<singegetsysteminfo,singeGetSystemInfo>>, <<sceneperformance,Prestazioni e requisiti>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un gioco con una schermata del titolo 3D e una piatta su cui ripiegare.
|
||
if singeHasGpu() then
|
||
buildSceneTitle()
|
||
else
|
||
buildSpriteTitle()
|
||
end
|
||
----
|
||
|
||
[#singeisrecording]
|
||
==== singeIsRecording
|
||
|
||
[source,text]
|
||
----
|
||
recording = singeIsRecording()
|
||
----
|
||
|
||
Se è in corso una registrazione, o se ne è stata chiesta una che parte con il prossimo fotogramma disegnato. Vedi <<singerecordstart,singeRecordStart>>.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<singerecordstart,singeRecordStart>>, <<singerecordstop,singeRecordStop>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un punto rosso nell'angolo della schermata di pausa del gioco mentre viene registrato.
|
||
if singeIsRecording() then
|
||
overlayCircle(overlayGetWidth() - 12, 12, 5)
|
||
end
|
||
----
|
||
|
||
[#singepack]
|
||
==== singePack
|
||
|
||
[source,text]
|
||
----
|
||
result = singePack(directory, database)
|
||
----
|
||
|
||
Impacchetta il gioco in `directory` nel database in un solo file `database`, esattamente come fa `singe --pack` dalla riga di comando, e restituisce `true` se ci è riuscito. La cartella deve essere quella del gioco: una che non contenga una cartella di supporto `Singe/`, né un launcher, né nient'altro che l'impacchettatore rifiuti, e i motivi di un rifiuto vengono stampati sulla console come per la riga di comando. È ciò che permette a uno strumento di creazione di pubblicare un gioco senza uscire dal motore.
|
||
|
||
*Parametri:*
|
||
|
||
* `directory` -- la cartella del gioco, come la accetta l'impacchettatore.
|
||
* `database` -- il file `.game` da scrivere, sostituito se esiste.
|
||
|
||
*Restituisce:* `true` in caso di successo, `false` altrimenti.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<singlefile,Giochi in un solo file>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Pubblica il gioco accanto alla sua cartella e riferisci.
|
||
if singePack(singeGetDataPath() .. "CoinRun", singeGetDataPath() .. "CoinRun.game") then
|
||
debugPrint("packed")
|
||
end
|
||
----
|
||
|
||
[#singequit]
|
||
==== singeQuit
|
||
|
||
[source,text]
|
||
----
|
||
singeQuit()
|
||
----
|
||
|
||
Termina lo script in esecuzione alla fine di questo fotogramma: il ciclo di gioco si ferma, viene eseguita `onShutdown` e il motore passa al prossimo script in coda, o esce se non ce n'è nessuno. Un gioco avviato dal menu con `scriptPush` torna quindi al menu, che riparte dall'inizio. Nel modello a thread, uscire da `singeMain` la chiama per te. L'interruttore assegnato a `INPUT_QUIT` in `controls.cfg` e la chiusura della finestra terminano lo script allo stesso modo. Non chiamarla da `onShutdown`, che è già la via d'uscita.
|
||
|
||
*Disponibile da:* 1.x (RDG)
|
||
*Vedi anche:* <<onshutdown,onShutdown>>, <<singereload,singeReload>>, <<scriptpush,scriptPush>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onInputPressed(what)
|
||
if what == SWITCH_QUIT then
|
||
singeQuit()
|
||
elseif what == SWITCH_START1 and gameOver then
|
||
startGame()
|
||
end
|
||
end
|
||
----
|
||
|
||
[#singerelaunch]
|
||
==== singeRelaunch
|
||
|
||
[source,text]
|
||
----
|
||
result = singeRelaunch(path)
|
||
----
|
||
|
||
Termina il motore come fa `singeQuit` e, una volta smontato tutto, avvia al suo posto l'eseguibile in `path` con gli stessi argomenti. È così che lo strumento Engine Update del menu porta un cabinato a un Singe più recente che ha messo accanto a quello in esecuzione: il nuovo file viene reso eseguibile e avviato, e su Windows, che non sostituisce un'immagine in esecuzione, viene avviato accanto a questo con il proprio nome. Il file deve già esistere; se non esiste, la chiamata risponde `false` senza fare nulla. Qui non viene estratto né copiato nulla: il nuovo Singe estrae i propri file di supporto al primo avvio.
|
||
|
||
*Parametri:*
|
||
|
||
* `path` -- l'eseguibile da avviare.
|
||
|
||
*Restituisce:* `true` quando il riavvio è stato predisposto, `false` quando il file non c'è.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<singequit,singeQuit>>, <<singegetsysteminfo,singeGetSystemInfo>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- È stato scaricato e verificato un Singe più recente; passagli il testimone.
|
||
local folder = singeGetSystemInfo().executable:match("^(.*[/\\])")
|
||
singeRelaunch(folder .. "Singe-v3.01-Linux-x86_64")
|
||
----
|
||
|
||
[#singerecordstart]
|
||
==== singeRecordStart
|
||
|
||
[source,text]
|
||
----
|
||
singeRecordStart([name])
|
||
----
|
||
|
||
Avvia la registrazione del gioco, immagine e suono insieme, in un MP4 nella cartella dei dati del gioco (vedi `singeGetDataPath`), a partire dal prossimo fotogramma disegnato. Senza un nome, i file si chiamano `singe000.mp4` e a salire, come gli screenshot; con un nome la registrazione va in quel file, con `.mp4` aggiunto quando il nome non ha estensione, sostituendo qualsiasi file con quel nome, e un nome che contiene una cartella termina lo script. Una chiamata mentre una registrazione è in corso viene ignorata. L'interruttore assegnato a `INPUT_RECORD` in `controls.cfg`, `F10` in modo predefinito, avvia e ferma le stesse registrazioni.
|
||
|
||
L'immagine è l'intera finestra così come viene mostrata, bande nere comprese, alle dimensioni della finestra, esattamente ciò che cattura uno screenshot; il suono è il mix finito, musica, effetti e colonne sonore dei video insieme. Entrambi vengono codificati su un thread proprio, quindi il gioco paga la copia di ogni fotogramma dalla GPU e poco altro. L'immagine è H.264 dove la macchina ha un encoder hardware che Singe può usare -- VA-API su Linux, VideoToolbox su macOS, Media Foundation su Windows, V4L2 sulle schede ARM che ne hanno uno -- e MPEG-4 dall'encoder di FFmpeg stesso ovunque altrove; il suono è AAC. La traccia nomina l'encoder quando la registrazione parte, e conta gli eventuali fotogrammi scartati quando si ferma.
|
||
|
||
Durante il gioco la registrazione va a 60 fotogrammi al secondo sull'orologio reale. Un fotogramma che arriva mentre l'encoder è ancora occupato viene scartato dalla registrazione invece di far aspettare il gioco, e il suono viene spostato più avanti della durata della coda del dispositivo audio, così i due coincidono come coincidevano sullo schermo. Con `--deterministic` la registrazione va invece a un fotogramma per passo dell'orologio, e il motore aspetta l'encoder invece di scartare qualcosa; se è avviata da `--record`, un'esecuzione deterministica mixa anche il suo suono in memoria un passo alla volta, così la registrazione è esatta fotogramma per fotogramma e campione per campione.
|
||
|
||
*Parametri:*
|
||
|
||
* `name` -- stringa facoltativa; un nome di file, non un percorso.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<singerecordstop,singeRecordStop>>, <<singeisrecording,singeIsRecording>>, <<singescreenshot,singeScreenshot>>, <<deterministic,Modalità di test deterministica>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La modalità attract registra il proprio trailer: un giro completo, poi stop.
|
||
function attractBegin()
|
||
singeRecordStart("trailer")
|
||
end
|
||
|
||
function attractEnd()
|
||
singeRecordStop()
|
||
end
|
||
----
|
||
|
||
[#singerecordstop]
|
||
==== singeRecordStop
|
||
|
||
[source,text]
|
||
----
|
||
singeRecordStop()
|
||
----
|
||
|
||
Conclude la registrazione al prossimo fotogramma disegnato: ciò che è ancora in attesa viene codificato e il file viene chiuso, così si riproduce dall'inizio alla fine. Viene ignorata quando non c'è nessuna registrazione in corso. Una registrazione ancora in corso quando il gioco termina viene conclusa allo stesso modo.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<singerecordstart,singeRecordStart>>, <<singeisrecording,singeIsRecording>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Registra una partita: dal primo gettone al game over.
|
||
function onGameOver()
|
||
singeRecordStop()
|
||
end
|
||
----
|
||
|
||
[#singereload]
|
||
==== singeReload
|
||
|
||
[source,text]
|
||
----
|
||
singeReload()
|
||
----
|
||
|
||
Riavvia il gioco dal suo script alla fine di questo fotogramma, come fanno `F5` e un file modificato con `--reload`; questa chiamata funziona anche senza l'opzione. Ogni suono si ferma, lo stato Lua viene scartato senza chiamare `onShutdown`, tutto ciò che lo script ha caricato viene liberato, la scena, la fisica (compreso il suo disegno di debug), le particelle e la navigazione vengono azzerate, l'overlay torna alla sua risoluzione predefinita, e il volume degli effetti sonori, la modalità del mouse, la cattura del mouse, la modalità della tastiera, il tasto di pausa, l'ascoltatore del suono e l'indicatore di pausa tornano ai loro valori predefiniti. Tasti e pulsanti tenuti premuti durante il ricaricamento vengono ignorati finché non vengono rilasciati, come a un avvio da zero, e il disco viene parcheggiato sul suo primo fotogramma, in pausa, con la traccia audio predefinita selezionata, come all'avvio. Il motore stesso (la finestra, i controller, i mouse) resta com'è. È adatta alla voce di riavvio di un menu di debug o a un editor di livelli che ricarica ciò che ha appena salvato.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<singequit,singeQuit>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Editor: salva il livello, poi riavvia il gioco su di esso.
|
||
function onKeyPressed(keysym, scancode)
|
||
if scancode == SCANCODE.F9.value then
|
||
saveLevel(singeGetDataPath() .. "level.lua")
|
||
singeReload()
|
||
end
|
||
end
|
||
----
|
||
|
||
[#singesavegeometry]
|
||
==== singeSaveGeometry
|
||
|
||
[source,text]
|
||
----
|
||
singeSaveGeometry()
|
||
----
|
||
|
||
Conserva per questa macchina la scala, lo spostamento e la rotazione attuali
|
||
dell'immagine. Vanno in `machine.cfg` nella radice dei dati, accanto al ritardo
|
||
audio, e il motore li applica a ogni gioco che avvia in seguito, dal menu o dalla
|
||
riga di comando, e anche dopo `singeReload`. Un'opzione scritta davvero sulla
|
||
riga di comando ha la precedenza: `--scalefactor` prevale sulla scala salvata,
|
||
`--shiftx` o `--shifty` sullo spostamento salvato, `--rotate` sulla rotazione
|
||
salvata. Il file viene scritto per intero, quindi il ritardo audio viene
|
||
conservato così com'è.
|
||
|
||
È un'impostazione del cabinato, non del gioco: è il modo di mettere in squadra un
|
||
monitor una volta sola invece che una volta per gioco. Lo strumento Display del
|
||
menu incluso è esattamente questa chiamata dietro il pulsante 1.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vldpsetscale,vldpSetScale>>, <<vldpsetshift,vldpSetShift>>, <<vldpsetrotate,vldpSetRotate>>, <<singesetaudiocalibration,singeSetAudioCalibration>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il gestore è soddisfatto dell'immagine: conservala per il cabinato.
|
||
function onInputPressed(input)
|
||
if input == SWITCH_BUTTON1 then
|
||
singeSaveGeometry()
|
||
end
|
||
end
|
||
----
|
||
|
||
|
||
[#singesavetier]
|
||
==== singeSaveTier
|
||
|
||
[source,text]
|
||
----
|
||
singeSaveTier(tier)
|
||
----
|
||
|
||
Conserva la fascia di prestazioni di questa macchina, da 0 a 3, in `machine.cfg`
|
||
accanto al ritardo audio e alla geometria, dove
|
||
<<singecapabilities,singeCapabilities>> la riporta e il menu la confronta con il
|
||
`TIER` di ogni gioco. Il menu incluso misura la fascia da sé, da quanto tempo
|
||
impiega a disegnare il suo sfondo 3D (<<scenemeasure,sceneMeasure>>), e la salva
|
||
solo quando cambia; al giocatore non viene chiesto niente.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<singecapabilities,singeCapabilities>>, <<singesavegeometry,singeSaveGeometry>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una macchina senza alcun 3D è di fascia 0.
|
||
if not singeHasGpu() then
|
||
singeSaveTier(0)
|
||
end
|
||
----
|
||
|
||
[#singescreenshot]
|
||
==== singeScreenshot
|
||
|
||
[source,text]
|
||
----
|
||
singeScreenshot()
|
||
singeScreenshot(name)
|
||
----
|
||
|
||
Chiede uno screenshot dell'intera finestra, bande nere comprese, scattato dopo che è stato disegnato il prossimo fotogramma e salvato come PNG nella cartella dei dati del gioco (vedi `singeGetDataPath`). Senza un nome, i file si chiamano `singe000.png` e a salire; ogni esecuzione dello script cerca da zero il primo nome libero e gli scatti successivi proseguono dopo l'ultimo salvato. Con un nome lo scatto va in quel file, con `.png` aggiunto quando il nome non ha estensione, sostituendo qualsiasi file con quel nome; un nome che contiene una cartella termina lo script. Dare un nome allo scatto serve agli script che producono immagini apposta, come quelli che illustrano le guide di un libro, così una nuova esecuzione sostituisce l'immagine invece di aggiungerne una. La chiamata forza un ridisegno, così lo scatto viene fatto anche mentre lo schermo è inattivo. L'interruttore assegnato a `INPUT_SCREENSHOT` in `controls.cfg` fa la stessa cosa.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<singegetdatapath,singeGetDataPath>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Uno script di test cattura alcuni fotogrammi mentre sorvola la scena.
|
||
function onOverlayUpdate()
|
||
frames = frames + 1
|
||
if frames == 40 or frames == 85 or frames == 130 then
|
||
singeScreenshot()
|
||
end
|
||
if frames == 180 then
|
||
singeQuit()
|
||
end
|
||
end
|
||
----
|
||
|
||
[#singesetaudiocalibration]
|
||
==== singeSetAudioCalibration
|
||
|
||
[source,text]
|
||
----
|
||
singeSetAudioCalibration(milliseconds)
|
||
----
|
||
|
||
Imposta il ritardo audio della macchina, lo applica subito e lo scrive in `machine.cfg` nella radice dei dati, così ogni gioco della macchina lo adotta. La chiama la schermata di calibrazione del menu; può farlo anche un gioco con un proprio menu di servizio. Il valore è positivo quando l'audio si sente più tardi di quanto riporta il dispositivo; il motore ritarda di altrettanto la presentazione del video. I valori fuori da `-1000` a `1000` terminano lo script. Se `machine.cfg` non può essere scritto, il valore vale comunque per questa esecuzione e un messaggio va sulla console.
|
||
|
||
*Parametri:*
|
||
|
||
* `milliseconds` -- intero, da `-1000` a `1000`, positivo quando l'audio è in ritardo.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<singegetaudiocalibration,singeGetAudioCalibration>>, <<singesetaudiodelay,singeSetAudioDelay>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Menu.singe: conserva o scarta la calibrazione regolata dal giocatore.
|
||
function calibrationEnd(save)
|
||
if save then
|
||
singeSetAudioCalibration(CAL_VALUE)
|
||
end
|
||
CALIBRATING = false
|
||
end
|
||
----
|
||
|
||
[#singesetaudiodelay]
|
||
==== singeSetAudioDelay
|
||
|
||
[source,text]
|
||
----
|
||
singeSetAudioDelay(milliseconds)
|
||
----
|
||
|
||
Imposta il ritardo audio del gioco per questa esecuzione: quanto più tardi (positivo) o prima (negativo) si sente l'audio rispetto a ciò che il motore può misurare, così che la presentazione del video si sposti per coincidere. Vale per il disco e per ogni video che lo script riproduce, in aggiunta alla calibrazione della macchina. Non viene salvato nulla; un gioco che offre la regolazione in un menu di servizio conserva il valore con le sue altre impostazioni e richiama questa funzione all'avvio. I valori fuori da `-1000` a `1000` terminano lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `milliseconds` -- intero, da `-1000` a `1000`, positivo quando l'audio è in ritardo.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<singegetaudiodelay,singeGetAudioDelay>>, <<singesetaudiocalibration,singeSetAudioCalibration>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Menu di servizio: ritocca lo scarto e ricordalo.
|
||
function onInputPressed(what)
|
||
if what == SWITCH_LEFT then
|
||
settings.audioDelay = math.max(-1000, settings.audioDelay - 10)
|
||
elseif what == SWITCH_RIGHT then
|
||
settings.audioDelay = math.min(1000, settings.audioDelay + 10)
|
||
end
|
||
singeSetAudioDelay(settings.audioDelay)
|
||
saveSettings()
|
||
end
|
||
----
|
||
|
||
[#singesetgamename]
|
||
==== singeSetGameName
|
||
|
||
[source,text]
|
||
----
|
||
singeSetGameName(title)
|
||
----
|
||
|
||
Imposta il titolo della finestra di Singe. Chiamala una volta all'avvio con il nome del gioco da mostrare; nient'altro usa questo valore. L'argomento deve essere una stringa o un numero.
|
||
|
||
*Parametri:*
|
||
|
||
* `title` -- il titolo della finestra.
|
||
|
||
*Disponibile da:* 1.15 (RDG)
|
||
*Vedi anche:* <<singeversion,singeVersion>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
singeSetGameName("Asteroid Patrol")
|
||
keyboardSetMode(MODE_NORMAL)
|
||
hudFont = fontLoad(DIR .. "fonts/FreeSansBold.ttf", 18)
|
||
----
|
||
|
||
[#singesetpauseflag]
|
||
==== singeSetPauseFlag
|
||
|
||
[source,text]
|
||
----
|
||
singeSetPauseFlag(paused)
|
||
----
|
||
|
||
Imposta dallo script l'indicatore di pausa del motore. `true` mette in pausa il disco se è in riproduzione, ogni video caricato e ogni canale degli effetti sonori, ricordando quali erano in corso; `false` riprende esattamente quelli. A differenza del tasto di pausa, questa chiamata non congela lo script: le callback continuano a scattare, così lo script può disegnare il suo menu di pausa e decidere quando azzerare l'indicatore. I giochi che gestiscono una pausa propria dovrebbero anche prendersi il tasto di pausa con `singeSetPauseKeyEnabled(false)`, perché il tasto del motore commuta lo stesso indicatore e altrimenti entrerebbe in conflitto con lo script. Un argomento non booleano termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `paused` -- booleano.
|
||
|
||
*Disponibile da:* 1.x (RDG)
|
||
*Vedi anche:* <<singegetpauseflag,singeGetPauseFlag>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
singeSetPauseKeyEnabled(false)
|
||
|
||
function onInputPressed(what)
|
||
if what == SWITCH_PAUSE then
|
||
menuOpen = not menuOpen
|
||
singeSetPauseFlag(menuOpen)
|
||
elseif menuOpen and what == SWITCH_QUIT then
|
||
singeQuit()
|
||
end
|
||
end
|
||
----
|
||
|
||
[#singesetpausekeyenabled]
|
||
==== singeSetPauseKeyEnabled
|
||
|
||
[source,text]
|
||
----
|
||
singeSetPauseKeyEnabled(enabled)
|
||
----
|
||
|
||
Sceglie a chi appartiene il tasto di pausa assegnato a `INPUT_PAUSE` in `controls.cfg`. Quando è abilitato (il valore predefinito, ripristinato a ogni ricaricamento) appartiene al motore: premere il tasto congela del tutto il gioco (vedi <<pausing,La pausa>>), e in `MODE_NORMAL` lo script non riceve mai `SWITCH_PAUSE`. Quando è disabilitato il motore ignora il tasto e `SWITCH_PAUSE` arriva a `onInputPressed` e `onInputReleased` come qualsiasi altro interruttore, così il gioco può gestire una pausa propria con `singeSetPauseFlag`. Un argomento non booleano termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<singesetpauseflag,singeSetPauseFlag>>, <<singegetpauseflag,singeGetPauseFlag>>, <<singedisablepausekey,singeDisablePauseKey>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il motore può mettere in pausa durante il gioco, ma non mentre il giocatore inserisce le iniziali.
|
||
function beginInitials()
|
||
singeSetPauseKeyEnabled(false)
|
||
keyboardSetMode(MODE_FULL)
|
||
end
|
||
|
||
function endInitials()
|
||
keyboardSetMode(MODE_NORMAL)
|
||
singeSetPauseKeyEnabled(true)
|
||
end
|
||
----
|
||
|
||
[#singesetquitkeyenabled]
|
||
==== singeSetQuitKeyEnabled
|
||
|
||
[source,text]
|
||
----
|
||
singeSetQuitKeyEnabled(enabled)
|
||
----
|
||
|
||
Estensione di Hypseus. Sceglie se il motore esce con l'interruttore assegnato a `INPUT_QUIT` in `controls.cfg`, dalla tastiera o da un controller. Quando è abilitato (il valore predefinito, ripristinato a ogni ricaricamento) premerlo termina il gioco. Quando è disabilitato il motore lo ignora, così un gioco può prima chiedere conferma o salvare; lo script riceve `SWITCH_QUIT` tramite `onInputPressed` e `onInputReleased` in entrambi i casi, e `singeQuit` termina comunque il gioco. Il pulsante di chiusura della finestra esce sempre. `Framework.singe` definisce `keyboardCatchQuit(catch)`, il nome che Hypseus Singe dà alla stessa impostazione con il senso opposto. Un argomento non booleano termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<keyboardcatchquit,keyboardCatchQuit>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singequit,singeQuit>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Chiedi conferma prima di lasciare una partita in corso.
|
||
singeSetQuitKeyEnabled(false)
|
||
|
||
function onInputPressed(what)
|
||
if what == SWITCH_QUIT then
|
||
if confirmQuit() then
|
||
singeQuit()
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
[#singeversion]
|
||
==== singeVersion
|
||
|
||
[source,text]
|
||
----
|
||
version = singeVersion()
|
||
----
|
||
|
||
Restituisce la versione del motore come numero, la maggiore più la minore come frazione, quindi `3.00` torna come `3`. Confrontala con `<` e `>=` per rifiutare un motore troppo vecchio o per scegliere un ramo in base alle funzionalità; `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR` e `SINGE_VERSION_STRING` sono impostate anche come globali per gli script che vogliono le singole parti o il testo.
|
||
|
||
*Restituisce:* numero.
|
||
|
||
*Disponibile da:* 1.x (RDG)
|
||
*Vedi anche:* <<singesetgamename,singeSetGameName>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
if singeVersion() < 3 then
|
||
error("This game needs Singe 3.00 or newer, found " .. SINGE_VERSION_STRING)
|
||
end
|
||
----
|
||
|
||
[#singewantscrosshairs]
|
||
==== singeWantsCrosshairs
|
||
|
||
[source,text]
|
||
----
|
||
wanted = singeWantsCrosshairs()
|
||
----
|
||
|
||
Restituisce `true` a meno che Singe non sia stato avviato con `--nocrosshair`. Disegna il mirino software solo quando restituisce `true`; i giocatori con pistole ottiche che mettono a schermo un proprio mirino disattivano quello software con quell'opzione. Il valore non cambia durante un'esecuzione, quindi leggilo una volta sola.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<mousegetposition,mouseGetPosition>>, <<onmousemoved,onMouseMoved>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
drawCrosshair = singeWantsCrosshairs()
|
||
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
drawTargets()
|
||
if drawCrosshair then
|
||
spriteDraw(reticle, cursorX, cursorY, true)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#soft]
|
||
=== Corpo morbido
|
||
|
||
Stoffa, corpi a pressione e corde: mesh i cui vertici vengono simulati come particelle (vedi <<physics,Fisica>>, Corpi morbidi). Un corpo morbido si indica tramite il suo nodo, e un nodo che non ne ha uno termina lo script in ogni chiamata dopo `softNew`. Le posizioni sono in unità del mondo, la massa in chilogrammi, e i valori di rigidità vanno da `0` a `1`. Il motore riscrive la mesh del nodo a partire dalle particelle dopo ogni passo, e la simulazione finisce da sola quando il nodo viene eliminato o riceve una mesh diversa. Fino a 16 corpi morbidi alla volta, con fino a 32 punti fissi ciascuno.
|
||
|
||
[#softnew]
|
||
==== softNew
|
||
|
||
[source,text]
|
||
----
|
||
softNew(node, SOFT_CLOTH)
|
||
softNew(node, SOFT_BODY)
|
||
softNew(node, SOFT_ROPE, x, y, z, segments, radius)
|
||
----
|
||
|
||
Per `SOFT_CLOTH` e `SOFT_BODY` la mesh del nodo, di tre vertici o più, diventa le particelle: i vertici nella stessa posizione (entro `0.0001` unità) vengono saldati in uno solo, collocati dove la trasformazione del nodo li mette nel mondo, e i triangoli danno i vincoli di stiramento, taglio e flessione. La stoffa collide su entrambe le facce ed è ciò che serve a un telo aperto; un corpo è una mesh chiusa che `softSetPressure` può gonfiare. Per `SOFT_ROPE` il nodo non ha bisogno di una mesh: `segments + 1` particelle corrono in linea dalla posizione del nodo nel mondo al punto del mondo indicato, il nodo riceve attorno a esse una nuova mesh a tubo a sei lati di quel raggio (mantenendo il materiale che aveva), e le particelle collidono come sfere di quel raggio, mentre le particelle della stoffa e dei corpi sono sfere di raggio `0.03`. Ogni tipo parte con stiramento `0.9`, flessione `0.2`, massa di `1` chilogrammo, smorzamento `0.1` e pressione `0`. Un corpo morbido già presente sul nodo viene rimosso prima che venga creato quello nuovo, quindi una chiamata che fallisce lascia il nodo senza; la mesh a tubo di una corda se ne va con esso, ed è per questo che il nodo di una corda non può diventare stoffa o un corpo (la chiamata fallisce per mancanza di una mesh). Termina lo script quando la fisica non è disponibile, il tipo è sconosciuto, il numero di argomenti non si addice al tipo, la mesh manca, una corda non ha segmenti o non ha raggio, o esistono già 16 corpi morbidi.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo che porta la mesh (stoffa, corpo), o il punto di partenza della corda.
|
||
* `x`, `y`, `z` -- l'estremità lontana della corda, in unità del mondo.
|
||
* `segments` -- quanti segmenti ha la corda, `1` o più; una dozzina basta e avanza.
|
||
* `radius` -- il raggio della corda, in unità del mondo; anche la dimensione con cui collidono le sue particelle.
|
||
|
||
*Note:* Una mesh condivisa da più nodi viene riscritta per tutti, quindi dai a un corpo morbido una mesh tutta sua. Le mesh con skin non possono essere morbide.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<softpin,softPin>>, <<softsetstiffness,softSetStiffness>>, <<softsetpressure,softSetPressure>>, <<meshplane,meshPlane>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una bandiera appesa all'asta: una griglia verticale, fissata lungo il bordo sinistro.
|
||
local flag = nodeNew()
|
||
nodeSetMesh(flag, meshPlane(1.6, 1.0, 16, 10), cloth)
|
||
nodeSetPosition(flag, -4.16, 2.4, 0)
|
||
nodeSetRotation(flag, 90, 0, 0)
|
||
softNew(flag, SOFT_CLOTH)
|
||
softSetStiffness(flag, 0.95, 0.1)
|
||
softSetMass(flag, 0.5)
|
||
for _, y in ipairs({ 1.9, 2.15, 2.4, 2.65, 2.9 }) do
|
||
softPin(flag, -4.96, y, 0) -- Lungo l'asta
|
||
end
|
||
----
|
||
|
||
[#softdelete]
|
||
==== softDelete
|
||
|
||
[source,text]
|
||
----
|
||
softDelete(node)
|
||
----
|
||
|
||
Termina la simulazione e ne libera il corpo. Il nodo mantiene la mesh nella forma in cui è stata scritta l'ultima volta, quindi un telo caduto resta drappeggiato sulla cassa e una corda mantiene il suo tubo; la forma originale della mesh non viene ripristinata. Un nodo senza un corpo morbido termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<softnew,softNew>>, <<nodedelete,nodeDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Quando il telo si è adagiato sulla statua, congelalo e smetti di pagarne il costo.
|
||
function onOverlayUpdate()
|
||
if covering then
|
||
settleFrames = settleFrames + 1
|
||
if settleFrames > 240 then
|
||
softDelete(sheet) -- Il drappeggio resta com'è
|
||
covering = false
|
||
end
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#softpin]
|
||
==== softPin
|
||
|
||
[source,text]
|
||
----
|
||
softPin(node, x, y, z)
|
||
softPin(node, x, y, z, otherNode)
|
||
----
|
||
|
||
Trova la particella più vicina al punto del mondo, ovunque si trovino le particelle in quel momento, e la fissa. Da sola la particella viene tenuta dov'è; con `otherNode` viene spostata nella posizione nel mondo di quel nodo prima di ogni passo, così una corda pende da una gru che oscilla e un mantello segue le spalle. Il punto non deve per forza stare sulla mesh, perché vince la particella più vicina, e fissare di nuovo la stessa particella cambia solo ciò che segue. Oltre i 32 punti fissi la chiamata non fa nulla.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo del corpo morbido.
|
||
* `x`, `y`, `z` -- un punto del mondo; viene fissata la particella più vicina.
|
||
* `otherNode` -- un nodo che la particella seguirà d'ora in poi.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<softunpin,softUnpin>>, <<softnew,softNew>>, <<nodegetworldposition,nodeGetWorldPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una corda dal gancio della gru verso l'esterno e in basso, che oscilla con la gru.
|
||
local crane = nodeNew()
|
||
nodeSetPosition(crane, 4, 4, -1)
|
||
local rope = nodeNew()
|
||
nodeSetPosition(rope, 4, 4, -1)
|
||
softNew(rope, SOFT_ROPE, 6.5, 4, -1, 16, 0.04)
|
||
nodeSetMaterial(rope, hemp)
|
||
softSetMass(rope, 1.5)
|
||
softPin(rope, 4, 4, -1, crane) -- L'estremità superiore segue il gancio
|
||
----
|
||
|
||
[#softunpin]
|
||
==== softUnpin
|
||
|
||
[source,text]
|
||
----
|
||
softUnpin(node, x, y, z)
|
||
----
|
||
|
||
Libera la particella fissata più vicina al punto del mondo. Se quella particella non è fissata non succede nulla. Il corpo viene ricreato attorno alla forma attuale, il che ferma anche di colpo ogni particella, quindi una bandiera lasciata andare comincia a cadere da ferma.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<softpin,softPin>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- L'angolo dello striscione si strappa quando arriva la raffica di vento.
|
||
function onOverlayUpdate()
|
||
gustFrames = gustFrames + 1
|
||
if gustFrames == 200 then
|
||
softUnpin(banner, 3.0, 4.5, 0)
|
||
soundPlay(ripSound)
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#softsetstiffness]
|
||
==== softSetStiffness
|
||
|
||
[source,text]
|
||
----
|
||
softSetStiffness(node, stretch, bend)
|
||
----
|
||
|
||
Quanto il materiale resiste allo stiramento (e, per la stoffa, al taglio) e alla piegatura, da `0` a `1`; i valori fuori intervallo vengono limitati. `1` è rigido, `0` il più lasco che il risolutore consente. Predefiniti `0.9` e `0.2`. Il corpo viene ricreato attorno alla forma attuale: le lunghezze a riposo diventano quelle di questo momento, e ogni particella si ferma, quindi prepara il materiale prima che la stoffa cominci a muoversi. Una corda ha solo lo stiramento.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo del corpo morbido.
|
||
* `stretch` -- resistenza allo stiramento, da `0` a `1`; predefinito `0.9`.
|
||
* `bend` -- resistenza alla piegatura, da `0` a `1`; predefinito `0.2`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<softsetmass,softSetMass>>, <<softsetdamping,softSetDamping>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Seta, tela e un foglio di gomma dalla stessa griglia.
|
||
local silk = clothNode(silkMaterial)
|
||
softNew(silk, SOFT_CLOTH)
|
||
softSetStiffness(silk, 0.7, 0.02)
|
||
local canvas = clothNode(canvasMaterial)
|
||
softNew(canvas, SOFT_CLOTH)
|
||
softSetStiffness(canvas, 1.0, 0.4)
|
||
local rubber = clothNode(rubberMaterial)
|
||
softNew(rubber, SOFT_CLOTH)
|
||
softSetStiffness(rubber, 0.3, 0.1)
|
||
----
|
||
|
||
[#softsetpressure]
|
||
==== softSetPressure
|
||
|
||
[source,text]
|
||
----
|
||
softSetPressure(node, pressure)
|
||
----
|
||
|
||
Gonfia un `SOFT_BODY`: `0` (predefinito) è un sacco sgonfio e poche unità un palloncino ben teso; un valore negativo diventa `0`. Cambia il corpo attivo senza ricrearlo e lo risveglia, così un palloncino può essere gonfiato o sgonfiato mentre rimbalza. La stoffa e la corda non hanno un volume da gonfiare.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<softnew,softNew>>, <<softsetstiffness,softSetStiffness>>, <<meshsphere,meshSphere>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
softNew(balloon, SOFT_BODY) -- Una meshSphere di 0.5
|
||
softSetMass(balloon, 0.6)
|
||
softSetPressure(balloon, 8) -- Un palloncino ben teso
|
||
|
||
function onOverlayUpdate()
|
||
if punctured then
|
||
pressure = math.max(pressure - 0.1, 0)
|
||
softSetPressure(balloon, pressure) -- Si sgonfia sibilando in un paio di secondi
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#softsetmass]
|
||
==== softSetMass
|
||
|
||
[source,text]
|
||
----
|
||
softSetMass(node, kilograms)
|
||
----
|
||
|
||
La massa dell'intero corpo, ripartita in parti uguali tra le sue particelle; predefinita `1`, mantenuta a `0.001` o più. Le particelle fissate non pesano nulla in ogni caso. Il corpo viene ricreato attorno alla forma attuale e ogni particella si ferma, quindi impostala all'inizio.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<softsetstiffness,softSetStiffness>>, <<softsetdamping,softSetDamping>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un telo fissato per i quattro angoli raccoglie una cassa che gli cade sopra.
|
||
local sheet = nodeNew()
|
||
nodeSetMesh(sheet, meshPlane(3, 3, 20, 20), linen)
|
||
nodeSetPosition(sheet, 0, 1.6, 0)
|
||
softNew(sheet, SOFT_CLOTH)
|
||
softSetStiffness(sheet, 1.0, 0.05)
|
||
softSetMass(sheet, 4) -- Abbastanza pesante da non svolazzare
|
||
for _, c in ipairs({ { -1.5, -1.5 }, { 1.5, -1.5 }, { -1.5, 1.5 }, { 1.5, 1.5 } }) do
|
||
softPin(sheet, c[1], 1.6, c[2])
|
||
end
|
||
----
|
||
|
||
[#softsetdamping]
|
||
==== softSetDamping
|
||
|
||
[source,text]
|
||
----
|
||
softSetDamping(node, damping)
|
||
----
|
||
|
||
Quanto in fretta si spegne il moto delle particelle, da `0` in su; predefinito `0.1`, e un valore negativo diventa `0`. Valori più alti calmano una bandiera che non smette di ondeggiare o una corda che oscilla all'infinito. Il corpo viene ricreato attorno alla forma attuale e ogni particella si ferma.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<softsetmass,softSetMass>>, <<softsetstiffness,softSetStiffness>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una tenda nell'aria ferma si calma in fretta; la stessa tenda vicino alla finestra aperta continua a muoversi.
|
||
local curtain = nodeNew()
|
||
nodeSetMesh(curtain, meshPlane(1.2, 2.4, 12, 24), velvet)
|
||
nodeSetPosition(curtain, 2, 2.4, -3)
|
||
nodeSetRotation(curtain, 90, 0, 0)
|
||
softNew(curtain, SOFT_CLOTH)
|
||
softSetDamping(curtain, windowOpen and 0.05 or 0.4)
|
||
for x = -0.6, 0.6, 0.1 do
|
||
softPin(curtain, 2 + x, 3.6, -3)
|
||
end
|
||
----
|
||
|
||
[#sound]
|
||
=== Suono
|
||
|
||
Gli effetti sonori sono clip audio (il WAV è la scelta sicura) caricate in memoria e mixate tramite SDL_mixer, separate dall'audio del disco e dall'audio delle risorse `video*`. `soundLoad` restituisce un handle del suono che identifica i dati della clip; `soundPlay` avvia quella clip su uno dei sedici canali del mixer e restituisce il numero del canale, da `0` a `15`, che è ciò che accettano `soundPause`, `soundResume`, `soundStop`, `soundIsPlaying` e ogni chiamata `soundSet*`. Una clip può suonare su più canali alla volta, e il numero di un canale viene riusato appena il canale è libero, quindi tieni separati i due tipi di numero. Un volume generale degli effetti (`soundSetVolume`, da `0` a `63`) scala ogni canale. Un canale in riproduzione può anche stare nella scena 3D, seguendo un nodo o fisso in un punto in unità del mondo, dove viene distribuito tra sinistra e destra e attenuato in base alla sua distanza dall'ascoltatore (vedi <<scenes3d,Scene 3D>>); per i giochi 2D `soundSetPan` distribuisce un canale tra sinistra e destra senza una scena.
|
||
|
||
[#soundfullstop]
|
||
==== soundFullStop
|
||
|
||
[source,text]
|
||
----
|
||
soundFullStop()
|
||
----
|
||
|
||
Ferma tutti i canali degli effetti in una volta, compresi quelli in pausa, e li libera per il riuso. Nessuna clip viene scaricata; gli handle di `soundLoad` restano validi. Ogni canale che era in riproduzione o in pausa lo segnala tramite `onSoundCompleted`, esattamente come dopo `soundStop`. Chiamala a un cambio di scena o prima di una scena d'intermezzo invece di tenere traccia di ogni canale che hai avviato.
|
||
|
||
*Disponibile da:* 1.16
|
||
*Vedi anche:* <<soundstop,soundStop>>, <<onsoundcompleted,onSoundCompleted>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function enterAttractMode()
|
||
soundFullStop()
|
||
ambientChannel = -1
|
||
engineChannel = -1
|
||
discPlay()
|
||
end
|
||
----
|
||
|
||
[#soundgetposition]
|
||
==== soundGetPosition
|
||
|
||
[source,text]
|
||
----
|
||
x, y, z, gain = soundGetPosition(channel)
|
||
----
|
||
|
||
Riporta dove si trova un canale posizionato rispetto all'ascoltatore, come direzione di lunghezza unitaria nel sistema di riferimento dell'ascoltatore (`x` positivo a destra, `y` positivo in alto, `z` positivo dietro l'ascoltatore), e il fattore di volume che gli dà in quel momento la sua distanza, da `0` a `1`. I valori vengono aggiornati una volta per fotogramma dall'ascoltatore e dal nodo o dal punto del canale. Un canale che non è stato collocato nella scena da quando è partito restituisce `0, 0, 0, 1`. Pensata per indicatori in stile radar e per controllare un mix da uno script di test; un canale fuori dall'intervallo da `0` a `15` termina lo script.
|
||
|
||
*Restituisce:* tre numeri per la direzione e uno per il guadagno dovuto alla distanza.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<soundsetrange,soundSetRange>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Punta una freccia dell'HUD verso il nemico udibile più vicino.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
local x, y, z, gain = soundGetPosition(growlChannel)
|
||
if gain > 0 then
|
||
local angle = math.atan(x, -z)
|
||
drawThreatArrow(angle, gain)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#soundgetvolume]
|
||
==== soundGetVolume
|
||
|
||
[source,text]
|
||
----
|
||
volume = soundGetVolume()
|
||
----
|
||
|
||
Restituisce il volume generale degli effetti, da `0` a `63`. All'avvio riflette il volume degli effetti dato sulla riga di comando, quindi un gioco che mostra un'impostazione del volume dovrebbe leggerlo invece di presumere il massimo.
|
||
|
||
*Restituisce:* intero, da `0` a `63`.
|
||
|
||
*Disponibile da:* 1.16
|
||
*Vedi anche:* <<soundsetvolume,soundSetVolume>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Abbassa gli effetti mentre parla il narratore, poi rimettili come prima.
|
||
function narratorBegin()
|
||
savedVolume = soundGetVolume()
|
||
soundSetVolume(math.floor(savedVolume / 3))
|
||
narratorChannel = soundPlay(narratorClip)
|
||
end
|
||
----
|
||
|
||
[#soundisplaying]
|
||
==== soundIsPlaying
|
||
|
||
[source,text]
|
||
----
|
||
playing = soundIsPlaying(channel)
|
||
----
|
||
|
||
Verifica se un canale è in riproduzione in questo momento. Un canale in pausa riporta `false`, come un canale la cui clip è finita. Un canale fuori dall'intervallo da `0` a `15` termina lo script; un canale che si è semplicemente zittito no.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 1.16 (RDG)
|
||
*Vedi anche:* <<soundplay,soundPlay>>, <<soundpause,soundPause>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Non far ripartire l'allarme mentre sta ancora suonando.
|
||
function raiseAlarm()
|
||
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
|
||
alarmChannel = soundPlay(alarmClip)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#soundload]
|
||
==== soundLoad
|
||
|
||
[source,text]
|
||
----
|
||
id = soundLoad(filename)
|
||
----
|
||
|
||
Carica una clip audio in memoria e ne restituisce l'handle. Il percorso passa per il file system virtuale del gioco, quindi funziona allo stesso modo da una cartella con i file sciolti e da un gioco impacchettato. Carica le clip una volta all'avvio, non dentro una callback che scatta a ogni fotogramma. Un file che non può essere aperto o decodificato termina lo script.
|
||
|
||
*Restituisce:* handle intero del suono. È un handle di dati, non un numero di canale.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<soundplay,soundPlay>>, <<soundunload,soundUnload>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Handle delle clip, caricate una volta e scaricate in onShutdown.
|
||
shotClip = soundLoad(DIR .. "sounds/shot.wav")
|
||
missClip = soundLoad(DIR .. "sounds/miss.wav")
|
||
coinClip = soundLoad(DIR .. "sounds/coin.wav")
|
||
ambientClip = soundLoad(DIR .. "sounds/wind.wav")
|
||
----
|
||
|
||
[#soundloaddata]
|
||
==== soundLoadData
|
||
|
||
[source,text]
|
||
----
|
||
id = soundLoadData(data)
|
||
----
|
||
|
||
Estensione di Hypseus. Costruisce un suono dai byte contenuti in una stringa Lua invece che da un file. Qui viene letto ogni formato che legge <<soundload,`soundLoad`>>, e l'handle restituito si usa con il resto della famiglia `sound` esattamente allo stesso modo.
|
||
|
||
La stringa può contenere byte zero; è la sua lunghezza, non il suo primo zero, a decidere quanto viene letto. I byte vengono decodificati durante la chiamata e la stringa non viene conservata, quindi dopo può essere scartata. Byte che non sono audio, o una stringa vuota, terminano lo script, come fa un file illeggibile in `soundLoad`.
|
||
|
||
Usala per l'audio che un gioco genera, estrae o decifra da sé. L'audio che il file system virtuale può raggiungere per nome -- compreso quello dentro un `.game` impacchettato -- è meglio caricarlo con `soundLoad`, che non tiene l'intero file in una stringa Lua.
|
||
|
||
*Parametri:*
|
||
|
||
* `data` -- stringa di byte.
|
||
|
||
*Restituisce:* numero, l'handle del suono.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<soundload,soundLoad>>, <<spriteloaddata,spriteLoadData>>, <<soundplay,soundPlay>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un effetto sonoro che il gioco estrae dal proprio archivio.
|
||
local bytes = myArchive:extract("beep.wav")
|
||
local beep = soundLoadData(bytes)
|
||
soundPlay(beep)
|
||
----
|
||
|
||
[#soundpause]
|
||
==== soundPause
|
||
|
||
[source,text]
|
||
----
|
||
wasPlaying = soundPause(channel)
|
||
----
|
||
|
||
Mette in pausa un canale dov'è; `soundResume` lo fa continuare dallo stesso punto. Un canale in pausa conta ancora come occupato, quindi `soundPlay` non lo assegna a un'altra clip, e `soundIsPlaying` riporta `false` per esso. Mettere in pausa un canale che non sta suonando non fa nulla. Un canale fuori dall'intervallo da `0` a `15` termina lo script.
|
||
|
||
*Restituisce:* booleano, `true` quando il canale era in riproduzione al momento della chiamata.
|
||
|
||
*Disponibile da:* 1.16 (RDG)
|
||
*Vedi anche:* <<soundresume,soundResume>>, <<soundisplaying,soundIsPlaying>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Sospendi il rumore del motore mentre va il messaggio radio.
|
||
function radioBegin(messageClip)
|
||
engineWasRunning = soundPause(engineChannel)
|
||
radioChannel = soundPlay(messageClip)
|
||
end
|
||
----
|
||
|
||
[#soundplay]
|
||
==== soundPlay
|
||
|
||
[source,text]
|
||
----
|
||
channel = soundPlay(id)
|
||
channel = soundPlay(id, loops)
|
||
channel = soundPlay(id, loops, volume)
|
||
----
|
||
|
||
Avvia una clip caricata sul primo canale libero e restituisce il numero di quel canale. Un canale conta come libero quando non è né in riproduzione né in pausa. Il nuovo canale parte al volume generale (o a `volume` sotto di esso), al centro, senza posizione nella scena e con la portata predefinita, qualunque cosa avesse impostato l'uso precedente del canale. Quando tutti e sedici i canali sono occupati non suona nulla e torna `-1` (`SOUND_ERROR_INVALID`); un gioco che lancia molti suoni sovrapposti dovrebbe controllarlo. Un handle non caricato termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle del suono di `soundLoad`.
|
||
* `loops` -- `0` (il valore predefinito) riproduce la clip una volta, un numero positivo la ripete altrettante volte in più, `-1` la ripete finché non viene fermata. Qualsiasi valore sotto `-1` termina lo script.
|
||
* `volume` -- il volume proprio del canale, da `0` a `63`, sotto quello generale; il predefinito è `63`, la clip alta quanto il volume generale permette. Un valore fuori da quell'intervallo termina lo script. `soundSetChannelVolume` lo cambia mentre la clip suona.
|
||
|
||
*Restituisce:* numero intero del canale, da `0` a `15`, o `-1` quando nessun canale è libero.
|
||
|
||
*Disponibile da:* 1.x; `volume` dalla 3.00.
|
||
*Vedi anche:* <<soundload,soundLoad>>, <<soundstop,soundStop>>, <<soundsetchannelvolume,soundSetChannelVolume>>, <<soundsetnode,soundSetNode>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onInputPressed(what)
|
||
if what == SWITCH_BUTTON1 then
|
||
local channel = soundPlay(shotClip)
|
||
if channel < 0 then
|
||
debugPrint("All sound channels busy; shot dropped")
|
||
end
|
||
elseif what == SWITCH_COIN1 then
|
||
soundPlay(coinClip)
|
||
credits = credits + 1
|
||
end
|
||
end
|
||
----
|
||
|
||
[#soundresume]
|
||
==== soundResume
|
||
|
||
[source,text]
|
||
----
|
||
wasPaused = soundResume(channel)
|
||
----
|
||
|
||
Fa continuare un canale fermato da `soundPause`, dal punto in cui si era interrotto. Riprendere un canale che non è in pausa non fa nulla. Un canale fuori dall'intervallo da `0` a `15` termina lo script.
|
||
|
||
*Restituisce:* booleano, `true` quando il canale era in pausa al momento della chiamata.
|
||
|
||
*Disponibile da:* 1.16 (RDG)
|
||
*Vedi anche:* <<soundpause,soundPause>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onSoundCompleted(channel)
|
||
if channel == radioChannel then
|
||
radioChannel = -1
|
||
if engineWasRunning then
|
||
soundResume(engineChannel)
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
[#soundsetchannelvolume]
|
||
==== soundSetChannelVolume
|
||
|
||
[source,text]
|
||
----
|
||
soundSetChannelVolume(channel, volume)
|
||
----
|
||
|
||
Imposta il volume proprio di un canale, in aggiunta a quello generale di `soundSetVolume`: il canale suona al volume generale moltiplicato per questo, e per la sua attenuazione con la distanza quando è nella scena. Ogni canale parte al massimo quando `soundPlay` lo prende, a meno che a `soundPlay` non sia stato dato un volume, così un passo leggero e uno sparo forte possono condividere un unico volume generale. I valori fuori dall'intervallo da `0` a `63` terminano lo script, e lo stesso fa un canale fuori dall'intervallo da `0` a `15`.
|
||
|
||
*Parametri:*
|
||
|
||
* `channel` -- numero del canale di `soundPlay`.
|
||
* `volume` -- intero, da `0` (muto) a `63` (alto quanto il volume generale).
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<soundplay,soundPlay>>, <<soundsetvolume,soundSetVolume>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un motore che si fa più forte con l'acceleratore: una sola clip in loop, con il volume del suo canale
|
||
-- che segue il pedale a ogni fotogramma.
|
||
engine = soundPlay(engineClip, -1, 20)
|
||
|
||
function onOverlayUpdate()
|
||
soundSetChannelVolume(engine, math.floor(20 + throttle * 43))
|
||
end
|
||
----
|
||
|
||
[#soundsetlistener]
|
||
==== soundSetListener
|
||
|
||
[source,text]
|
||
----
|
||
soundSetListener(node)
|
||
soundSetListener()
|
||
----
|
||
|
||
Sceglie il nodo da cui si sentono i canali posizionati, rivolto nella direzione in cui è rivolto il nodo. Senza argomento, o con `nil`, l'ascoltatore è la telecamera della scena, il che va bene per un gioco in prima persona o a telecamera fissa; un gioco in terza persona di solito preferisce il nodo del giocatore, così i suoni si distribuiscono attorno al personaggio invece che attorno alla telecamera. Un nodo che viene poi eliminato riporta l'ascoltatore alla telecamera. Un handle di nodo che non esiste termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<cameraset,cameraSet>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Telecamera all'inseguimento, ma il mondo si sente dall'auto.
|
||
local car = modelInstance(carModel)
|
||
local chaseCamera = nodeNew(car)
|
||
nodeSetPosition(chaseCamera, 0, 2, 6)
|
||
cameraSet(chaseCamera)
|
||
soundSetListener(car)
|
||
----
|
||
|
||
[#soundsetnode]
|
||
==== soundSetNode
|
||
|
||
[source,text]
|
||
----
|
||
soundSetNode(channel, node)
|
||
soundSetNode(channel)
|
||
----
|
||
|
||
Aggancia un canale in riproduzione a un nodo della scena, così lo si sente da dovunque si trovi il nodo a ogni fotogramma: il canale viene spostato tra sinistra e destra verso la direzione del nodo vista dall'ascoltatore e attenuato in base alla sua distanza secondo `soundSetRange`. Chiamarla senza nodo, o con `nil`, riporta il canale fuori dalla scena al pieno volume generale. Un canale il cui nodo viene eliminato torna da solo alla riproduzione semplice. L'aggancio dura finché il canale non viene fermato o riusato da `soundPlay`. Un canale fuori dall'intervallo da `0` a `15`, o un nodo che non esiste, termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<soundsetposition,soundSetPosition>>, <<soundsetrange,soundSetRange>>, <<soundsetlistener,soundSetListener>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ogni braciere crepita dal punto in cui si trova.
|
||
for i, brazier in ipairs(braziers) do
|
||
local channel = soundPlay(crackleClip, -1)
|
||
if channel >= 0 then
|
||
soundSetNode(channel, brazier)
|
||
soundSetRange(channel, 1.5, 14)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#soundsetpan]
|
||
==== soundSetPan
|
||
|
||
[source,text]
|
||
----
|
||
soundSetPan(channel, pan)
|
||
----
|
||
|
||
Sposta un canale tra i due altoparlanti stereo, per i giochi senza una scena: `-1` è tutto a sinistra, `1` tutto a destra, `0` al centro, e i valori fuori da quell'intervallo vengono limitati. Allontanarsi dal centro abbassa solo l'altoparlante opposto; con `pan = 0.5` l'altoparlante sinistro suona a metà guadagno e il destro al massimo. La chiamata porta il canale fuori dalla scena se `soundSetNode` o `soundSetPosition` ce lo avevano messo. Un canale fuori dall'intervallo da `0` a `15` termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<soundplay,soundPlay>>, <<soundsetnode,soundSetNode>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Sposta tra sinistra e destra un'auto che passa, secondo dove si trova sull'overlay.
|
||
function carPassBy(x)
|
||
local channel = soundPlay(carClip)
|
||
if channel >= 0 then
|
||
soundSetPan(channel, (x / overlayGetWidth()) * 2 - 1)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#soundsetposition]
|
||
==== soundSetPosition
|
||
|
||
[source,text]
|
||
----
|
||
soundSetPosition(channel, x, y, z)
|
||
----
|
||
|
||
Colloca un canale in riproduzione in un punto fisso della scena, in unità del mondo, sostituendo qualsiasi nodo stesse seguendo. Da quel momento viene distribuito tra sinistra e destra e attenuato rispetto all'ascoltatore come un canale agganciato a un nodo. Va bene per un suono che resta fermo, come una cascata o un telefono che squilla; una sorgente in movimento è più semplice con `soundSetNode`. Un canale fuori dall'intervallo da `0` a `15` termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<soundsetnode,soundSetNode>>, <<soundsetrange,soundSetRange>>, <<soundgetposition,soundGetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La cascata non si muove mai, quindi non serve un nodo.
|
||
waterfallChannel = soundPlay(waterfallClip, -1)
|
||
if waterfallChannel >= 0 then
|
||
soundSetPosition(waterfallChannel, 42, 3, -18)
|
||
soundSetRange(waterfallChannel, 6, 80)
|
||
end
|
||
----
|
||
|
||
[#soundsetrange]
|
||
==== soundSetRange
|
||
|
||
[source,text]
|
||
----
|
||
soundSetRange(channel, near, far)
|
||
----
|
||
|
||
Imposta come un canale posizionato si attenua con la distanza dall'ascoltatore. Entro `near` suona a pieno volume; oltre, il suo guadagno è `near` diviso la distanza; nell'ultimo quinto di `far` si attenua anche linearmente, fino al silenzio a `far`. I valori predefiniti sono `1` e `30` unità del mondo, ripristinati ogni volta che `soundPlay` riusa il canale. Un sussurro vuole un `far` piccolo, una cascata uno grande.
|
||
|
||
*Parametri:*
|
||
|
||
* `channel` -- numero del canale di `soundPlay`.
|
||
* `near` -- distanza entro cui il canale è a pieno volume, maggiore di `0`.
|
||
* `far` -- distanza a cui è muto, maggiore di `near`. Qualsiasi altra coppia termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<soundgetposition,soundGetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- I passi di una guardia si sentono solo da vicino; la sirena arriva lontano.
|
||
local steps = soundPlay(footstepClip, -1)
|
||
soundSetNode(steps, guard)
|
||
soundSetRange(steps, 0.5, 8)
|
||
local siren = soundPlay(sirenClip, -1)
|
||
soundSetNode(siren, tower)
|
||
soundSetRange(siren, 10, 200)
|
||
----
|
||
|
||
[#soundsetvolume]
|
||
==== soundSetVolume
|
||
|
||
[source,text]
|
||
----
|
||
soundSetVolume(volume)
|
||
----
|
||
|
||
Imposta il volume generale degli effetti e lo applica subito a ogni canale, in riproduzione o in pausa. Il volume proprio di ciascun canale, da `soundSetChannelVolume`, e, per i canali posizionati, l'attenuazione con la distanza restano applicati sopra di esso. I valori fuori dall'intervallo da `0` a `63` terminano lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `volume` -- intero, da `0` (muto) a `63` (massimo).
|
||
|
||
*Disponibile da:* 1.16
|
||
*Vedi anche:* <<soundgetvolume,soundGetVolume>>, <<soundsetchannelvolume,soundSetChannelVolume>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Cursore del volume nel menu di servizio, comandato dagli interruttori sinistra e destra.
|
||
function onInputPressed(what)
|
||
local volume = soundGetVolume()
|
||
if what == SWITCH_LEFT then
|
||
soundSetVolume(math.max(0, volume - 4))
|
||
elseif what == SWITCH_RIGHT then
|
||
soundSetVolume(math.min(63, volume + 4))
|
||
end
|
||
soundPlay(clickClip)
|
||
end
|
||
----
|
||
|
||
[#soundstop]
|
||
==== soundStop
|
||
|
||
[source,text]
|
||
----
|
||
wasPlaying = soundStop(channel)
|
||
----
|
||
|
||
Ferma subito un canale, che sia in riproduzione o in pausa, e lo libera per il riuso. Il canale lo segnala tramite `onSoundCompleted` proprio come quando la clip finisce da sola, quindi uno script che concatena suoni da quella callback dovrebbe controllare se è stato lui stesso a fermare il canale. Il prossimo `soundPlay` può assegnare di nuovo lo stesso numero, quindi scarta il numero dal lato Lua. Un canale fuori dall'intervallo da `0` a `15` termina lo script.
|
||
|
||
*Restituisce:* booleano, `true` quando il canale era in riproduzione al momento della chiamata. Un canale in pausa restituisce `false` ma viene fermato comunque.
|
||
|
||
*Disponibile da:* 1.x (RDG)
|
||
*Vedi anche:* <<soundfullstop,soundFullStop>>, <<onsoundcompleted,onSoundCompleted>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function playerDied()
|
||
if engineChannel >= 0 then
|
||
soundStop(engineChannel)
|
||
engineChannel = -1
|
||
end
|
||
soundPlay(explosionClip)
|
||
lives = lives - 1
|
||
end
|
||
----
|
||
|
||
[#soundunload]
|
||
==== soundUnload
|
||
|
||
[source,text]
|
||
----
|
||
soundUnload(id)
|
||
----
|
||
|
||
Libera una clip caricata e invalida il suo handle. Il mixer tiene in vita i dati della clip finché ogni canale che la sta ancora riproducendo non si è fermato, quindi lo scaricamento non tronca un canale in riproduzione. Qualsiasi uso successivo dell'handle, compreso un altro `soundUnload`, termina lo script. Il posto abituale per questa chiamata è `onShutdown`.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle del suono di `soundLoad`, non un numero di canale.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<soundload,soundLoad>>, <<onshutdown,onShutdown>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onShutdown()
|
||
soundUnload(shotClip)
|
||
soundUnload(missClip)
|
||
soundUnload(coinClip)
|
||
soundUnload(ambientClip)
|
||
fontUnload(hudFont)
|
||
end
|
||
----
|
||
|
||
[#sprite]
|
||
=== Sprite
|
||
|
||
Gli sprite sono bitmap 2D caricate dal disco con `spriteLoad`, o generate da un testo con `fontToSprite`, e disegnate sull'overlay in coordinate dell'overlay. Ogni chiamata `sprite*` riceve l'handle intero come primo argomento (Singe 2.10 lo metteva per ultimo; vedi <<migrating,Migrazione da Singe 2.10>>), e un handle già scaricato termina lo script con un errore. È accettato qualsiasi formato che SDL_image sappia leggere; una GIF o una WEBP animata con due o più fotogrammi si carica come un'animazione ferma sul fotogramma `0`, non avviata e senza ripetizione, finché `spritePlay` e `spriteLoop` non dicono altrimenti. Il colore `0` viene disegnato trasparente, come in Hypseus: l'indice di palette `0`, o il nero puro, compreso il nero opaco in un'immagine con canale alfa, il cui alfa viene comunque rispettato. Per mantenere il nero opaco di un'immagine, caricala dopo <<drawtransparent,`drawTransparent(true)`>>. Rilascia ogni sprite con `spriteUnload`, di solito da <<onshutdown,onShutdown>>.
|
||
|
||
*Sprite sheet.* Hypseus Singe estende la famiglia con gli sprite sheet: `spriteLoadFrames` divide una singola striscia di fotogrammi uguali in un'animazione non avviata, e `spriteDrawFrame` disegna un fotogramma scelto (a base 1) senza toccare la riproduzione. Per il resto uno sprite sheet è un'animazione qualunque, quindi `spriteSetFrame` (a base 0), `spritePlay`, `spriteDraw`, `nodeSetSprite` ed `emitterSetTexture` lo accettano tutti, e `spriteFrameWidth` e `spriteFrameHeight` riportano la dimensione non scalata di un fotogramma. Le voci contrassegnate «Estensione di Hypseus» esistono perché i giochi scritti per Hypseus Singe girino senza modifiche.
|
||
|
||
[#drawtransparent]
|
||
==== drawTransparent
|
||
|
||
[source,text]
|
||
----
|
||
drawTransparent(boolean)
|
||
----
|
||
|
||
Estensione di Hypseus. Decide se il colore 0 viene disegnato trasparente sugli sprite caricati, e sul testo generato, dopo di essa. `false`, il valore predefinito, rende trasparente il colore 0 come fa Hypseus: l'indice di palette 0, o il nero puro, compreso il nero opaco in un'immagine che ha un canale alfa, così le scritte nere o un contorno nero scompaiono. `true` li disegna solo in base al loro canale alfa, così il nero opaco resta nero. Gli sprite già caricati mantengono l'impostazione con cui sono stati caricati, e ogni script parte da `false`.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Le scritte del logo sono in nero opaco: caricalo solo in base al suo alfa, poi torna
|
||
-- alla solita chiave di colore per la grafica del gioco.
|
||
drawTransparent(true)
|
||
logo = spriteLoad("art/logo.png")
|
||
drawTransparent(false)
|
||
----
|
||
|
||
[#spriteblend]
|
||
==== spriteBlend
|
||
|
||
[source,text]
|
||
----
|
||
spriteBlend(boolean, sprite)
|
||
----
|
||
|
||
Estensione di Hypseus, accettata e ignorata. È un rimedio per quel motore: Hypseus carica gli sprite con la modalità di fusione di SDL disattivata, e questa la riattiva per un singolo sprite. Singe disegna già ogni sprite con il suo canale alfa, quindi la fusione non è mai disattivata in partenza. L'handle dello sprite viene comunque controllato, quindi uno non valido resta un errore. Per sommare la luce di uno sprite o moltiplicare per esso, vedi <<spritesetblend,`spriteSetBlend`>>.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00
|
||
|
||
[#spritedraw]
|
||
==== spriteDraw
|
||
|
||
[source,text]
|
||
----
|
||
spriteDraw(id, x, y)
|
||
spriteDraw(id, x, y, centered)
|
||
spriteDraw(id, x, y, x2, y2)
|
||
spriteDraw(id, x, y, x2, y2, centered)
|
||
----
|
||
|
||
Esegue il blit dello sprite sull'overlay, ruotato e scalato come è in quel momento. Va chiamata da `onOverlayUpdate`. Le prime due forme disegnano alla dimensione riportata da `spriteGetWidth` e `spriteGetHeight`, con `(x, y)` come angolo in alto a sinistra; le ultime due allungano quell'immagine nel rettangolo da `(x, y)` a `(x2, y2)` compreso, sempre con il campionamento al pixel più vicino, qualunque cosa dica `spriteQuality`. Se lo sprite è un'animazione in riproduzione, il disegno prima la fa avanzare del tempo reale trascorso dal disegno precedente (o da `spritePlay`), passando per tutti i fotogrammi che quel tempo copre, e mostra ogni fotogramma per almeno 10 ms anche quando il file chiede meno. Quando l'ultimo fotogramma è passato, la riproduzione torna al fotogramma `0` se `spriteLoop` è impostato, altrimenti resta sull'ultimo fotogramma e si ferma. Le parti che cadono fuori dall'overlay vengono ritagliate.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle dello sprite da `spriteLoad` o `fontToSprite`.
|
||
* `x`, `y` -- punto di ancoraggio in coordinate dell'overlay; le parti frazionarie vengono troncate.
|
||
* `x2`, `y2` -- angolo opposto del rettangolo di destinazione, compreso, per le forme allungate.
|
||
* `centered` -- booleano. Quando è `true`, l'immagine (o il rettangolo allungato) viene spostata in modo che `(x, y)` sia il suo centro invece del suo angolo in alto a sinistra.
|
||
|
||
*Note:* Un'animazione in riproduzione che non viene disegnata per un po' recupera al disegno successivo; chiama `spritePause` se invece deve restare ferma sul suo fotogramma. Usa la forma centrata per gli sprite ruotati, perché la rotazione cambia il riquadro di delimitazione e un ancoraggio in alto a sinistra sembra spostarsi.
|
||
|
||
*Disponibile da:* 1.x. Forme centrata e allungata centrata aggiunte nella 2.10.
|
||
*Vedi anche:* <<spriteload,spriteLoad>>, <<spriterotate,spriteRotate>>, <<spritegetwidth,spriteGetWidth>>, <<videodraw,videoDraw>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La grafica del cabinato a grandezza naturale, l'immagine fissa dell'attract schiacciata nel suo riquadro,
|
||
-- e un cursore ancorato al suo centro.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
spriteDraw(cabinetSprite, CABINET_X, CABINET_Y)
|
||
spriteDraw(attractStill, THUMB_X, THUMB_Y, THUMB_X + THUMB_W - 1, THUMB_Y + THUMB_H - 1)
|
||
spriteDraw(cursorSprite, cursorX, cursorY, true)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spritedrawframe]
|
||
==== spriteDrawFrame
|
||
|
||
[source,text]
|
||
----
|
||
spriteDrawFrame(id, x, y, frame)
|
||
spriteDrawFrame(id, x, y, frame, scale)
|
||
spriteDrawFrame(id, x, y, frame, scaleX, scaleY)
|
||
----
|
||
|
||
Estensione di Hypseus. Esegue il blit di un fotogramma di uno sprite sheet (o di qualsiasi animazione) sull'overlay con `(x, y)` come angolo in alto a sinistra, senza cambiare il fotogramma su cui lo sprite è fermo né il fatto che sia in riproduzione. I fotogrammi sono numerati da `1`, come li conta Hypseus; un fotogramma fuori dall'intervallo da `1` al numero di fotogrammi disegna il fotogramma `1`. Un'immagine fissa disegna se stessa e ignora `frame`. Uno specchiamento impostato da `spriteFlip` si applica. La rotazione e la scala dello sprite impostate da `spriteRotate` e `spriteScale` vengono ignorate: la prima forma disegna il fotogramma alla sua dimensione naturale, le altre moltiplicano la larghezza del fotogramma per `scaleX` (o `scale`) e la sua altezza per `scaleY` (o `scale`) con il campionamento al pixel più vicino. Va chiamata da `onOverlayUpdate`.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle dello sprite da `spriteLoadFrames` o `spriteLoad`.
|
||
* `x`, `y` -- angolo in alto a sinistra in coordinate dell'overlay; le parti frazionarie vengono troncate.
|
||
* `frame` -- numero del fotogramma, da `1` al numero di fotogrammi; le parti frazionarie vengono troncate.
|
||
* `scale`, `scaleX`, `scaleY` -- moltiplicatori della dimensione, `1` per la dimensione naturale.
|
||
|
||
*Note:* `spriteSetFrame` conta i fotogrammi da `0`; `spriteDrawFrame` conta da `1`.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<spriteloadframes,spriteLoadFrames>>, <<spritedraw,spriteDraw>>, <<spritesetframe,spriteSetFrame>>, <<spriteframewidth,spriteFrameWidth>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una striscia di cifre da dieci fotogrammi disegna il punteggio, una cifra per fotogramma.
|
||
digits = spriteLoadFrames(10, DIR .. "digits.png")
|
||
|
||
function drawScore(x, y, score)
|
||
local text = tostring(score)
|
||
for i = 1, #text do
|
||
spriteDrawFrame(digits, x + (i - 1) * spriteFrameWidth(digits), y, tonumber(text:sub(i, i)) + 1)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#spritedrawgrid]
|
||
==== spriteDrawGrid
|
||
|
||
[source,text]
|
||
----
|
||
spriteDrawGrid(id, x, y, sourceX, sourceY, width, height)
|
||
spriteDrawGrid(id, x, y, sourceX, sourceY, width, height, x2, y2)
|
||
----
|
||
|
||
Estensione di Hypseus. Disegna un rettangolo ritagliato da uno sprite con il suo angolo in alto a sinistra in `x`, `y`: alla sua dimensione, oppure, dati `x2` e `y2`, allungato a riempire il riquadro fino a quell'angolo, come <<spritedraw,`spriteDraw`>> allunga uno sprite intero. È l'unica chiamata che accetta un rettangolo di origine: `spriteDraw` prende lo sprite intero e <<spritedrawframe,`spriteDrawFrame`>> prende un fotogramma intero di uno sprite sheet, mentre questa prende un rettangolo qualsiasi. È così che un gioco ritaglia una tessera, una cifra o una lettera da un'unica immagine senza caricarla molte volte.
|
||
|
||
La forma allungata è propria di Singe, ed è la base su cui si costruisce un gioco di corse su strada. Ogni riga dello schermo sotto l'orizzonte è una fetta di strada a una certa distanza, quindi il gioco ritaglia una riga da un'immagine della sezione trasversale della strada e la allunga fino alla larghezza della strada a quella distanza, centrata dove l'hanno portata le curve. Un centinaio abbondante di chiamate per fotogramma disegna l'intera strada, curve e colline comprese, e non viene ricostruito niente. La stessa forma disegna uno sprite in parte nascosto dietro la cresta di una collina: ritagliane la parte superiore e allungala su quanto ne resta visibile. <<spritescale,`spriteScale`>> non può fare né l'una né l'altra cosa, perché ricostruisce l'intera immagine ogni volta che la dimensione cambia.
|
||
|
||
Il rettangolo è nei pixel dello sprite stesso e deve stare al suo interno; un rettangolo che non ci sta, o uno senza larghezza o altezza, termina lo script indicando la dimensione dello sprite, e così fa un `x2` senza `y2`. Un allungamento il cui riquadro non ha larghezza o altezza, perché `x2` è a sinistra di `x` o `y2` sopra `y`, non disegna niente. L'allungamento prende il pixel più vicino invece di fondere, il che mantiene i colori di una riga di un pixel esattamente come sono stati disegnati. Lo sprite da cui si disegna è lo sprite come viene disegnato in quel momento, quindi un <<spriterotate,`spriteRotate`>> o uno `spriteScale` cambia il significato delle coordinate.
|
||
|
||
Con `LEGACY_SPRITE_ARGS` l'handle è il terzo argomento, `spriteDrawGrid(x, y, spr, sourceX, sourceY, width, height)`, che è dove lo mette Hypseus, e `x2`, `y2` seguono in fondo come qui.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; un handle di sprite.
|
||
* `x`, `y` -- numeri; dove cade l'angolo in alto a sinistra del rettangolo, in coordinate dell'overlay.
|
||
* `sourceX`, `sourceY` -- numeri; l'angolo in alto a sinistra del rettangolo nello sprite.
|
||
* `width`, `height` -- numeri; la sua dimensione, almeno 1.
|
||
* `x2`, `y2` -- numeri, facoltativi insieme; l'angolo in basso a destra del riquadro in cui allungarlo, compreso.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe; la forma allungata è nuova nella 3.00 ed è propria di Singe.
|
||
*Vedi anche:* <<spritedraw,spriteDraw>>, <<spritedrawframe,spriteDrawFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un numero disegnato da un'unica striscia di dieci cifre, ognuna 16 per 24.
|
||
local function drawNumber(sheet, x, y, value)
|
||
local text = tostring(value)
|
||
for i = 1, #text do
|
||
local digit = tonumber(text:sub(i, i))
|
||
spriteDrawGrid(sheet, x + (i - 1) * 16, y, digit * 16, 0, 16, 24)
|
||
end
|
||
end
|
||
----
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una strada dritta fino all'orizzonte, una riga dello schermo alla volta. road.png è largo 256 pixel e
|
||
-- alto due righe: una sezione trasversale chiara della strada e una scura, che si alternano lungo la strada
|
||
-- per mostrarla in movimento. La riga y dello schermo è il terreno a EYE * FOCAL / (y - HORIZON) di distanza.
|
||
local EYE = 60
|
||
local FOCAL = 160
|
||
local HORIZON = 110
|
||
local HALF = 100
|
||
|
||
local function drawRoad(road, traveled)
|
||
for y = HORIZON + 1, 239 do
|
||
local z = EYE * FOCAL / (y - HORIZON)
|
||
local half = HALF * FOCAL / z
|
||
local light = math.floor((z + traveled) / 60) % 2
|
||
spriteDrawGrid(road, 160 - half, y, 0, light, 256, 1, 160 + half, y)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#spritedrawrotatedframe]
|
||
==== spriteDrawRotatedFrame
|
||
|
||
[source,text]
|
||
----
|
||
spriteDrawRotatedFrame(id, x, y)
|
||
spriteDrawRotatedFrame(id, x, y, scale)
|
||
spriteDrawRotatedFrame(id, x, y, scaleX, scaleY)
|
||
----
|
||
|
||
Estensione di Hypseus. Disegna il fotogramma che <<spriterotateframe,`spriteRotateFrame`>> ha ruotato per ultimo, *centrato* su `x`, `y`. È la compagna di quella chiamata e il motivo per cui esistono entrambe: <<spritedrawframe,`spriteDrawFrame`>> ignora di proposito la rotazione e la scala di uno sprite, quindi un gioco che vuole ruotare un fotogramma di uno sprite sheet usa invece queste due.
|
||
|
||
Data una scala, il fotogramma ruotato viene allungato di quel fattore mentre viene disegnato -- in aggiunta a qualunque scala con cui sia stato ruotato -- e il risultato resta centrato su `x`, `y`. `scaleX` da solo scala entrambi gli assi.
|
||
|
||
Disegnare prima di qualsiasi `spriteRotateFrame` termina lo script, perché non c'è nessun fotogramma ruotato da disegnare.
|
||
|
||
Con `LEGACY_SPRITE_ARGS` l'handle è l'ultimo argomento, come lo scrive Hypseus.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; un handle di sprite.
|
||
* `x`, `y` -- numeri; il *centro* del fotogramma disegnato, in coordinate dell'overlay.
|
||
* `scale` o `scaleX`, `scaleY` -- numeri facoltativi; un'ulteriore scala al momento del disegno.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<spriterotateframe,spriteRotateFrame>>, <<spritedrawframe,spriteDrawFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- L'ago di una bussola: un fotogramma dello sprite sheet, che punta dove va il giocatore.
|
||
spriteRotateFrame(needle, heading, 1)
|
||
spriteDrawRotatedFrame(needle, 320, 240)
|
||
----
|
||
|
||
[#spriteflip]
|
||
==== spriteFlip
|
||
|
||
[source,text]
|
||
----
|
||
spriteFlip(id, horizontal, vertical)
|
||
----
|
||
|
||
Specchia lo sprite da sinistra a destra, dall'alto in basso, o in entrambi i sensi, finché non viene chiamata di nuovo con `false, false`. Lo specchiamento si applica a ogni disegno dello sprite: `spriteDraw`, ogni fotogramma di `spriteDrawFrame` e il fotogramma ruotato di `spriteDrawRotatedFrame`. Viene applicato dopo la rotazione e la scala di `spriteRotate` e `spriteScale`, quindi uno sprite ruotato viene specchiato come appare sullo schermo, e un fotogramma ruotato viene semplicemente specchiato dall'altra parte invece di essere ruotato di nuovo. L'immagine specchiata viene ricostruita solo quando i flag cambiano; un fotogramma disegnato da `spriteDrawFrame` viene specchiato una volta e conservato finché non viene chiesto un altro fotogramma. `spriteDrawGrid` ignora lo specchiamento, perché il suo rettangolo viene ritagliato dall'immagine non specchiata.
|
||
|
||
A un personaggio che cammina in entrambe le direzioni basta una grafica rivolta da una parte sola: specchia lo sprite quando si gira.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle dello sprite.
|
||
* `horizontal` -- `true` specchia da sinistra a destra.
|
||
* `vertical` -- `true` specchia dall'alto in basso.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<spriterotate,spriteRotate>>, <<spritedrawframe,spriteDrawFrame>>, <<spritedrawrotatedframe,spriteDrawRotatedFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Lo sprite sheet dell'eroe guarda a destra; a sinistra sono gli stessi fotogrammi, specchiati.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
spriteFlip(hero, heroVx < 0, false)
|
||
spriteDrawFrame(hero, heroX, heroY, walkFrame)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spriteframeheight]
|
||
==== spriteFrameHeight
|
||
|
||
[source,text]
|
||
----
|
||
height = spriteFrameHeight(id)
|
||
----
|
||
|
||
Estensione di Hypseus. Restituisce l'altezza di un fotogramma dello sprite prima di qualsiasi scala o rotazione: l'altezza della striscia per uno sprite sheet di `spriteLoadFrames`, l'altezza di un fotogramma per un'animazione e l'altezza dell'immagine per un'immagine fissa (qui Hypseus riporta la superficie disegnata). `spriteGetHeight` riporta invece la dimensione come viene disegnata.
|
||
|
||
*Restituisce:* intero.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<spriteframewidth,spriteFrameWidth>>, <<spritegetheight,spriteGetHeight>>, <<spriteloadframes,spriteLoadFrames>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Centra un fotogramma su un punto.
|
||
function drawCentered(sheet, frame, x, y)
|
||
spriteDrawFrame(sheet, x - spriteFrameWidth(sheet) / 2, y - spriteFrameHeight(sheet) / 2, frame)
|
||
end
|
||
----
|
||
|
||
[#spriteframewidth]
|
||
==== spriteFrameWidth
|
||
|
||
[source,text]
|
||
----
|
||
width = spriteFrameWidth(id)
|
||
----
|
||
|
||
Estensione di Hypseus. Restituisce la larghezza di un fotogramma dello sprite prima di qualsiasi scala o rotazione: la larghezza della striscia divisa per il numero di fotogrammi per uno sprite sheet di `spriteLoadFrames`, la larghezza di un fotogramma per un'animazione e la larghezza dell'immagine per un'immagine fissa (lì Hypseus riporta `0`). `spriteGetWidth` riporta invece la dimensione come viene disegnata.
|
||
|
||
*Restituisce:* intero.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<spriteframeheight,spriteFrameHeight>>, <<spritegetwidth,spriteGetWidth>>, <<spriteloadframes,spriteLoadFrames>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Disponi i fotogrammi di una striscia uno accanto all'altro.
|
||
function drawStrip(sheet, count, x, y)
|
||
for frame = 1, count do
|
||
spriteDrawFrame(sheet, x + (frame - 1) * spriteFrameWidth(sheet), y, frame)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#spritegetframe]
|
||
==== spriteGetFrame
|
||
|
||
[source,text]
|
||
----
|
||
result = spriteGetFrame(id)
|
||
----
|
||
|
||
Restituisce l'indice a base zero del fotogramma che lo sprite mostra in quel momento. Le immagini fisse e le animazioni che non sono avanzate riportano `0`. I fotogrammi avanzano solo dentro `spriteDraw`, quindi il valore cambia tra un disegno e l'altro, non tra chiamate qualsiasi. Usala per sincronizzare un suono o la logica di gioco con un particolare fotogramma di un'animazione.
|
||
|
||
*Restituisce:* indice intero del fotogramma, `0` per un'immagine fissa.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<spritesetframe,spriteSetFrame>>, <<spriteisplaying,spriteIsPlaying>>, <<spritedraw,spriteDraw>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fai partire lo sparo nel fotogramma in cui compare la vampata.
|
||
function onOverlayUpdate()
|
||
spriteDraw(muzzleFlash, gunX, gunY, true)
|
||
local frame = spriteGetFrame(muzzleFlash)
|
||
if frame == FLASH_FRAME and frame ~= lastFlashFrame then
|
||
soundPlay(gunshot)
|
||
end
|
||
lastFlashFrame = frame
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spritegetframes]
|
||
==== spriteGetFrames
|
||
|
||
[source,text]
|
||
----
|
||
count = spriteGetFrames(id)
|
||
----
|
||
|
||
Estensione di Hypseus. Quanti fotogrammi ha uno sprite: il numero di fotogrammi di uno sprite sheet caricato da <<spriteloadframes,`spriteLoadFrames`>> o di una GIF o WEBP animata, e `0` per un'immagine fissa. È il numero fino a cui arriva il numero di fotogramma a base zero di <<spritesetframe,`spriteSetFrame`>>, e uno in più del più grande che accetta.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; un handle di sprite.
|
||
|
||
*Restituisce:* numero; `0` per un'immagine fissa.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<spriteloadframes,spriteLoadFrames>>, <<spritegetframe,spriteGetFrame>>, <<spritesetframe,spriteSetFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Passa al fotogramma successivo, comunque sia fatto lo sprite sheet.
|
||
local frames = spriteGetFrames(walk)
|
||
if frames > 0 then
|
||
spriteSetFrame(walk, (spriteGetFrame(walk) + 1) % frames)
|
||
end
|
||
----
|
||
|
||
[#spritegetheight]
|
||
==== spriteGetHeight
|
||
|
||
[source,text]
|
||
----
|
||
result = spriteGetHeight(id)
|
||
----
|
||
|
||
Restituisce l'altezza dello sprite come verrebbe disegnato in questo momento, in coordinate dell'overlay. La scala e la rotazione applicate con `spriteScale`, `spriteRotate` o `spriteRotateAndScale` sono incluse, quindi uno sprite ruotato riporta l'altezza del suo riquadro di delimitazione ruotato, non dell'immagine di origine. Per un'animazione viene misurato il fotogramma corrente. Le forme allungate di `spriteDraw` non influiscono sul valore.
|
||
|
||
*Restituisce:* altezza intera.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<spritegetwidth,spriteGetWidth>>, <<spritescale,spriteScale>>, <<spriterotate,spriteRotate>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Impila le righe di descrizione già generate, distanziate dell'altezza degli sprite di testo.
|
||
function onOverlayUpdate()
|
||
local y = TEXT_Y
|
||
for _, line in ipairs(descriptionLines) do
|
||
spriteDraw(line, TEXT_X, y)
|
||
y = y + spriteGetHeight(line) + 1
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spritegetwidth]
|
||
==== spriteGetWidth
|
||
|
||
[source,text]
|
||
----
|
||
result = spriteGetWidth(id)
|
||
----
|
||
|
||
Restituisce la larghezza dello sprite come verrebbe disegnato in questo momento, in coordinate dell'overlay, compresa qualsiasi scala e rotazione applicata in quel momento. Per un'animazione viene misurato il fotogramma corrente. Insieme a `spriteGetHeight` è il modo abituale per centrare uno sprite in un riquadro o per misurare il testo generato da `fontToSprite`.
|
||
|
||
*Restituisce:* larghezza intera.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<spritegetheight,spriteGetHeight>>, <<spritescale,spriteScale>>, <<fonttosprite,fontToSprite>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Centra l'immagine del cabinato nel suo pannello, e il titolo nella larghezza dell'overlay.
|
||
function onOverlayUpdate()
|
||
local x = CABINET_X + (CABINET_W - spriteGetWidth(cabinetSprite)) * 0.5
|
||
local y = CABINET_Y + (CABINET_H - spriteGetHeight(cabinetSprite)) * 0.5
|
||
spriteDraw(cabinetSprite, x, y)
|
||
spriteDraw(titleSprite, (overlayGetWidth() - spriteGetWidth(titleSprite)) / 2, 25)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spriteisplaying]
|
||
==== spriteIsPlaying
|
||
|
||
[source,text]
|
||
----
|
||
result = spriteIsPlaying(id)
|
||
----
|
||
|
||
Riporta se l'animazione dello sprite sta avanzando in quel momento. Uno sprite appena caricato riporta `false`; `spritePlay` la attiva, e `spritePause` o la fine di un'animazione senza ripetizione la disattiva. Interrogala per sapere quando un'animazione da riprodurre una volta sola è finita.
|
||
|
||
*Restituisce:* booleano, `true` mentre l'animazione è in riproduzione.
|
||
|
||
*Note:* `spritePlay` e `spritePause` non controllano che ci siano dati di animazione, quindi un'immagine fissa riporta quello che quelle chiamate hanno impostato per ultimo.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<spriteplay,spritePlay>>, <<spritepause,spritePause>>, <<spriteloop,spriteLoop>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Trattieni il menu finché l'animazione del logo, che si riproduce una volta sola, non è finita.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
spriteDraw(logoReveal, overlayGetWidth() / 2, overlayGetHeight() / 2, true)
|
||
if not spriteIsPlaying(logoReveal) then
|
||
drawMenu()
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spriteload]
|
||
==== spriteLoad
|
||
|
||
[source,text]
|
||
----
|
||
result = spriteLoad(filename)
|
||
result = spriteLoad(filename, width, height)
|
||
----
|
||
|
||
Carica un'immagine e restituisce un handle intero per le altre chiamate `sprite*`. Il nome passa per il file system virtuale del motore, quindi il file può stare sciolto sul disco o dentro un gioco impacchettato; viene risolto relativamente alla cartella in cui è stato avviato Singe, quindi anteponi `DIR` per i file distribuiti con il tuo gioco. Viene letto ogni formato in <<formats,Formati video, audio e dei contenitori>>. Una GIF o una WEBP animata con due o più fotogrammi si carica come un'animazione, ferma sul fotogramma `0`, non in riproduzione e senza ripetizione; ogni altro file, compresa una GIF di un solo fotogramma, si carica come immagine fissa. Un'immagine con più di 8 bit per canale, o con pixel in virgola mobile, viene convertita in RGBA a 8 bit al caricamento e trattata come tale da quel momento in poi. I pixel con valore grezzo `0` nel formato dei pixel del file stesso diventano trasparenti. Il caricamento è sincrono, e un file che non si riesce ad aprire o a decodificare termina lo script con il messaggio d'errore del caricatore invece di restituire `nil`.
|
||
|
||
La dimensione facoltativa è per un'immagine vettoriale -- un SVG -- che non ha una dimensione propria che valga qualcosa: l'immagine viene rasterizzata per stare in quel riquadro, mantenendo le sue proporzioni, invece di essere rasterizzata alla dimensione del file e poi ingrandita. Chiedi a <<spritegetwidth,`spriteGetWidth`>> e <<spritegetheight,`spriteGetHeight`>> che dimensione ne è uscita, perché far stare un disegno quadrato in un riquadro largo lascia inutilizzata parte della larghezza. Tutti gli altri formati ignorano la dimensione.
|
||
|
||
*Parametri:*
|
||
|
||
* `filename` -- stringa; l'immagine da caricare.
|
||
* `width` -- numero facoltativo; la larghezza in cui far stare un SVG, `1` o più.
|
||
* `height` -- numero facoltativo; l'altezza in cui far stare un SVG, `1` o più. Vanno dati entrambi insieme.
|
||
|
||
*Restituisce:* handle intero dello sprite.
|
||
|
||
*Note:* Le animazioni non partono da sole. Chiama `spriteLoop(id, true)` e `spritePlay(id)` dopo il caricamento se l'animazione deve girare e ripetersi.
|
||
|
||
*Disponibile da:* 1.x. Supporto per GIF/WEBP animate aggiunto nella 2.10; la dimensione vettoriale nella 3.00.
|
||
*Vedi anche:* <<spriteunload,spriteUnload>>, <<spriteplay,spritePlay>>, <<spriteloop,spriteLoop>>, <<fonttosprite,fontToSprite>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Carica la grafica del menu una volta all'avvio; il ciclo dell'attract è una WEBP animata.
|
||
cabinetSprite = spriteLoad(DIR .. "cabinet.png")
|
||
marqueeSprite = spriteLoad(DIR .. "marquee.png")
|
||
attractLoop = spriteLoad(DIR .. "attract.webp")
|
||
spriteQuality(attractLoop, RENDER_SMOOTH)
|
||
spriteLoop(attractLoop, true)
|
||
spritePlay(attractLoop)
|
||
----
|
||
|
||
[#spriteloaddata]
|
||
==== spriteLoadData
|
||
|
||
[source,text]
|
||
----
|
||
id = spriteLoadData(data)
|
||
----
|
||
|
||
Estensione di Hypseus. Crea uno sprite da un'immagine contenuta in una stringa Lua invece che da un file. Qui viene letto ogni formato che legge <<spriteload,`spriteLoad`>>, GIF e WEBP animate comprese, e un'animazione caricata in questo modo si comporta esattamente come una caricata da un file.
|
||
|
||
La stringa può contenere byte zero; è la sua lunghezza, non il suo primo zero, a decidere quanto viene letto. L'immagine viene decodificata durante la chiamata e la stringa non viene conservata. Byte che non sono un'immagine, o una stringa vuota, terminano lo script, come fa un file illeggibile in `spriteLoad`.
|
||
|
||
Usala per la grafica che un gioco genera, estrae o decifra da sé. La grafica che il file system virtuale può raggiungere per nome -- compresa quella dentro un `.game` impacchettato -- è meglio caricarla con `spriteLoad`, che non tiene l'intero file in una stringa Lua.
|
||
|
||
*Parametri:*
|
||
|
||
* `data` -- stringa di byte.
|
||
|
||
*Restituisce:* numero, l'handle dello sprite.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<spriteload,spriteLoad>>, <<soundloaddata,soundLoadData>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un'immagine che il gioco tiene dentro il proprio file di dati.
|
||
local bytes = myArchive:extract("logo.png")
|
||
local logo = spriteLoadData(bytes)
|
||
spriteDraw(logo, 10, 10)
|
||
----
|
||
|
||
[#spriteloadframes]
|
||
==== spriteLoadFrames
|
||
|
||
[source,text]
|
||
----
|
||
result = spriteLoadFrames(count, filename)
|
||
----
|
||
|
||
Estensione di Hypseus. Carica un'immagine che contiene `count` fotogrammi uguali su una sola riga e restituisce un handle di sprite. La larghezza del fotogramma è la larghezza dell'immagine divisa per `count` (divisione intera; le colonne avanzate a destra vengono scartate) e l'altezza del fotogramma è l'altezza dell'immagine. Lo sprite sheet diventa un'animazione qualunque ferma sul fotogramma `0`, non in riproduzione e senza ripetizione, quindi `spriteDraw` mostra il fotogramma corrente invece dell'intera striscia (come la disegna Hypseus), e `spriteDrawFrame` sceglie direttamente qualsiasi fotogramma. Il nome passa per il file system virtuale come in `spriteLoad`, e i pixel con valore grezzo `0` diventano trasparenti in ogni fotogramma. Un numero fuori dall'intervallo da `2` a `512`, un'immagine più stretta di `count` pixel o un file che non si riesce a leggere terminano lo script con un errore.
|
||
|
||
*Parametri:*
|
||
|
||
* `count` -- numero di fotogrammi nella larghezza dell'immagine, da `2` a `512`.
|
||
* `filename` -- l'immagine della striscia.
|
||
|
||
*Restituisce:* handle intero dello sprite.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<spritedrawframe,spriteDrawFrame>>, <<spriteframewidth,spriteFrameWidth>>, <<spriteload,spriteLoad>>, <<spriteunload,spriteUnload>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una striscia dell'auto da cinque fotogrammi; il fotogramma 3 è l'auto rivolta dritta in avanti.
|
||
carSprite = spriteLoadFrames(5, DIR .. "car.png")
|
||
carWidth = spriteFrameWidth(carSprite)
|
||
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
spriteDrawFrame(carSprite, (overlayGetWidth() - carWidth) / 2, CAR_Y, carFrame)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spriteloop]
|
||
==== spriteLoop
|
||
|
||
[source,text]
|
||
----
|
||
spriteLoop(id, loop)
|
||
----
|
||
|
||
Imposta se l'animazione riparte dal fotogramma `0` dopo l'ultimo fotogramma o si ferma lì. I nuovi sprite non si ripetono. L'impostazione viene consultata ogni volta che `spriteDraw` supera l'ultimo fotogramma, quindi si può cambiare mentre l'animazione gira. Impostarla su uno sprite che si è già fermato non fa ripartire la riproduzione; per quello chiama `spritePlay`. L'argomento deve essere un booleano Lua.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle dello sprite.
|
||
* `loop` -- booleano. `true` torna al fotogramma `0`; `false` (il valore predefinito) si ferma sull'ultimo fotogramma e `spriteIsPlaying` comincia a restituire `false`.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<spriteplay,spritePlay>>, <<spriteisplaying,spriteIsPlaying>>, <<spritesetframe,spriteSetFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La torcia tremola per sempre; l'esplosione si riproduce una volta per colpo.
|
||
torchFlame = spriteLoad(DIR .. "torch.gif")
|
||
explosion = spriteLoad(DIR .. "explosion.gif")
|
||
spriteLoop(torchFlame, true)
|
||
spriteLoop(explosion, false)
|
||
spritePlay(torchFlame)
|
||
----
|
||
|
||
[#spritenew]
|
||
==== spriteNew
|
||
|
||
[source,text]
|
||
----
|
||
id = spriteNew(width, height)
|
||
----
|
||
|
||
Crea uno sprite vuoto, con ogni pixel trasparente, che <<spritesetpixels,`spriteSetPixels`>> riempirà: un'immagine che lo script crea da sé invece di caricarla. Ogni lato va da 1 a 4096 pixel; qualsiasi altro valore termina lo script. Lo sprite è un'immagine fissa e fa tutto quello che fa qualsiasi altra immagine fissa: si disegna, ruota, si scala, si allunga e si colora, e può essere la texture di un materiale.
|
||
|
||
*Parametri:*
|
||
|
||
* `width`, `height` -- numeri; la sua dimensione in pixel.
|
||
|
||
*Restituisce:* numero, l'handle dello sprite.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<spritesetpixels,spriteSetPixels>>, <<spriteloaddata,spriteLoadData>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
local minimap = spriteNew(64, 64)
|
||
----
|
||
|
||
[#spritepause]
|
||
==== spritePause
|
||
|
||
[source,text]
|
||
----
|
||
spritePause(id)
|
||
----
|
||
|
||
Ferma l'animazione sul fotogramma corrente. Il fotogramma continua a essere disegnato da `spriteDraw`, e il tempo passato in pausa non viene contato quando `spritePlay` riprende, quindi il fotogramma non salta in avanti. Mettere in pausa uno sprite che non è in riproduzione non cambia niente.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<spriteplay,spritePlay>>, <<spriteisplaying,spriteIsPlaying>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Blocca il nemico a metà passo mentre si riproduce il lampo del colpo.
|
||
function onOverlayUpdate()
|
||
if playerHit and spriteIsPlaying(enemyWalk) then
|
||
spritePause(enemyWalk)
|
||
end
|
||
spriteDraw(enemyWalk, enemyX, enemyY, true)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spriteplay]
|
||
==== spritePlay
|
||
|
||
[source,text]
|
||
----
|
||
spritePlay(id)
|
||
----
|
||
|
||
Avvia o riprende l'animazione dal fotogramma corrente, cronometrando il primo passo dal momento della chiamata. Uno sprite già in riproduzione viene lasciato com'è, quindi il timer dei fotogrammi non si azzera. Un nuovo sprite va avviato con questa chiamata; il solo caricamento non lo anima.
|
||
|
||
*Note:* Un'animazione senza ripetizione che ha raggiunto l'ultimo fotogramma resta su quel fotogramma quando viene riprodotta di nuovo. Per rivederla da capo, chiama prima `spriteSetFrame(id, 0)`.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<spritepause,spritePause>>, <<spritesetframe,spriteSetFrame>>, <<spriteloop,spriteLoop>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fai ripartire l'esplosione, che si riproduce una volta sola, nel punto d'impatto.
|
||
function onCollision(a, b, x, y, z, speed)
|
||
if speed > IMPACT_SPEED then
|
||
blastX = x
|
||
blastY = y
|
||
spriteSetFrame(explosion, 0)
|
||
spritePlay(explosion)
|
||
soundPlay(boom)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#spritequality]
|
||
==== spriteQuality
|
||
|
||
[source,text]
|
||
----
|
||
spriteQuality(id, mode)
|
||
----
|
||
|
||
Sceglie il campionamento usato quando lo sprite viene ricostruito per la rotazione o la scala. `RENDER_PIXELATED` mantiene netti i bordi dei pixel, `RENDER_SMOOTH` interpola. I nuovi sprite sono pixelati. L'impostazione conta solo mentre è in vigore una rotazione o una scala diversa da `1.0`; uno sprite non trasformato viene disegnato direttamente dalla sua immagine di origine, e le forme allungate di `spriteDraw` usano sempre il campionamento al pixel più vicino. Cambiare la modalità ricostruisce subito l'immagine trasformata, quindi impostala una volta dopo il caricamento invece che a ogni fotogramma.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle dello sprite.
|
||
* `mode` -- `RENDER_PIXELATED` (`0`) o `RENDER_SMOOTH` (`1`). Qualsiasi numero diverso da zero seleziona la modalità smussata.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<spritescale,spriteScale>>, <<spriterotate,spriteRotate>>, <<spriterotateandscale,spriteRotateAndScale>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La grafica fotografica del marquee va interpolata; la pixel art deve restare nitida.
|
||
marqueeSprite = spriteLoad(DIR .. "marquee.jpg")
|
||
playerSprite = spriteLoad(DIR .. "player.png")
|
||
spriteQuality(marqueeSprite, RENDER_SMOOTH)
|
||
spriteQuality(playerSprite, RENDER_PIXELATED)
|
||
spriteScale(marqueeSprite, 0.5)
|
||
spriteScale(playerSprite, 3)
|
||
----
|
||
|
||
[#spriteresetcolorkey]
|
||
==== spriteResetColorKey
|
||
|
||
[source,text]
|
||
----
|
||
spriteResetColorKey(id, enabled)
|
||
----
|
||
|
||
Estensione di Hypseus. Accettata per compatibilità, non fa niente. In Hypseus il flag riapplica il colore trasparente alle copie prodotte da `spriteRotate` e `spriteScale`, che altrimenti disegnerebbero il loro sfondo opaco; le copie trasformate di Singe portano la trasparenza nel loro canale alfa, quindi non c'è nessuna chiave di colore da riapplicare. Un handle non valido o un argomento non booleano termina lo script, come per ogni altra chiamata degli sprite.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle dello sprite.
|
||
* `enabled` -- booleano, ignorato.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>, <<spriteload,spriteLoad>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Codice di un gioco per Hypseus; innocuo in Singe.
|
||
badge = spriteLoad(DIR .. "badge.png")
|
||
spriteResetColorKey(badge, true)
|
||
----
|
||
|
||
[#spriterotate]
|
||
==== spriteRotate
|
||
|
||
[source,text]
|
||
----
|
||
spriteRotate(id, degrees)
|
||
----
|
||
|
||
Ruota lo sprite in senso orario attorno al suo centro. L'angolo viene ridotto modulo 360 e può essere frazionario o negativo. La scala corrente viene mantenuta. L'immagine ruotata viene ricostruita solo quando l'angolo cambia davvero, quindi ripetere lo stesso angolo a ogni fotogramma non costa niente, mentre un angolo nuovo a ogni fotogramma ricostruisce a ogni fotogramma. La rotazione ingrandisce il riquadro di delimitazione, quindi `spriteGetWidth` e `spriteGetHeight` cambiano e la forma centrata di `spriteDraw` è quella da usare.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<spriterotateandscale,spriteRotateAndScale>>, <<spritedraw,spriteDraw>>, <<spritequality,spriteQuality>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Casse da una scena di fisica 2D, disegnate all'angolo dei loro corpi.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
for _, crate in ipairs(crates) do
|
||
local x, y = nodeGetPosition(crate)
|
||
local _, _, angle = nodeGetRotation(crate)
|
||
spriteRotate(crateSprite, angle)
|
||
spriteDraw(crateSprite, x, y, true)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spriterotateandscale]
|
||
==== spriteRotateAndScale
|
||
|
||
[source,text]
|
||
----
|
||
spriteRotateAndScale(id, degrees, scale)
|
||
spriteRotateAndScale(id, degrees, scaleX, scaleY)
|
||
----
|
||
|
||
Imposta insieme rotazione e scala. Il risultato è lo stesso di `spriteRotate` seguita da `spriteScale`, ma l'immagine trasformata viene ricostruita una volta invece di due, e per niente quando non è cambiato niente. I fattori di scala dovrebbero essere maggiori di `0`; uno negativo viene preso senza il segno, e uno che non lascerebbe nessun pixel, zero compreso, ne lascia invece uno.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle dello sprite.
|
||
* `degrees` -- rotazione in senso orario, ridotta modulo 360.
|
||
* `scale` -- fattore uniforme, `1.0` per la dimensione naturale.
|
||
* `scaleX`, `scaleY` -- fattori orizzontale e verticale separati.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>, <<spritequality,spriteQuality>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una moneta che gira e rimpicciolisce mentre vola verso il contatore del punteggio.
|
||
function onOverlayUpdate()
|
||
if coinFlying then
|
||
coinT = math.min(coinT + 0.02, 1)
|
||
spriteRotateAndScale(coinSprite, coinT * 720, 1 - coinT * 0.75)
|
||
spriteDraw(coinSprite, coinX + (SCORE_X - coinX) * coinT, coinY + (SCORE_Y - coinY) * coinT, true)
|
||
coinFlying = coinT < 1
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spriterotateframe]
|
||
==== spriteRotateFrame
|
||
|
||
[source,text]
|
||
----
|
||
spriteRotateFrame(id, degrees, frame)
|
||
----
|
||
|
||
Estensione di Hypseus. Prende un fotogramma da uno sprite sheet, lo ruota in senso orario di `degrees` e lo scala secondo lo <<spritescale,`spriteScale`>> dello sprite stesso, e conserva il risultato per <<spritedrawrotatedframe,`spriteDrawRotatedFrame`>>. La superficie disegnata dello sprite non viene toccata, quindi <<spritedraw,`spriteDraw`>> e `spriteDrawFrame` continuano a mostrare quello che mostravano.
|
||
|
||
I fotogrammi sono numerati da 1, come li numera `spriteDrawFrame` (e a differenza di `spriteGetFrame` e `spriteSetFrame`, che contano da 0); un fotogramma fuori dallo sprite sheet diventa il fotogramma 1 invece di un errore. Un'immagine fissa ruota l'intera immagine.
|
||
|
||
Ruotare non è gratis: il fotogramma viene ridisegnato pixel per pixel ogni volta, quindi ruotalo quando l'angolo cambia invece che a ogni fotogramma. L'angolo viene preso modulo 360, e <<spritequality,`spriteQuality`>> decide se i bordi ruotati vengono smussati.
|
||
|
||
Con `LEGACY_SPRITE_ARGS` l'handle è l'ultimo argomento, `spriteRotateFrame(degrees, frame, spr)`, come lo scrive Hypseus.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; un handle di sprite.
|
||
* `degrees` -- numero; in senso orario.
|
||
* `frame` -- numero; da 1 al numero di fotogrammi.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<spritedrawrotatedframe,spriteDrawRotatedFrame>>, <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Lo sprite di una nave il cui scafo è il fotogramma 1 e la cui spinta è il fotogramma 2.
|
||
spriteRotateFrame(ship, angle, thrusting and 2 or 1)
|
||
spriteDrawRotatedFrame(ship, shipX, shipY)
|
||
----
|
||
|
||
[#spritescale]
|
||
==== spriteScale
|
||
|
||
[source,text]
|
||
----
|
||
spriteScale(id, scale)
|
||
spriteScale(id, scaleX, scaleY)
|
||
----
|
||
|
||
Scala lo sprite attorno al suo centro, in modo uniforme o per asse, mantenendo la rotazione corrente. I fattori devono essere maggiori di `0`; `1.0` è la dimensione naturale, e un fattore esattamente `1.0` su entrambi gli assi senza rotazione disegna direttamente l'immagine di origine. L'immagine scalata viene ricostruita solo quando un fattore cambia, usando il campionamento scelto da `spriteQuality`. Preferisci la forma allungata di `spriteDraw` quando la dimensione di destinazione cambia a ogni fotogramma, perché non ricostruisce niente.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle dello sprite.
|
||
* `scale` -- fattore uniforme.
|
||
* `scaleX`, `scaleY` -- fattori orizzontale e verticale separati.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<spriterotateandscale,spriteRotateAndScale>>, <<spritequality,spriteQuality>>, <<spritedraw,spriteDraw>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ingrandisci la voce di menu evidenziata; la ricostruzione avviene solo quando la selezione si sposta.
|
||
function onOverlayUpdate()
|
||
for i, label in ipairs(menuLabels) do
|
||
if i == selected then
|
||
spriteScale(label, 1.25)
|
||
else
|
||
spriteScale(label, 1)
|
||
end
|
||
spriteDraw(label, MENU_X, MENU_Y + i * LINE_HEIGHT, true)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spritesetalpha]
|
||
==== spriteSetAlpha
|
||
|
||
[source,text]
|
||
----
|
||
spriteSetAlpha(id, alpha)
|
||
----
|
||
|
||
Moltiplica l'alfa di ogni pixel per `alpha / 255` a ogni disegno dello sprite, in ogni forma: `spriteDraw`, `spriteDrawFrame`, `spriteDrawGrid` e `spriteDrawRotatedFrame`, ruotato o no. `255`, il valore predefinito, lo disegna così com'è e `0` non lo disegna affatto, così un logo appare e scompare in dissolvenza senza una seconda immagine. I valori fuori intervallo vengono limitati.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; un handle di sprite.
|
||
* `alpha` -- numero; da `0` a `255`.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<spritesetblend,spriteSetBlend>>, <<spritesettint,spriteSetTint>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il titolo appare in dissolvenza in due secondi.
|
||
spriteSetAlpha(title, math.floor(255 * math.min(1, elapsed / 2)))
|
||
spriteDraw(title, 120, 80)
|
||
----
|
||
|
||
[#spritesetblend]
|
||
==== spriteSetBlend
|
||
|
||
[source,text]
|
||
----
|
||
spriteSetBlend(id, SPRITE_ALPHA | SPRITE_ADD | SPRITE_MULTIPLY)
|
||
----
|
||
|
||
Come ogni disegno dello sprite lo stende su ciò che è già sull'overlay. `SPRITE_ALPHA`, il valore predefinito, lo disegna sopra in base al suo alfa. `SPRITE_ADD` somma il suo colore, scalato dal suo alfa, a ciò che c'è già, quindi può solo schiarire: bagliori, luce, scintille e gli shadebob delle vecchie demo, dove una palla disegnata sopra se stessa più e più volte brucia fino al bianco. `SPRITE_MULTIPLY` scurisce ciò che c'è già secondo il suo colore, per ombre e vetri colorati.
|
||
|
||
I pixel sommati e moltiplicati mantengono l'alfa dell'overlay stesso, quindi si vedono solo dove sull'overlay c'è già qualcosa di disegnato: su un overlay trasparente, sopra un video o la scena 3D, non cambiano niente. Disegna prima qualcosa di opaco, oppure usa `emitterSetBlend` con `PARTICLE_ADD`, che somma sopra tutto. Qualsiasi cosa diversa dalle tre costanti termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; un handle di sprite.
|
||
* `blend` -- `SPRITE_ALPHA`, `SPRITE_ADD` o `SPRITE_MULTIPLY`.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<spritesetalpha,spriteSetAlpha>>, <<spritesettint,spriteSetTint>>, <<emittersetblend,emitterSetBlend>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Shadebob: una palla bianca sfumata sommata lungo un percorso, mai cancellata, così la scia arriva al bianco.
|
||
spriteSetBlend(ball, SPRITE_ADD)
|
||
spriteSetAlpha(ball, 40)
|
||
for i = 0, 15 do
|
||
local a = t * 2 + i * 0.4
|
||
spriteDraw(ball, 320 + math.cos(a) * 200, 240 + math.sin(a * 1.3) * 150, true)
|
||
end
|
||
----
|
||
|
||
[#spritesetframe]
|
||
==== spriteSetFrame
|
||
|
||
[source,text]
|
||
----
|
||
spriteSetFrame(id, frame)
|
||
----
|
||
|
||
Fa saltare un'animazione al fotogramma dato e riavvia il timer di ritardo di quel fotogramma, così il fotogramma viene mostrato per tutta la sua durata prima del successivo. Lo stato di riproduzione non cambia: uno sprite in pausa resta in pausa sul nuovo fotogramma e uno in riproduzione continua da lì. La chiamata viene ignorata per le immagini fisse, per i fotogrammi fuori dall'intervallo da `0` all'ultimo fotogramma, e quando lo sprite mostra già quel fotogramma.
|
||
|
||
*Note:* Qui i fotogrammi sono numerati da `0`; `spriteDrawFrame`, l'estensione di Hypseus, li numera da `1`.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<spritegetframe,spriteGetFrame>>, <<spriteplay,spritePlay>>, <<spritedrawframe,spriteDrawFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Mostra la porta nello stato in cui la logica di gioco dice che si trova.
|
||
function onOverlayUpdate()
|
||
if doorOpen then
|
||
spriteSetFrame(doorSprite, DOOR_OPEN_FRAME)
|
||
else
|
||
spriteSetFrame(doorSprite, DOOR_CLOSED_FRAME)
|
||
end
|
||
spriteDraw(doorSprite, DOOR_X, DOOR_Y)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spritesetpixels]
|
||
==== spriteSetPixels
|
||
|
||
[source,text]
|
||
----
|
||
spriteSetPixels(id, pixels)
|
||
spriteSetPixels(id, pixels, palette)
|
||
----
|
||
|
||
Sostituisce ogni pixel di uno sprite fisso con quelli di una stringa, una riga alla volta a partire dall'alto. Senza palette la stringa ha quattro byte per pixel, rosso, verde, blu e alfa. Con una palette ha un byte per pixel, ciascuno il numero di un colore della palette, che è di 256 colori da tre byte (rosso, verde e blu, tutti opachi) o da quattro (con l'alfa). Una stringa della lunghezza sbagliata, una palette della lunghezza sbagliata o uno sprite animato terminano lo script.
|
||
|
||
È il modo in cui uno script disegna un'immagine un pixel alla volta, abbastanza in fretta da farlo a ogni fotogramma: i plasma, i tunnel, il fuoco e l'acqua delle vecchie demo, una minimappa o una heightmap in fase di modifica. Costruire la stringa è compito dello script, quindi tieni l'immagine piccola e allungala quando la disegni: un ciclo Lua riempie un'immagine di 320 per 240 in pochi millisecondi su un desktop. La modalità palette è il vecchio modo di animare spendendo poco: imposta di nuovo gli stessi pixel con la palette ruotata di un colore, e tutto cambia colore senza ricalcolare niente. Uno sprite ruotato o scalato viene ruotato e scalato di nuovo con i suoi nuovi pixel; un materiale che mostra lo sprite non vede il cambiamento finché <<materialsettexture,`materialSetTexture`>> non viene chiamata di nuovo con lo sprite, il che lo carica di nuovo da capo.
|
||
|
||
Anche a un'immagine caricata si possono dare nuovi pixel; ha la dimensione con cui è stata caricata.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; uno sprite fisso.
|
||
* `pixels` -- stringa; `width * height * 4` byte, o `width * height` con una palette.
|
||
* `palette` -- stringa facoltativa; 768 o 1024 byte.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<spritenew,spriteNew>>, <<materialsettexture,materialSetTexture>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fuoco: ogni pixel è la media di quelli sotto di lui, e si raffredda salendo, attraverso una palette che va
|
||
-- dal nero al rosso e al giallo fino al bianco. Allungato sulla parte bassa dello schermo a ogni fotogramma.
|
||
local W = 160
|
||
local H = 60
|
||
local fire = spriteNew(W, H)
|
||
local heat = {}
|
||
local color = {}
|
||
for i = 0, 255 do
|
||
color[#color + 1] = string.char(math.min(255, i * 3), math.max(0, math.min(255, i * 3 - 255)), math.max(0, i * 3 - 510))
|
||
end
|
||
local palette = table.concat(color)
|
||
for i = 0, W * (H + 1) - 1 do
|
||
heat[i] = 0
|
||
end
|
||
|
||
function onOverlayUpdate()
|
||
for x = 0, W - 1 do
|
||
heat[H * W + x] = math.random(0, 255)
|
||
end
|
||
local rows = {}
|
||
for y = 0, H - 1 do
|
||
local row = {}
|
||
for x = 0, W - 1 do
|
||
local below = (y + 1) * W
|
||
local v = (heat[below + (x - 1) % W] + heat[below + x] + heat[below + (x + 1) % W] + heat[y * W + x]) // 4
|
||
heat[y * W + x] = math.max(0, v - 1)
|
||
row[x + 1] = heat[y * W + x]
|
||
end
|
||
rows[y + 1] = string.char(table.unpack(row))
|
||
end
|
||
spriteSetPixels(fire, table.concat(rows), palette)
|
||
overlayClear()
|
||
spriteDraw(fire, 0, 240, 639, 479)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#spritesettint]
|
||
==== spriteSetTint
|
||
|
||
[source,text]
|
||
----
|
||
spriteSetTint(id, r, g, b)
|
||
----
|
||
|
||
Moltiplica il colore di ogni pixel per `r`, `g`, `b` / 255 a ogni disegno dello sprite, in ogni forma. Uno sprite bianco si disegna in qualsiasi colore, e qualsiasi sprite si può scurire o spostare verso un colore, senza una seconda immagine: una palla bianca diventa un arcobaleno di palle, e uno sprite bianco di un pixel allungato con `spriteDraw` diventa un rettangolo pieno di qualsiasi colore. `255, 255, 255`, il valore predefinito, lo disegna così com'è. I valori fuori intervallo vengono limitati.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; un handle di sprite.
|
||
* `r`, `g`, `b` -- numeri; da `0` a `255`.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00
|
||
*Vedi anche:* <<spritesetalpha,spriteSetAlpha>>, <<spritesetblend,spriteSetBlend>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un anello di palle, ognuna del suo colore, da un'unica immagine bianca.
|
||
for i = 1, 12 do
|
||
local hue = i / 12
|
||
spriteSetTint(ball, 128 + 127 * math.sin(hue * 6.28), 128 + 127 * math.sin(hue * 6.28 + 2.1), 128 + 127 * math.sin(hue * 6.28 + 4.2))
|
||
spriteDraw(ball, 320 + math.cos(hue * 6.28) * 150, 240 + math.sin(hue * 6.28) * 150, true)
|
||
end
|
||
spriteSetTint(ball, 255, 255, 255)
|
||
----
|
||
|
||
[#spriteunload]
|
||
==== spriteUnload
|
||
|
||
[source,text]
|
||
----
|
||
spriteUnload(id)
|
||
----
|
||
|
||
Libera lo sprite: l'immagine di origine, la copia trasformata se esiste, e tutti i fotogrammi dell'animazione. Dopo, l'handle non è più valido e qualsiasi altro uso termina lo script con un errore. Gli sprite creati da `fontToSprite` si rilasciano allo stesso modo. Gli sprite ancora caricati quando il gioco finisce vengono liberati dal motore, ma i giochi che creano sprite durante la partita (per esempio testo rigenerato quando cambia il punteggio) devono scaricare quelli vecchi, altrimenti la memoria cresce senza limiti.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<spriteload,spriteLoad>>, <<fonttosprite,fontToSprite>>, <<onshutdown,onShutdown>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Rigenera il punteggio solo quando cambia, rilasciando lo sprite precedente.
|
||
function updateScoreSprite()
|
||
if scoreSprite ~= nil then
|
||
spriteUnload(scoreSprite)
|
||
end
|
||
scoreSprite = fontToSprite("SCORE " .. score)
|
||
end
|
||
|
||
function onShutdown()
|
||
spriteUnload(scoreSprite)
|
||
spriteUnload(cabinetSprite)
|
||
end
|
||
----
|
||
|
||
[#subtitleref]
|
||
=== Sottotitolo
|
||
|
||
Sottotitoli SubRip sopra l'immagine del disco. Un gioco carica un `.srt` con <<srtload,`srtLoad`>>, lo attiva con <<srtenable,`srtEnable`>>, e il motore mostra ogni battuta e la toglie di nuovo man mano che il disco la raggiunge. Vedi <<subtitles,Sottotitoli>> per il formato del file, la tempistica e come cambiarne lo stile.
|
||
|
||
I sottotitoli e <<overlaybanner,`overlayBanner`>> condividono un unico documento distribuito con Singe, `Singe/subtitle.rml`, con lo stile di `Singe/subtitle.rcss`; un gioco che vuole un altro aspetto distribuisce il proprio `subtitle.rml` accanto al suo script, e Singe usa quello (vedi <<subtitles,Sottotitoli>>). Entrambi vengono disegnati sopra l'immagine, sopra l'overlay e sotto le GUI nello spazio dello schermo, e non ricevono input dal mouse.
|
||
|
||
[#srtclear]
|
||
==== srtClear
|
||
|
||
[source,text]
|
||
----
|
||
srtClear()
|
||
----
|
||
|
||
Estensione di Hypseus. Toglie subito quello che c'è sullo schermo -- una battuta o un banner -- e lascia stare le battute caricate e lo stato di attivazione. La battuta che era visibile resta nascosta finché il disco non raggiunge la successiva; non serve ricaricare niente né riattivare niente.
|
||
|
||
*Parametri:* nessuno.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<srtenable,srtEnable>>, <<overlaybanner,overlayBanner>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una scena d'intermezzo finisce in anticipo; togli la battuta ancora visibile.
|
||
srtClear()
|
||
----
|
||
|
||
[#srtenable]
|
||
==== srtEnable
|
||
|
||
[source,text]
|
||
----
|
||
srtEnable(enabled)
|
||
----
|
||
|
||
Estensione di Hypseus. Se le battute caricate vengono mostrate mentre il disco va avanti. Disattivarla toglie con sé qualsiasi battuta visibile; riattivarla mostra la battuta che copre il fotogramma su cui si trova il disco. Uno script parte con i sottotitoli disattivati, quindi un gioco che li carica deve attivarli.
|
||
|
||
Un argomento non booleano termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<srtload,srtLoad>>, <<srtclear,srtClear>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
srtEnable(SETTINGS.subtitles)
|
||
----
|
||
|
||
[#srtload]
|
||
==== srtLoad
|
||
|
||
[source,text]
|
||
----
|
||
loaded = srtLoad(filename)
|
||
----
|
||
|
||
Estensione di Hypseus. Legge un file SubRip tramite il file system virtuale e trasforma le sue marche temporali in fotogrammi del disco alla frequenza dei fotogrammi del disco stesso. Il caricamento sostituisce quello che era stato caricato prima; poi <<srtenable,`srtEnable`>> lo mostra.
|
||
|
||
Risponde `false`, senza aver caricato niente, quando il file non si riesce a leggere, quando non contiene nessuna battuta interpretabile, o quando non c'è un disco con cui sincronizzarlo -- quindi un file malformato è un `false`, non un crash. In ognuno di questi casi viene stampato un avviso che nomina il file. Le battute non interpretabili dentro un file per il resto valido vengono saltate, come le salta Hypseus.
|
||
|
||
Poiché le marche temporali diventano numeri di fotogramma quando il file viene caricato, e il numero di fotogramma è ciò che il motore confronta, una battuta viene ritrovata ovunque venga mandato il disco: una ricerca, un salto o un passo arrivano subito sulla riga giusta.
|
||
|
||
*Parametri:*
|
||
|
||
* `filename` -- stringa; un percorso che il file system virtuale può raggiungere.
|
||
|
||
*Restituisce:* booleano, se è stata caricata qualche battuta.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<srtenable,srtEnable>>, <<srtposition,srtPosition>>, <<subtitles,Sottotitoli>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
if srtLoad("subs/" .. language .. ".srt") then
|
||
srtEnable(true)
|
||
end
|
||
----
|
||
|
||
[#srtloadtrack]
|
||
==== srtLoadTrack
|
||
|
||
[source,text]
|
||
----
|
||
loaded = srtLoadTrack(track)
|
||
----
|
||
|
||
Uguale a <<srtload,`srtLoad`>>, salvo che i sottotitoli vengono da dentro il contenitore del disco stesso invece che da un file `.srt` accanto a esso. Tutto ciò che segue il caricamento è identico: le battute diventano numeri di fotogramma del disco alla frequenza dei fotogrammi del disco, quindi vengono ritrovate dopo qualsiasi salto, e <<srtenable,`srtEnable`>>, <<srtposition,`srtPosition`>> e <<srtclear,`srtClear`>> funzionano allo stesso modo.
|
||
|
||
L'intero file viene percorso una volta per raccogliere la traccia, senza decodificare immagini, quindi un disco lungo richiede un momento -- fallo mentre sullo schermo c'è qualcos'altro invece che tra due fotogrammi di gioco.
|
||
|
||
Una traccia che contiene immagini invece di parole (VobSub o PGS) non si può leggere: risponde `false`, come fa un `.srt` illeggibile, e lascia i sottotitoli cancellati. Lo stesso vale per un numero di traccia che non indica niente, e per un gioco senza disco.
|
||
|
||
*Parametri:*
|
||
|
||
* `track` -- numero; da `0` a `discGetSubtitleTracks() - 1`.
|
||
|
||
*Restituisce:* `true` quando è stata letta almeno una battuta.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<srtload,srtLoad>>, <<discgetsubtitletracks,discGetSubtitleTracks>>, <<srtenable,srtEnable>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- I sottotitoli contenuti nel rip, con un .srt accanto al gioco come riserva.
|
||
if discGetSubtitleTracks() > 0 then
|
||
srtLoadTrack(0)
|
||
else
|
||
srtLoad(DIR .. "subtitles.srt")
|
||
end
|
||
srtEnable(true)
|
||
----
|
||
|
||
[#srtposition]
|
||
==== srtPosition
|
||
|
||
[source,text]
|
||
----
|
||
srtPosition(percent)
|
||
----
|
||
|
||
Estensione di Hypseus. Dove si trova un sottotitolo, come percentuale dall'alto dell'immagine, da 1 a 95. Il valore predefinito è 80. Un numero fuori da quell'intervallo viene ignorato, come lo ignora Hypseus, invece di essere limitato o rifiutato.
|
||
|
||
L'altezza si applica subito alla battuta sullo schermo e a ogni battuta successiva. <<overlaybanner,`overlayBanner`>> ha una sua altezza e non ne è influenzato.
|
||
|
||
*Parametri:*
|
||
|
||
* `percent` -- numero; da 1 a 95.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<srtload,srtLoad>>, <<subtitles,Sottotitoli>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Tieni le righe lontane dal segnapunti lungo il bordo inferiore.
|
||
srtPosition(70)
|
||
----
|
||
|
||
[#onlinescores]
|
||
=== Punteggi record online
|
||
|
||
Un gioco invia un punteggio con una riga:
|
||
|
||
[source,lua]
|
||
----
|
||
scoreSubmit(playerScore)
|
||
----
|
||
|
||
Tutto il resto è stato configurato una volta, nel menu: quale servizio, quale account e
|
||
con quale nome compare il giocatore (vedi
|
||
xref:Manual.adoc#onlineservice[Online: account, giochi e punteggi]). A un gioco serve solo un
|
||
`GAME_ID` nella sua voce di `games.dat` -- un UUID, che è la chiave con cui viene conservata la
|
||
classifica. Vedi <<singegetgameid,singeGetGameId>> per il formato.
|
||
|
||
|===
|
||
| Chiamata | Cosa fa
|
||
|
||
| `scoreBegin()`
|
||
| Annuncia che sta iniziando una partita. Facoltativa, e vale la pena chiamarla: vedi sotto.
|
||
|
||
| `scoreSubmit(value [, board [, meta]])`
|
||
| Mette in coda un punteggio. Restituisce `false` se il cabinato non è connesso o il gioco non ha un `GAME_ID`.
|
||
|
||
| `scoreLowestWins([board])`
|
||
| Dice che una classifica mette per primo il punteggio più basso. Chiamala prima del primo punteggio della classifica.
|
||
|
||
| `scoreBoard(onDone [, board])`
|
||
| Recupera la classifica. `onDone(ok, result)` con `result.top` (una lista di `{name, value}`),
|
||
`result.standing` (il `value` di questo giocatore, il `rank` e il numero di `players`) e
|
||
`result.order`, `"highest"` o `"lowest"`, il modo in cui la classifica ordina. Anche un cabinato
|
||
non connesso può recuperarla: vedi sotto.
|
||
|
||
| `scorePlayerName()`
|
||
| Il nome con cui questo cabinato invia i punteggi, o `nil` se non ne è stato scelto nessuno.
|
||
|
||
| `scoreSignedIn()`
|
||
| Se questo cabinato è connesso a un account online.
|
||
|
||
| `scoreWaiting()`
|
||
| Quanti punteggi sono in coda e non ancora inviati.
|
||
|
||
| `scoreUpdate()`
|
||
| Una volta per fotogramma, da `onOverlayUpdate`. Senza di essa non va avanti niente.
|
||
|
||
| `scoreGameId()`
|
||
| Il `GAME_ID` del gioco dalla sua voce in `games.dat`, o `nil` se non ne ha. Un gioco che non ce l'ha si può giocare, ma non classificare.
|
||
|===
|
||
|
||
**Un punteggio viene messo in coda, non inviato.** Viene scritto su disco e consegnato al
|
||
servizio quando c'è una connessione, perché un cabinato è spesso offline e un gioco non deve
|
||
mai bloccarsi sulla rete per mostrare la propria classifica. Un punteggio in coda sopravvive
|
||
allo spegnimento della macchina.
|
||
|
||
**Chiunque può leggere una classifica.** Un cabinato non connesso recupera comunque la
|
||
classifica di un gioco che il servizio offre a tutti, senza `standing` perché non c'è
|
||
nessun giocatore da mettere in classifica, così un gioco può mostrare dove invierebbe i
|
||
punteggi e chiedere al giocatore di accedere. `scoreSignedIn()` distingue i due casi: un
|
||
cabinato connesso che non ha scelto un nome non ha neanche uno `scorePlayerName()`, e
|
||
chiedergli di accedere sarebbe sbagliato. Un gioco che il servizio riserva a determinati
|
||
account non ha classifica per un cabinato che non sia connesso a uno di essi; il recupero
|
||
fallisce come fallirebbe per un gioco che il servizio non conosce. Inviare richiede sempre
|
||
un account.
|
||
|
||
**Un gioco può avere più di una classifica.** Passa un nome di classifica per tenere separata
|
||
ogni modalità di gioco -- un time attack separato da un totale di punti, la difficoltà facile
|
||
separata da quella difficile -- e rileggila con lo stesso nome. Le classifiche sono tenute
|
||
separate anche in base allo script in esecuzione: senza un nome, un punteggio va sulla
|
||
classifica che porta il nome dello script, cioè il nome del suo file senza estensione, quindi
|
||
`Collection/Raid.singe` invia a `raid`; con un nome, viene prima il nome dello script, quindi
|
||
`scoreSubmit(points, "hard")` da quello script invia a `raid.hard`.
|
||
Ogni titolo in un `.game` porta lo stesso `GAME_ID`, ed è questo che tiene i giochi di una
|
||
raccolta su classifiche proprie anche quando due di loro chiamano allo stesso modo le loro
|
||
modalità, mentre un gioco elencato due volte (in due lingue, per esempio: due voci di
|
||
`games.dat`, un solo script) mantiene un unico insieme. I nomi delle classifiche sono in
|
||
minuscolo e contengono lettere, cifre, `-`, `_` e `.`; qualsiasi altra cosa diventa `-`. Il
|
||
nome intero è lungo al massimo 32 caratteri, e la parte dello script viene accorciata per
|
||
prima così che il nome della modalità sopravviva.
|
||
|
||
**Una classifica può mettere per primo il punteggio più basso.** Colpi, morti, secondi:
|
||
alcuni giochi contano qualcosa che il giocatore vuole avere il meno possibile. Chiama
|
||
`scoreLowestWins` con il nome della classifica, come lo riceve `scoreSubmit`, prima del primo
|
||
punteggio della classifica, e ogni punteggio inviato a quella classifica e ogni suo recupero
|
||
vanno in quel verso. Il primo punteggio di una classifica ne fissa il verso per sempre, quindi
|
||
nessuna classifica viene mai ordinata in entrambi i modi: un punteggio inviato in seguito
|
||
nell'altro verso viene rifiutato, e la coda lo scarta invece di riprovare. In una classifica
|
||
così lo zero è un punteggio, e lo è anche un numero sotto zero, un giro sotto il par.
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un gioco di golf: vince chi fa meno colpi, e il par ha una classifica tutta sua.
|
||
scoreLowestWins("strokes")
|
||
scoreLowestWins("par")
|
||
|
||
function roundOver(strokes, coursePar)
|
||
scoreSubmit(strokes, "strokes")
|
||
scoreSubmit(strokes - coursePar, "par") -- -2 vuol dire due sotto il par
|
||
end
|
||
----
|
||
|
||
**Il nome su una classifica è un nickname, mai un indirizzo.** Si sceglie nello strumento
|
||
Online Account, è tenuto separato dall'account, ed è l'unica cosa di un giocatore che gli altri
|
||
giocatori vedono. Da tre a venti caratteri tra lettere, cifre, trattini e trattini bassi, che
|
||
iniziano e finiscono con una lettera o una cifra -- niente spazi, niente punti e niente `@`,
|
||
così un nickname non può nemmeno sembrare un indirizzo email. Maiuscole e minuscole non contano
|
||
per l'unicità, quindi `Ann` e `ANN` sono lo stesso nome.
|
||
|
||
**Un nickname si sceglie una volta sola.** Poter cambiare nome liberamente vorrebbe dire che
|
||
la storia di una classifica non è davvero di nessuno, quindi un giocatore non può cambiarlo in
|
||
seguito, e un indirizzo email non si può cambiare affatto.
|
||
|
||
Un nickname che nessuno dovrebbe rivendicare (`admin`, `staff`), o uno con parole che non
|
||
hanno posto su uno schermo in un locale pubblico, viene rifiutato quando viene scelto. Un
|
||
punteggio viene rifiutato finché non è stato scelto un nome, perché un punteggio senza il nome
|
||
di nessuno non sta su una classifica in alcun senso utile.
|
||
|
||
**Cosa significa un punteggio.** Il cabinato è la macchina del giocatore, quindi un punteggio
|
||
inviato è una *dichiarazione* -- niente di quello che il client può fare lo rende qualcosa di
|
||
più, e firmarlo con un segreto distribuito nel client sembrerebbe soltanto sicurezza. Quindi
|
||
niente qui finge di verificarne uno, e un gioco non dovrebbe essere scritto come se qualcos'altro
|
||
lo facesse.
|
||
|
||
**Vale la pena chiamare `scoreBegin()`** per l'unica cosa che non è una dichiarazione.
|
||
Chiamala quando inizia una partita; il punteggio che segue porta con sé quanto è durata la
|
||
partita, misurato all'altro capo invece che su questa macchina. Quel numero non è passato per
|
||
la macchina del giocatore, ed è questo che lo rende prezioso.
|
||
|
||
Niente viene rifiutato in base a esso, quindi chiamarla costa a un gioco una riga e non può
|
||
mai far scartare un punteggio legittimo. Un cabinato che è offline quando inizia una partita
|
||
semplicemente non registra nessun tempo, il che è onesto, non un guasto.
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function startLevel()
|
||
scoreBegin() -- il cronometro parte ora, all'altro capo
|
||
...
|
||
end
|
||
----
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Alla fine di una partita, invia il punteggio e poi mostra dove è finito in classifica.
|
||
function gameOver(points)
|
||
scoreSubmit(points)
|
||
scoreBoard(function(ok, board)
|
||
if not ok then
|
||
showMessage("Not signed in; score kept for later.")
|
||
return
|
||
end
|
||
for place, row in ipairs(board.top) do
|
||
showLine(place .. ". " .. row.name .. " " .. row.value)
|
||
end
|
||
end)
|
||
end
|
||
|
||
function onOverlayUpdate()
|
||
scoreUpdate()
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#stats]
|
||
=== Statistiche
|
||
|
||
L'overlay dello sviluppatore, nell'angolo in alto a sinistra: quanto è durato il fotogramma e
|
||
il suo caso peggiore negli ultimi due secondi, la frequenza dei fotogrammi, la memoria di Lua,
|
||
quanti sprite, suoni e timer sono vivi, quanti nodi della scena sono stati disegnati e in
|
||
quanti batch, quanta memoria per le texture sta occupando, e cosa sta facendo il disco.
|
||
|
||
Viene disegnato con il font di debug di SDL, quindi non ha bisogno di nessun file di font,
|
||
nessuna risorsa e nessun documento della GUI, e compare anche su una macchina dove il
|
||
dispositivo 3D non si è mai avviato. Viene disegnato dopo che
|
||
<<singescreenshot,`singeScreenshot`>> ha scattato la sua foto, quindi non finisce mai in uno
|
||
screenshot.
|
||
|
||
Lascialo spento in un gioco distribuito. Niente ti impedisce di metterlo dietro un tasto nel
|
||
tuo menu di servizio.
|
||
|
||
[#statsenable]
|
||
==== statsEnable
|
||
|
||
[source,text]
|
||
----
|
||
statsEnable(enabled)
|
||
----
|
||
|
||
Mostra o nasconde l'overlay. Mentre è visibile l'immagine viene ridisegnata a ogni fotogramma, così i numeri continuano a muoversi anche quando nient'altro sullo schermo si muove.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<statsisenabled,statsIsEnabled>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un tasto per lo sviluppatore, dietro un flag così il gioco distribuito non ha un tasto del genere.
|
||
function onInputPressed(key)
|
||
if DEVELOPER and key == SWITCH_SERVICE then
|
||
statsEnable(not statsIsEnabled())
|
||
end
|
||
end
|
||
----
|
||
|
||
[#statsisenabled]
|
||
==== statsIsEnabled
|
||
|
||
[source,text]
|
||
----
|
||
shown = statsIsEnabled()
|
||
----
|
||
|
||
Se l'overlay è attivo.
|
||
|
||
*Parametri:*
|
||
|
||
* Nessuno.
|
||
|
||
*Restituisce:* `true` o `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#terrain]
|
||
=== Terreno
|
||
|
||
Terreno da heightmap creato con `meshHeightmap`, che trasforma un'immagine in scala di grigi in una mesh con un vertice per pixel; `terrainGetHeight` rilegge quella mesh attraverso il nodo che la porta. Le posizioni e le altezze sono in unità del mondo, e la posizione e la scala del nodo vengono tenute in conto. Vedi Terreno in <<scenes3d,Scene 3D>>.
|
||
|
||
[#terraingetheight]
|
||
==== terrainGetHeight
|
||
|
||
[source,text]
|
||
----
|
||
height = terrainGetHeight(node, x, z)
|
||
----
|
||
|
||
L'altezza nel mondo della mesh heightmap su `node` nel punto `x, z` del mondo, interpolata tra i quattro campioni più vicini, o `nil` oltre i suoi bordi o quando la mesh del nodo non è una heightmap. La posizione nel mondo e la scala del nodo contano; la sua rotazione viene ignorata, perché i terreni stanno in piano. Usala per piazzare alberi, generare cose sul terreno, o per una telecamera che segue la superficie, senza il costo di un raycast. Termina lo script per un handle di nodo non valido.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo la cui mesh viene da `meshHeightmap`.
|
||
* `x, z` -- la posizione nel mondo da campionare.
|
||
|
||
*Restituisce:* Un numero, o `nil` fuori dal terreno.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<meshheightmap,meshHeightmap>>, <<physicsraycast,physicsRaycast>>, <<nodesetposition,nodeSetPosition>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Alberi sulle colline, e una telecamera che non scende mai sotto il terreno.
|
||
for i = 1, 40 do
|
||
local x = math.random(-50, 50)
|
||
local z = math.random(-50, 50)
|
||
local tree = modelInstance(treeModel)
|
||
nodeSetPosition(tree, x, terrainGetHeight(hills, x, z) or 0, z)
|
||
end
|
||
|
||
function onOverlayUpdate()
|
||
local cx, cy, cz = nodeGetWorldPosition(camera)
|
||
local ground = terrainGetHeight(hills, cx, cz)
|
||
if ground and cy < ground + 1.5 then
|
||
nodeSetPosition(camera, cx, ground + 1.5, cz)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#timer]
|
||
=== Timer
|
||
|
||
Qualcosa che deve succedere più tardi, senza contare i fotogrammi da te. I timer girano
|
||
sull'orologio del motore, che è quello virtuale con `--deterministic`, quindi un gioco
|
||
temporizzato si ripete esattamente tra un'esecuzione e l'altra. Scattano insieme alle altre
|
||
callback del fotogramma, sullo stesso thread di tutto il resto -- non c'è niente da bloccare e
|
||
niente che possa scattare mentre il tuo script è a metà di qualcosa.
|
||
|
||
Un timer rimasto indietro -- un caricamento lungo, un breakpoint -- scatta una volta quando il
|
||
gioco si rimette in pari invece di scattare le venti volte che ha perso.
|
||
|
||
Tutto viene dimenticato quando lo script si ricarica, quindi un timer dell'esecuzione
|
||
precedente non scatta mai in quella successiva.
|
||
|
||
[#timerafter]
|
||
==== timerAfter
|
||
|
||
[source,text]
|
||
----
|
||
id = timerAfter(milliseconds, function)
|
||
----
|
||
|
||
Chiama la funzione una volta, più tardi. La funzione riceve l'id del timer stesso, così una funzione può servire più timer.
|
||
|
||
*Parametri:*
|
||
|
||
* `milliseconds` -- numero; zero o meno scatta al fotogramma successivo.
|
||
* `function` -- chiamata come `function(id)`.
|
||
|
||
*Restituisce:* un numero; l'id del timer.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<timercancel,timerCancel>>, <<timerevery,timerEvery>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Lascia finire l'esplosione prima della schermata di game over.
|
||
soundPlay(bang)
|
||
timerAfter(1500, function()
|
||
showGameOver()
|
||
end)
|
||
----
|
||
|
||
[#timercancel]
|
||
==== timerCancel
|
||
|
||
[source,text]
|
||
----
|
||
timerCancel(id)
|
||
----
|
||
|
||
Ferma un timer. Annullarne uno che è già scattato non è un errore, quindi non serve controllare prima.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; da `timerAfter` o `timerEvery`.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#timerevery]
|
||
==== timerEvery
|
||
|
||
[source,text]
|
||
----
|
||
id = timerEvery(milliseconds, function)
|
||
----
|
||
|
||
Chiama la funzione più e più volte finché non viene annullato.
|
||
|
||
*Parametri:*
|
||
|
||
* `milliseconds` -- numero; quanto tempo tra una chiamata e l'altra.
|
||
* `function` -- chiamata come `function(id)`.
|
||
|
||
*Restituisce:* un numero; l'id del timer.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<timercancel,timerCancel>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fai scendere il conto alla rovescia una volta al secondo, e fermalo quando arriva a zero.
|
||
local left = 30
|
||
local clock
|
||
clock = timerEvery(1000, function()
|
||
left = left - 1
|
||
if left <= 0 then
|
||
timerCancel(clock)
|
||
outOfTime()
|
||
end
|
||
end)
|
||
----
|
||
|
||
[#timerisactive]
|
||
==== timerIsActive
|
||
|
||
[source,text]
|
||
----
|
||
running = timerIsActive(id)
|
||
----
|
||
|
||
Se il timer scatterà ancora: `true` per uno ripetuto finché non viene annullato, `false` per uno singolo che è già scattato.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero.
|
||
|
||
*Restituisce:* `true` o `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#tween]
|
||
=== Tween
|
||
|
||
Portare un numero da un valore a un altro nel tempo, lungo una curva. Dissolvenze,
|
||
scorrimenti, movimenti della telecamera, una barra della salute che si svuota, un menu che
|
||
entra scorrendo -- tutti la stessa cosa.
|
||
|
||
`tweenValue` ti consegna il numero e ti lascia decidere cosa significa. È voluto: il motore
|
||
ha nodi, sprite, elementi della GUI, materiali e luci, e un tween che li conoscesse tutti
|
||
sarebbe cinque gruppi di chiamate da tenere allineati. Uno che consegna un numero non ne ha
|
||
nessuno, e funziona su cose di cui il motore non ha mai sentito parlare.
|
||
|
||
Le curve di easing sono quelle solite. `EASE_LINEAR` non è affatto una curva;
|
||
`EASE_QUAD_*`, `EASE_CUBIC_*`, `EASE_SINE_*` ed `EASE_EXPO_*` vanno da dolci a brusche;
|
||
`EASE_BACK_*` va oltre e torna indietro; `EASE_ELASTIC_*` molleggia; `EASE_BOUNCE_*` cade e
|
||
rimbalza. Ognuna esiste in versione `_IN` (lenta all'inizio), `_OUT` (lenta alla fine) e
|
||
`_IN_OUT` (entrambe).
|
||
|
||
[#tweencancel]
|
||
==== tweenCancel
|
||
|
||
[source,text]
|
||
----
|
||
tweenCancel(id)
|
||
----
|
||
|
||
Ferma un tween dove si trova. Nessuna delle due funzioni viene chiamata di nuovo, e il valore resta dove era arrivato.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero; da `tweenValue`.
|
||
|
||
*Restituisce:* niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#tweenisactive]
|
||
==== tweenIsActive
|
||
|
||
[source,text]
|
||
----
|
||
running = tweenIsActive(id)
|
||
----
|
||
|
||
Se il tween è ancora in corso.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- numero.
|
||
|
||
*Restituisce:* `true` o `false`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#tweenvalue]
|
||
==== tweenValue
|
||
|
||
[source,text]
|
||
----
|
||
id = tweenValue(from, to, milliseconds, easing, onUpdate [, onDone])
|
||
----
|
||
|
||
Porta un numero da `from` a `to` in `milliseconds`, chiamando `onUpdate(value, progress)` a ogni fotogramma -- `progress` va da 0 a 1 e ignora la curva -- e `onDone()` una volta alla fine. L'ultimo `onUpdate` arriva sempre esattamente su `to`, così niente finisce un pixel prima.
|
||
|
||
*Parametri:*
|
||
|
||
* `from`, `to` -- numeri.
|
||
* `milliseconds` -- numero; zero o meno arriva al fotogramma successivo.
|
||
* `easing` -- uno dei valori `EASE_`.
|
||
* `onUpdate` -- chiamata come `function(value, progress)`.
|
||
* `onDone` -- facoltativa; chiamata senza argomenti quando finisce.
|
||
|
||
*Restituisce:* un numero; l'id del tween.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<tweencancel,tweenCancel>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fai entrare il titolo scorrendo e intanto fai apparire l'overlay in dissolvenza.
|
||
tweenValue(-200, 40, 600, EASE_BACK_OUT, function(x)
|
||
titleX = x
|
||
end)
|
||
|
||
tweenValue(0, 255, 600, EASE_SINE_OUT, function(alpha)
|
||
setOverlayOpacity(math.floor(alpha))
|
||
end, function()
|
||
startAttractLoop()
|
||
end)
|
||
----
|
||
|
||
[#vehicle]
|
||
=== Veicolo
|
||
|
||
Auto, moto, carri armati e barche costruiti su un corpo che fa da telaio (vedi <<physics,Fisica>>, Veicoli e Acqua). Un veicolo non ha un handle proprio: lo si indica tramite il nodo del suo telaio, che deve già portare un corpo `BODY_DYNAMIC` creato da `bodyNew`, e un nodo senza veicolo termina lo script in ogni chiamata dopo `vehicleNew`. Il telaio è rivolto verso -Z con Y in alto; le distanze sono in unità del mondo, le forze in newton, le coppie in newton metri e i regimi del motore in giri al minuto. Ogni chiamata di regolazione segna il veicolo da ricostruire al passo successivo della fisica, attorno alle posizioni delle ruote registrate quando le ruote sono state aggiunte. Fino a 16 veicoli alla volta, ciascuno con un massimo di 16 ruote.
|
||
|
||
[#vehiclenew]
|
||
==== vehicleNew
|
||
|
||
[source,text]
|
||
----
|
||
vehicleNew(node, kind)
|
||
----
|
||
|
||
Trasforma il corpo dinamico su `node` nel telaio di un veicolo. Un'auto, una moto o un carro armato hanno bisogno di almeno due ruote da `vehicleAddWheel` prima che il passo successivo della fisica li costruisca; una barca non ha ruote ed è spinta da un'elica che funziona solo sotto una superficie creata con `bodySetWater`. Il nuovo veicolo parte dai valori predefiniti: un motore da 500 Nm con il picco a 6000 giri al minuto e il minimo a 1000, il cambio automatico a cinque marce di Jolt, 35 gradi di sterzata massima, freni da 1500 Nm e un freno a mano da 4000 Nm, sospensioni da 1,5 Hz con smorzamento 0,5 e nessuna barra antirollio. Chiamarla di nuovo sullo stesso nodo scarta prima il vecchio veicolo e le sue ruote. Termina lo script per un tipo sconosciuto, un nodo il cui corpo non è dinamico, un tipo a ruote in un mondo 2D, o quando esistono già 16 veicoli.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo del telaio, che porta un corpo `BODY_DYNAMIC`.
|
||
* `kind` -- `VEHICLE_CAR` (qualsiasi numero di assali), `VEHICLE_MOTORCYCLE` (due ruote in linea, che si inclina in curva), `VEHICLE_TANK` (ruote su due cingoli, che gira sul posto) o `VEHICLE_BOAT` (uno scafo galleggiante con un'elica e un timone).
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehicledrive,vehicleDrive>>, <<vehiclesetthrust,vehicleSetThrust>>, <<bodynew,bodyNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
local car = nodeNew()
|
||
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
|
||
nodeSetPosition(car, 0, 0.9, 0)
|
||
bodyNew(car, BODY_DYNAMIC, SHAPE_BOX, 1.8, 0.5, 4.0)
|
||
bodySetMass(car, 1500)
|
||
vehicleNew(car, VEHICLE_CAR)
|
||
for _, p in ipairs({ { -0.95, -0.2, -1.4 }, { 0.95, -0.2, -1.4 }, { -0.95, -0.2, 1.4 }, { 0.95, -0.2, 1.4 } }) do
|
||
local hub = nodeNew()
|
||
nodeSetParent(hub, car)
|
||
nodeSetPosition(hub, p[1], p[2], p[3])
|
||
vehicleAddWheel(car, hub, 0.35, 0.25, 0.4)
|
||
end
|
||
----
|
||
|
||
[#vehicledelete]
|
||
==== vehicleDelete
|
||
|
||
[source,text]
|
||
----
|
||
vehicleDelete(node)
|
||
----
|
||
|
||
Rimuove il veicolo dal suo telaio: il vincolo delle ruote, l'input del pilota e tutte le regolazioni. Il corpo resta un normale corpo dinamico, quindi un rottame continua a scivolare e a ribaltarsi, e i nodi delle ruote restano dove li ha messi in posa l'ultimo passo; niente li muove più, a meno che non lo faccia lo script. Un nodo senza veicolo termina lo script.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehiclenew,vehicleNew>>, <<bodydelete,bodyDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onCollision(a, b, x, y, z, speed)
|
||
if (a == car or b == car) and speed > 25 and not wrecked then
|
||
wrecked = true
|
||
vehicleDelete(car) -- Il telaio continua a ribaltarsi come corpo
|
||
soundPlay(crashSound)
|
||
emitterStart(smoke)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#vehicleaddwheel]
|
||
==== vehicleAddWheel
|
||
|
||
[source,text]
|
||
----
|
||
index = vehicleAddWheel(node, wheelNode, radius, width, suspension)
|
||
----
|
||
|
||
Aggiunge al veicolo una ruota nel punto in cui `wheelNode` si trova rispetto al telaio quando il veicolo viene costruito la volta successiva. La sospensione appende la ruota `suspension` unità sotto quel punto quando è scarica e si comprime fino a metà di quella lunghezza. Da quel momento il motore di gioco scrive la trasformazione nel mondo del nodo della ruota dopo ogni passo, con X come assale e Y in alto, quindi lo pneumatico visibile va su un nodo figlio: un `meshCylinder` ruotato di 90 gradi attorno a Z. Le ruote davanti alla posizione media delle ruote lungo il muso contano come ruote anteriori: sterzano per impostazione predefinita e non hanno freno a mano. Un carro armato assegna le ruote al cingolo sinistro o destro secondo il lato del telaio su cui si trovano, e ha bisogno di almeno una ruota per lato. I valori sotto `0.001` vengono portati a quel valore. Termina lo script alla diciassettesima ruota.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo del telaio.
|
||
* `wheelNode` -- il nodo a cui è appesa la ruota; la sua posizione rispetto al telaio è il punto di attacco, e da ora in poi è il motore di gioco a metterlo in posa.
|
||
* `radius` -- il raggio della ruota, in unità del mondo.
|
||
* `width` -- la larghezza della ruota, in unità del mondo.
|
||
* `suspension` -- la lunghezza della sospensione a piena estensione, in unità del mondo; si comprime fino a metà.
|
||
|
||
*Restituisce:* L'indice della ruota, da `0`, per `vehicleSetWheel`, `vehicleIsWheelOnGround` e `vehicleGetWheelSlip`.
|
||
|
||
*Note:* Un veicolo viene costruito solo quando ha due ruote, e un'auto o una moto hanno bisogno anche di una ruota motrice; fino ad allora resta un semplice corpo e le chiamate di guida non fanno niente.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehiclesetwheel,vehicleSetWheel>>, <<vehiclesetsuspension,vehicleSetSuspension>>, <<nodefind,nodeFind>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il modello del camion porta nodi vuoti che hanno il nome delle sue ruote.
|
||
local truck = modelInstance(truckModel)
|
||
local wheels = {}
|
||
nodeSetPosition(truck, 0, 1.2, -8)
|
||
bodyNew(truck, BODY_DYNAMIC, SHAPE_BOX, 2.4, 1.4, 6.0)
|
||
bodySetMass(truck, 4000)
|
||
vehicleNew(truck, VEHICLE_CAR)
|
||
for i, name in ipairs({ "wheelFL", "wheelFR", "wheelRL", "wheelRR" }) do
|
||
wheels[i] = vehicleAddWheel(truck, nodeFind(name, truck), 0.5, 0.35, 0.45)
|
||
end
|
||
----
|
||
|
||
[#vehiclesetthrust]
|
||
==== vehicleSetThrust
|
||
|
||
[source,text]
|
||
----
|
||
vehicleSetThrust(node, maxForce, x, y, z)
|
||
----
|
||
|
||
Imposta l'elica di una barca: la forza con cui spinge a tutto gas, predefinita `2000` newton, e il punto nel sistema di riferimento dello scafo in cui spinge, predefinito `0, -0.2, 1`, appena sotto il ponte a poppa. La spinta è lungo il muso dello scafo e avviene solo mentre quel punto è sotto una superficie d'acqua; lo stesso punto decide se il timone e il freno agiscono. Una forza negativa diventa `0`. I tipi a ruote memorizzano i valori e li ignorano.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo dello scafo.
|
||
* `maxForce` -- la spinta a tutto gas, in newton.
|
||
* `x`, `y`, `z` -- dove si trova l'elica, nel sistema di riferimento dello scafo; la poppa è +Z.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehiclesetrudder,vehicleSetRudder>>, <<bodysetwater,bodySetWater>>, <<bodysetbuoyancy,bodySetBuoyancy>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
local raft = nodeNew()
|
||
nodeSetMesh(raft, meshBox(1.6, 0.3, 2.4), wood)
|
||
nodeSetPosition(raft, 1, 0.3, 4)
|
||
bodyNew(raft, BODY_DYNAMIC, SHAPE_BOX, 1.6, 0.3, 2.4)
|
||
bodySetMass(raft, 120)
|
||
bodySetBuoyancy(raft, 3.0) -- Galleggia alta sulla piscina
|
||
vehicleNew(raft, VEHICLE_BOAT)
|
||
vehicleSetThrust(raft, 600, 0, -0.1, 1.2) -- Un fuoribordo appeso a poppa
|
||
vehicleSetRudder(raft, 300)
|
||
----
|
||
|
||
[#vehiclesetwheel]
|
||
==== vehicleSetWheel
|
||
|
||
[source,text]
|
||
----
|
||
vehicleSetWheel(node, index, steered, driven)
|
||
----
|
||
|
||
Dice se una ruota gira con l'input di sterzo e se il motore la aziona, sostituendo i valori predefiniti per quella ruota: le ruote anteriori sterzano, e tutte le ruote sono motrici tranne quella anteriore di una moto. Le ruote motrici si accoppiano da un lato all'altro del telaio in differenziali, uno per assale, e la coppia del motore viene divisa in parti uguali tra gli assali; una ruota motrice senza compagna è un assale a sé. Un carro armato ignora entrambi i flag, perché i suoi cingoli muovono ogni ruota e fanno girare lo scafo. Ha effetto alla ricostruzione del passo successivo. Termina lo script per un indice a cui non corrisponde nessuna ruota del veicolo.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo del telaio.
|
||
* `index` -- l'indice della ruota da `vehicleAddWheel`.
|
||
* `steered` -- `true` perché la ruota giri fino a `vehicleSetSteering` a sterzata massima.
|
||
* `driven` -- `true` perché il motore la faccia girare.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclesetsteering,vehicleSetSteering>>, <<vehiclesetantiroll,vehicleSetAntiRoll>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un'auto sportiva a trazione posteriore: le ruote anteriori sterzano soltanto.
|
||
vehicleSetWheel(car, wheels[1], true, false)
|
||
vehicleSetWheel(car, wheels[2], true, false)
|
||
vehicleSetWheel(car, wheels[3], false, true)
|
||
vehicleSetWheel(car, wheels[4], false, true)
|
||
if fourWheelSteer then
|
||
vehicleSetWheel(car, wheels[3], true, true)
|
||
vehicleSetWheel(car, wheels[4], true, true)
|
||
end
|
||
----
|
||
|
||
[#vehiclesetengine]
|
||
==== vehicleSetEngine
|
||
|
||
[source,text]
|
||
----
|
||
vehicleSetEngine(node, maxTorque, maxRpm)
|
||
vehicleSetEngine(node, maxTorque, maxRpm, minRpm)
|
||
----
|
||
|
||
Imposta il motore: la coppia massima in newton metri, il limite di giri e il regime minimo in giri al minuto. I valori predefiniti sono `500`, `6000` e `1000`, e il minimo torna a `1000` quando viene omesso. La coppia viene mantenuta a `1` o più, il limite di giri a `100` o più e il minimo tra `1` e il limite di giri. Una barca non ha motore e ignora la chiamata. Ha effetto alla ricostruzione del passo successivo.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo del telaio.
|
||
* `maxTorque` -- coppia massima, in newton metri.
|
||
* `maxRpm` -- il limite di giri, in giri al minuto.
|
||
* `minRpm` -- il regime minimo, in giri al minuto; predefinito `1000`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehiclesetgears,vehicleSetGears>>, <<vehiclegetrpm,vehicleGetRpm>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
vehicleSetEngine(tank, 1500, 4000, 800) -- Un diesel: coppia ai bassi regimi
|
||
vehicleSetEngine(bike, 150, 9000, 1200) -- Un motore che urla
|
||
|
||
function onOverlayUpdate()
|
||
local rpm = vehicleGetRpm(bike)
|
||
overlayClear()
|
||
fontPrint(20, 20, string.format("%5.0f rpm", rpm))
|
||
if rpm > 8500 then
|
||
spriteDraw(shiftLight, 200, 20) -- Ora di salire di marcia
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#vehiclesetgears]
|
||
==== vehicleSetGears
|
||
|
||
[source,text]
|
||
----
|
||
vehicleSetGears(node, { ratio, ... })
|
||
vehicleSetGears(node, { ratio, ... }, reverseRatio)
|
||
vehicleSetGears(node, { ratio, ... }, reverseRatio, automatic)
|
||
----
|
||
|
||
Imposta il cambio: da uno a otto rapporti in avanti, a partire dalla prima, il rapporto della retromarcia (predefinito `2.9`, dato come numero positivo) e se il cambio cambia marcia da solo (predefinito `true`). Un veicolo che non riceve mai questa chiamata usa il cambio a cinque marce di Jolt, `2.66`, `1.78`, `1.3`, `1.0` e `0.74`, con la stessa retromarcia. Con `automatic` falso il cambio non cambia più marcia da solo, e non c'è nessuna chiamata per cambiare marcia dallo script. Una barca non ha cambio e ignora la chiamata. Ha effetto alla ricostruzione del passo successivo. Termina lo script quando la tabella è vuota o contiene più di otto rapporti.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo del telaio.
|
||
* `{ ratio, ... }` -- rapporti delle marce in avanti, giri del motore per giro del cambio, a partire dalla prima; da uno a otto.
|
||
* `reverseRatio` -- il rapporto della retromarcia; predefinito `2.9`.
|
||
* `automatic` -- `true` (predefinito) perché il cambio cambi marcia da solo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehiclegetgear,vehicleGetGear>>, <<vehiclesetengine,vehicleSetEngine>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un sei marce a rapporti ravvicinati per l'auto da corsa, un tre marce lungo per l'autobus.
|
||
vehicleSetGears(raceCar, { 3.2, 2.3, 1.8, 1.4, 1.1, 0.9 }, 3.0)
|
||
vehicleSetGears(bus, { 2.4, 1.3, 0.8 })
|
||
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
fontPrint(20, 20, string.format("Gear %d %.0f rpm", vehicleGetGear(raceCar), vehicleGetRpm(raceCar)))
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#vehiclesetrudder]
|
||
==== vehicleSetRudder
|
||
|
||
[source,text]
|
||
----
|
||
vehicleSetRudder(node, maxTorque)
|
||
----
|
||
|
||
Imposta il timone di una barca: la coppia attorno all'asse verticale dello scafo a pieno input di sterzo, predefinita `800` newton metri. Il timone fa presa con la velocità, un quinto della coppia da fermo e tutta da 3 unità del mondo al secondo in su, e gira al contrario mentre la barca va all'indietro. Agisce solo mentre il punto dell'elica è sott'acqua. Una coppia negativa diventa `0`; i tipi a ruote ignorano la chiamata.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehiclesetthrust,vehicleSetThrust>>, <<vehicledrive,vehicleDrive>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
vehicleNew(patrolBoat, VEHICLE_BOAT)
|
||
vehicleSetThrust(patrolBoat, 4000, 0, -0.4, 3.5)
|
||
vehicleSetRudder(patrolBoat, 2500) -- Scafo pesante, timone grande
|
||
|
||
function onOverlayUpdate()
|
||
local helm = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) / 32767
|
||
local gas = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_TRIGGER) / 32767
|
||
vehicleDrive(patrolBoat, gas, helm, 0)
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#vehiclesetsuspension]
|
||
==== vehicleSetSuspension
|
||
|
||
[source,text]
|
||
----
|
||
vehicleSetSuspension(node, frequency, damping)
|
||
----
|
||
|
||
Imposta la molla di ogni ruota: la sua frequenza propria in hertz (predefinita `1.5`, mantenuta a `0.1` o più; più alta è più rigida) e il suo rapporto di smorzamento (predefinito `0.5`; `0` rimbalza, `1` si assesta senza sovraelongazione). Ha effetto alla ricostruzione del passo successivo, su tutte le ruote insieme; non esiste una molla per singola ruota. Una barca non ha sospensioni e ignora la chiamata.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo del telaio.
|
||
* `frequency` -- la frequenza propria della molla, in hertz.
|
||
* `damping` -- il rapporto di smorzamento, da `0` in su.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclesetantiroll,vehicleSetAntiRoll>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il buggy fluttua sulle dune; l'auto da corsa si muove appena sulle molle.
|
||
vehicleSetSuspension(buggy, 1.0, 0.3)
|
||
vehicleSetSuspension(raceCar, 3.0, 0.7)
|
||
if surface == "gravel" then
|
||
vehicleSetSuspension(raceCar, 2.0, 0.5)
|
||
end
|
||
----
|
||
|
||
[#vehiclesetsteering]
|
||
==== vehicleSetSteering
|
||
|
||
[source,text]
|
||
----
|
||
vehicleSetSteering(node, maxDegrees)
|
||
----
|
||
|
||
Imposta quanto girano le ruote sterzanti a sterzata massima, in gradi; predefinito `35`, limitato tra `0` e `89`. Un input di sterzo di `-1` o `1` da `vehicleDrive` raggiunge questo angolo, e input più piccoli una sua parte proporzionale. Ha effetto alla ricostruzione del passo successivo. Un carro armato gira con i cingoli e una barca con il timone, quindi entrambi ignorano la chiamata.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehiclesetwheel,vehicleSetWheel>>, <<vehicledrive,vehicleDrive>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il go-kart gira su un fazzoletto; il camion ha bisogno di un arco ampio.
|
||
vehicleSetSteering(kart, 45)
|
||
vehicleSetSteering(truck, 25)
|
||
vehicleSetSteering(bus, 20)
|
||
if difficulty == "arcade" then
|
||
vehicleSetSteering(truck, 35) -- Guida indulgente
|
||
end
|
||
----
|
||
|
||
[#vehiclesetbrakes]
|
||
==== vehicleSetBrakes
|
||
|
||
[source,text]
|
||
----
|
||
vehicleSetBrakes(node, brakeTorque, handBrakeTorque)
|
||
----
|
||
|
||
Imposta la coppia frenante che ogni ruota può applicare con il pedale premuto a fondo, predefinita `1500` newton metri, e la coppia del freno a mano sulle ruote posteriori, predefinita `4000`; le ruote anteriori, quelle davanti alla posizione media delle ruote, non hanno freno a mano. I valori negativi diventano `0`. Un carro armato frena i cingoli con i valori predefiniti dei cingoli di Jolt e ignora la chiamata, e una barca frena opponendo resistenza alla propria velocità, quindi ignora anch'essa la chiamata. Ha effetto alla ricostruzione del passo successivo.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo del telaio.
|
||
* `brakeTorque` -- coppia del freno a pedale su ogni ruota, in newton metri.
|
||
* `handBrakeTorque` -- coppia del freno a mano sulle ruote posteriori, in newton metri.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehicledrive,vehicleDrive>>, <<vehiclegetwheelslip,vehicleGetWheelSlip>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
vehicleSetBrakes(car, 2200, 6000) -- Pastiglie da corsa e un freno a mano pronto per il drift
|
||
|
||
function onOverlayUpdate()
|
||
local brake = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_TRIGGER) / 32767
|
||
local handBrake = controllerGetButton(0, GAMEPAD_0.BUTTON_B.value) and 1 or 0
|
||
vehicleDrive(car, throttle(), steering(), brake, handBrake)
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#vehiclesetantiroll]
|
||
==== vehicleSetAntiRoll
|
||
|
||
[source,text]
|
||
----
|
||
vehicleSetAntiRoll(node, stiffness)
|
||
----
|
||
|
||
Aggiunge una barra antirollio su ogni assale, in newton per metro; `0` (predefinito) non ne monta nessuna e un valore negativo diventa `0`. Una barra collega le due ruote motrici di un assale, quindi un assale le cui ruote non sono motrici non riceve nessuna barra, e una moto non ne ha. Qualche centinaio tiene un'auto piatta in curva; troppo solleva le ruote interne. Un carro armato ignora la chiamata. Ha effetto alla ricostruzione del passo successivo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehiclesetsuspension,vehicleSetSuspension>>, <<vehiclesetwheel,vehicleSetWheel>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
local car = nodeNew()
|
||
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
|
||
nodeSetPosition(car, 0, 0.9, 0)
|
||
bodyNew(car, BODY_DYNAMIC, SHAPE_BOX, 1.8, 0.5, 4.0)
|
||
bodySetMass(car, 1500)
|
||
vehicleNew(car, VEHICLE_CAR)
|
||
addFourWheels(car)
|
||
vehicleSetEngine(car, 600, 6500, 1000)
|
||
vehicleSetAntiRoll(car, 800) -- Resta piatta nella chicane
|
||
----
|
||
|
||
[#vehicledrive]
|
||
==== vehicleDrive
|
||
|
||
[source,text]
|
||
----
|
||
vehicleDrive(node, forward, right)
|
||
vehicleDrive(node, forward, right, brake)
|
||
vehicleDrive(node, forward, right, brake, handBrake)
|
||
----
|
||
|
||
Imposta l'input del pilota, che resta valido fino alla chiamata successiva, quindi chiamala a ogni fotogramma da `onOverlayUpdate`. Acceleratore e sterzo vanno da `-1` a `1`, freno e freno a mano da `0` a `1`; i valori fuori intervallo vengono limitati e gli input omessi valgono `0`. Un'auto o una moto riceve tutti e quattro come input del pilota di Jolt, e un acceleratore negativo va indietro con la retromarcia. Un carro armato prende `forward` come acceleratore e `right` rallenta il cingolo interno; senza acceleratore i cingoli girano in senso opposto l'uno all'altro così lo scafo ruota sul posto, e il freno a mano viene ignorato. Una barca prende `forward` come acceleratore dell'elica, `right` come timone e `brake` come una resistenza contro la velocità dello scafo, ignora il freno a mano, e non fa proprio niente mentre l'elica è fuori dall'acqua. Qualsiasi input diverso da zero sveglia un telaio che si è addormentato.
|
||
|
||
*Parametri:*
|
||
|
||
* `node` -- il nodo del telaio.
|
||
* `forward` -- acceleratore, da `-1` a `1`; un valore negativo va in retromarcia.
|
||
* `right` -- sterzo, da `-1` (sinistra) a `1` (destra).
|
||
* `brake` -- freno a pedale, da `0` a `1`; predefinito `0`.
|
||
* `handBrake` -- freno a mano, da `0` a `1`; predefinito `0`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehiclegetspeed,vehicleGetSpeed>>, <<vehiclesetbrakes,vehicleSetBrakes>>, <<controllergetaxis,controllerGetAxis>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
local stick = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X)
|
||
local steer = (math.abs(stick) > SINGE_DEAD_ZONE) and stick / 32767 or 0
|
||
local gas = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_TRIGGER) / 32767
|
||
local brake = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_TRIGGER) / 32767
|
||
local handBrake = controllerGetButton(0, GAMEPAD_0.BUTTON_B.value) and 1 or 0
|
||
if controllerGetButton(0, GAMEPAD_0.BUTTON_A.value) then
|
||
gas = -gas -- Retromarcia mentre A è premuto
|
||
end
|
||
vehicleDrive(car, gas, steer, brake, handBrake)
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#vehiclegetspeed]
|
||
==== vehicleGetSpeed
|
||
|
||
[source,text]
|
||
----
|
||
speed = vehicleGetSpeed(node)
|
||
----
|
||
|
||
La velocità del corpo del telaio lungo il suo muso, in unità del mondo al secondo, negativa mentre va all'indietro; il moto laterale in una derapata non conta. Funziona per ogni tipo, barche comprese, dal momento in cui viene chiamata `vehicleNew`.
|
||
|
||
*Restituisce:* Un numero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehiclegetrpm,vehicleGetRpm>>, <<vehiclegetgear,vehicleGetGear>>, <<bodygetvelocity,bodyGetVelocity>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
local speed = vehicleGetSpeed(car)
|
||
overlayClear()
|
||
fontPrint(20, 20, string.format("%3.0f km/h", math.abs(speed) * 3.6))
|
||
if speed < -1 then
|
||
spriteDraw(reversingLamp, 300, 20)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#vehiclegetrpm]
|
||
==== vehicleGetRpm
|
||
|
||
[source,text]
|
||
----
|
||
rpm = vehicleGetRpm(node)
|
||
----
|
||
|
||
Il regime del motore in giri al minuto, tra il minimo e il limite di giri di `vehicleSetEngine`, per un contagiri o per l'intonazione di un suono del motore in loop. `0` per una barca, che non ha motore, e per un veicolo che non è ancora stato costruito perché gli mancano le ruote.
|
||
|
||
*Restituisce:* Un numero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehiclesetengine,vehicleSetEngine>>, <<vehiclegetgear,vehicleGetGear>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una barra contagiri che diventa rossa oltre 6000.
|
||
function onOverlayUpdate()
|
||
local rpm = vehicleGetRpm(car)
|
||
overlayClear()
|
||
if rpm > 6000 then
|
||
colorForeground(255, 40, 40)
|
||
else
|
||
colorForeground(60, 220, 60)
|
||
end
|
||
overlayBox(20, 20, 20 + math.floor(rpm / 6500 * 200), 32)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#vehiclegetgear]
|
||
==== vehicleGetGear
|
||
|
||
[source,text]
|
||
----
|
||
gear = vehicleGetGear(node)
|
||
----
|
||
|
||
La marcia che il cambio ha innestato: `0` in folle, `-1` in retromarcia, da `1` in su per le marce in avanti nell'ordine in cui le ha date `vehicleSetGears`. `0` per una barca e per un veicolo che non è ancora stato costruito.
|
||
|
||
*Restituisce:* Un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehiclesetgears,vehicleSetGears>>, <<vehiclegetrpm,vehicleGetRpm>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
local lastGear = 0
|
||
|
||
function onOverlayUpdate()
|
||
local gear = vehicleGetGear(car)
|
||
if gear ~= lastGear then
|
||
soundPlay(gearChange)
|
||
lastGear = gear
|
||
end
|
||
overlayClear()
|
||
fontPrint(20, 20, (gear == -1) and "R" or ((gear == 0) and "N" or tostring(gear)))
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#vehicleiswheelonground]
|
||
==== vehicleIsWheelOnGround
|
||
|
||
[source,text]
|
||
----
|
||
touching = vehicleIsWheelOnGround(node, index)
|
||
----
|
||
|
||
Se il cast della sospensione di quella ruota ha trovato qualcosa contro cui premere in questo passo. `false` per un indice a cui non corrisponde nessuna ruota del veicolo e per un veicolo che non è ancora stato costruito; in nessuno dei due casi viene sollevato un errore.
|
||
|
||
*Restituisce:* Vero o falso.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclegetwheelslip,vehicleGetWheelSlip>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- In volo quando nessuna ruota tocca; un atterraggio dopo un salto scuote la telecamera.
|
||
function onOverlayUpdate()
|
||
local airborne = true
|
||
for i = 0, 3 do
|
||
airborne = airborne and not vehicleIsWheelOnGround(car, i)
|
||
end
|
||
if wasAirborne and not airborne then
|
||
cameraShake = 12
|
||
end
|
||
wasAirborne = airborne
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#vehiclegetwheelslip]
|
||
==== vehicleGetWheelSlip
|
||
|
||
[source,text]
|
||
----
|
||
slip = vehicleGetWheelSlip(node, index)
|
||
----
|
||
|
||
Quanto la ruota slitta o striscia lungo la sua direzione di rotolamento, da `0` in presa a `1` che gira a vuoto o bloccata, per lo stridio delle gomme, il fumo e i segni di frenata. Sempre `0` per le ruote di un carro armato, per un indice a cui non corrisponde nessuna ruota del veicolo e per un veicolo che non è ancora stato costruito.
|
||
|
||
*Restituisce:* Un numero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vehicleiswheelonground,vehicleIsWheelOnGround>>, <<vehiclegetspeed,vehicleGetSpeed>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
local slip = math.max(vehicleGetWheelSlip(car, 2), vehicleGetWheelSlip(car, 3))
|
||
if slip > 0.4 and vehicleIsWheelOnGround(car, 2) then
|
||
emitterStart(tireSmoke)
|
||
if not soundIsPlaying(squealChannel) then
|
||
squealChannel = soundPlay(squeal)
|
||
end
|
||
else
|
||
emitterStop(tireSmoke)
|
||
end
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
----
|
||
|
||
[#view]
|
||
=== Vista
|
||
|
||
Una vista è una seconda telecamera disegnata su una texture a ogni fotogramma, per un monitor, uno specchio o un portale nella scena: `viewNew` ne crea una e restituisce un handle intero, `viewSetCamera` la punta su un nodo, e `materialSetView` la mostra su un materiale. Ne possono esistere fino a quattro alla volta; ognuna disegna di nuovo l'intera scena alla propria dimensione, in pixel indipendenti dall'overlay, con la proiezione della telecamera principale, le ombre del fotogramma (comprese le cascate di una luce direzionale: ogni punto prende la cascata più fine che lo copre) e senza bloom, quindi tienile poche e piccole. Ogni vista gira i billboard (`nodeSetBillboard`) verso la propria telecamera e ordina da lì i disegni con fusione dal fondo verso il davanti, così una vista la cui telecamera guarda da un altro punto vede comunque i billboard di fronte e gli oggetti trasparenti nell'ordine giusto. Un handle non valido termina lo script. Vedi <<scenes3d,Scene 3D>>.
|
||
|
||
[#viewdelete]
|
||
==== viewDelete
|
||
|
||
[source,text]
|
||
----
|
||
viewDelete(view)
|
||
----
|
||
|
||
Libera la vista e le sue texture, e ferma la passata di rendering in più che costava. I materiali che la mostrano tornano alla loro texture. Dopo, l'handle non è più valido e termina lo script se viene usato di nuovo; poiché esistono solo quattro viste alla volta, elimina quelle di cui un livello non ha più bisogno.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<viewnew,viewNew>>, <<materialsetview,materialSetView>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- I monitor della sala di sorveglianza vengono disegnati solo mentre il giocatore è dentro.
|
||
function onEnterSecurityRoom()
|
||
feed = viewNew(512, 384)
|
||
viewSetCamera(feed, hallwayCamera)
|
||
materialSetView(monitorLook, feed)
|
||
end
|
||
|
||
function onLeaveSecurityRoom()
|
||
viewDelete(feed)
|
||
feed = nil
|
||
end
|
||
----
|
||
|
||
[#viewnew]
|
||
==== viewNew
|
||
|
||
[source,text]
|
||
----
|
||
view = viewNew(width, height)
|
||
----
|
||
|
||
Crea una telecamera disegnata su una texture di `width` per `height` a ogni fotogramma (ciascuna dimensione limitata tra `1` e `4096`), dalla vista predefinita finché `viewSetCamera` non indica un nodo, e restituisce l'handle della vista per `materialSetView`. La vista usa la proiezione di `cameraSetPerspective` o `cameraSetOrthographic` con le proprie proporzioni. La scena viene disegnata una volta in più per ogni vista, quindi `512` per `384` basta e avanza per un monitor e un Raspberry Pi ne vuole di più piccole. Termina lo script quando il 3D non è disponibile o quando sono già in uso quattro viste.
|
||
|
||
*Parametri:*
|
||
|
||
* `width` -- la larghezza della texture in pixel.
|
||
* `height` -- l'altezza della texture in pixel.
|
||
|
||
*Restituisce:* L'handle della vista, un intero.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<viewsetcamera,viewSetCamera>>, <<materialsetview,materialSetView>>, <<viewdelete,viewDelete>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un monitor di sorveglianza che mostra una telecamera dietro il giocatore.
|
||
eye = nodeNew()
|
||
nodeSetPosition(eye, 0, 2.2, -5)
|
||
nodeLookAt(eye, 0, 0.6, 0)
|
||
feed = viewNew(512, 384)
|
||
viewSetCamera(feed, eye)
|
||
screen = materialNew()
|
||
materialSetView(screen, feed)
|
||
materialSetUnlit(screen, true)
|
||
nodeSetMesh(monitor, meshPlane(2, 1.5), screen)
|
||
nodeSetRotation(monitor, 90, 0, 0)
|
||
----
|
||
|
||
[#viewsetcamera]
|
||
==== viewSetCamera
|
||
|
||
[source,text]
|
||
----
|
||
viewSetCamera(view, node)
|
||
viewSetCamera(view, nil)
|
||
viewSetCamera(view)
|
||
----
|
||
|
||
Il nodo da cui guarda la vista, lungo il proprio -Z come la telecamera principale, quindi il movimento dei suoi genitori conta. Con `nil` o senza secondo argomento la vista torna alla vista predefinita da `(0, 0, 5)` che guarda l'origine. Rendi il nodo figlio di un'auto per uno specchietto retrovisore, o rifletti la posizione della telecamera principale rispetto a un muro per uno specchio su quel muro. Termina lo script per un handle di nodo non valido.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<viewnew,viewNew>>, <<cameraset,cameraSet>>, <<nodelookat,nodeLookAt>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il monitor dell'atrio passa a turno per tre telecamere di sorveglianza.
|
||
cameras = { lobbyCamera, garageCamera, roofCamera }
|
||
current = 1
|
||
|
||
function onOverlayUpdate()
|
||
if singeGetTicks() - lastSwitch > 4000 then
|
||
current = current % #cameras + 1
|
||
lastSwitch = singeGetTicks()
|
||
viewSetCamera(feed, cameras[current])
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#video]
|
||
=== Video
|
||
|
||
La famiglia `video*` gestisce clip video aggiuntive sopra il disco: un personaggio che parla in un angolo, un'animazione pre-renderizzata in un momento chiave della storia, diversi spezzoni contemporanei. Con `videoLoad` si può caricare un numero qualsiasi di video, e ciascuno restituisce un handle intero opaco che ogni altra funzione `video*` accetta come primo argomento; un handle già scaricato termina lo script. I video si disegnano sull'overlay con `videoDraw` in coordinate dell'overlay e si compongono come gli sprite, e un video avanza solo mentre viene disegnato (o usato come materiale tramite <<materialsetvideo,materialSetVideo>>). Ogni video ha la propria selezione della traccia audio e il proprio volume stereo, indipendenti dal disco. Come gli sprite, i video vanno disegnati da `onOverlayUpdate` e prima o poi liberati con `videoUnload`; le regole del disegno sono in <<overlay,Overlay>>.
|
||
|
||
[#videodraw]
|
||
==== videoDraw
|
||
|
||
[source,text]
|
||
----
|
||
videoDraw(id, x, y, x2, y2)
|
||
videoDraw(id, x, y, centered)
|
||
----
|
||
|
||
Fa avanzare un video fino al fotogramma che il suo orologio richiede e disegna quel fotogramma sull'overlay. L'orologio segue l'audio del video quando ne ha uno e il tempo reale altrimenti, quindi una chiamata può avanzare di zero, uno o più fotogrammi; un video che arriva alla fine riparte dal fotogramma `0`. La forma a cinque argomenti stira il fotogramma nel rettangolo da `(x, y)` a `(x2, y2)` compresi, sempre con campionamento nearest-neighbor e ignorando `videoScale`, `videoRotate` e `videoQuality`. La forma a quattro argomenti disegna il fotogramma alla sua dimensione nativa trasformato da `videoScale` e `videoRotate`, ancorato al suo angolo in alto a sinistra o, quando `centered` è `true`, al suo centro; il fotogramma trasformato viene ricostruito solo quando cambia il fotogramma o la trasformazione, e gli angoli lasciati dalla rotazione sono trasparenti. Se il decoder non ha ancora prodotto un fotogramma non viene disegnato nulla.
|
||
|
||
Un fotogramma che `videoScale` o `videoRotate` non scalano né girano, disegnato sull'overlay dove non è stato disegnato nulla dopo un `overlayClear` con un colore trasparente -- un video di sfondo disegnato per primo, per esempio -- viene disegnato invece dalla GPU, sotto l'overlay, e non costa quasi nulla; tutto ciò che viene disegnato dopo gli va comunque sopra. In quel caso viene scalato sullo schermo come l'overlay, invece di passare prima nell'overlay con il nearest-neighbor. In tutti gli altri casi il fotogramma viene copiato nell'overlay dalla CPU, cosa che su una macchina piccola costa diversi millisecondi a fotogramma, quindi disegna un video prima di ciò che gli va sopra, non dopo.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle del video restituito da `videoLoad`.
|
||
* `x`, `y` -- punto di ancoraggio in coordinate dell'overlay.
|
||
* `x2`, `y2` -- angolo opposto del rettangolo di destinazione, compreso.
|
||
* `centered` -- booleano. `true` mette il centro del fotogramma disegnato in `(x, y)`, `false` il suo angolo in alto a sinistra. Usa `true` per i video ruotati.
|
||
|
||
*Note:* Un video che non viene disegnato non avanza, quindi la pausa è implicita mentre una clip è nascosta. Disegna solo da `onOverlayUpdate`.
|
||
|
||
*Disponibile da:* 2.00. La forma scalata/ruotata con il flag `centered` è stata aggiunta nella 2.10.
|
||
*Vedi anche:* <<videoload,videoLoad>>, <<videoscale,videoScale>>, <<videorotate,videoRotate>>, <<spritedraw,spriteDraw>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Clip della modalità attract in un riquadro fisso, in loop tra i propri fotogrammi di inizio e di fine.
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
videoDraw(attractVideo, VIDEO_X, VIDEO_Y, VIDEO_X + VIDEO_W - 1, VIDEO_Y + VIDEO_H - 1)
|
||
if videoGetFrame(attractVideo) > ATTRACT_END then
|
||
videoSeek(attractVideo, ATTRACT_START)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#videogetaudiotrack]
|
||
==== videoGetAudioTrack
|
||
|
||
[source,text]
|
||
----
|
||
track = videoGetAudioTrack(id)
|
||
----
|
||
|
||
Restituisce l'indice a base zero della traccia audio che un video caricato sta riproducendo. `videoLoad` seleziona la traccia indicata da `--audio` o dalla voce `AUDIO_TRACK` di `games.dat` (predefinita `0`) quando il file ha abbastanza tracce; un video senza alcun audio riporta `-1`.
|
||
|
||
*Restituisce:* indice intero della traccia.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<videosetaudiotrack,videoSetAudioTrack>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videogetlanguage,videoGetLanguage>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Mostra nell'overlay delle opzioni quale doppiaggio usa il filmato.
|
||
function drawAudioOption()
|
||
local track = videoGetAudioTrack(cutscene)
|
||
local label = "None"
|
||
if track >= 0 then
|
||
label = videoGetLanguageDescription(videoGetLanguage(cutscene, track))
|
||
end
|
||
overlayPrint(OPTION_X, OPTION_Y, "Cutscene audio: " .. label)
|
||
end
|
||
----
|
||
|
||
[#videogetaudiotracks]
|
||
==== videoGetAudioTracks
|
||
|
||
[source,text]
|
||
----
|
||
count = videoGetAudioTracks(id)
|
||
----
|
||
|
||
Restituisce quanti flussi audio contiene un video caricato. Una clip muta riporta `0`; proteggi i cicli e l'aritmetica in modulo da questo caso.
|
||
|
||
*Restituisce:* conteggio intero, `0` o più.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<videogetlanguage,videoGetLanguage>>, <<videosetaudiotrack,videoSetAudioTrack>>, <<videogetaudiotrack,videoGetAudioTrack>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Offri il menu delle lingue solo quando il filmato ha davvero delle scelte.
|
||
cutscene = videoLoad("videos/briefing.mkv")
|
||
if videoGetAudioTracks(cutscene) > 1 then
|
||
buildLanguageMenu(cutscene)
|
||
else
|
||
languageMenuEnabled = false
|
||
end
|
||
----
|
||
|
||
[#videogetframe]
|
||
==== videoGetFrame
|
||
|
||
[source,text]
|
||
----
|
||
frame = videoGetFrame(id)
|
||
----
|
||
|
||
Restituisce il fotogramma su cui è posizionato un video caricato, a base zero. La posizione cambia solo mentre il video è in riproduzione e viene disegnato, e torna a `0` quando la clip ricomincia. Confrontala con i tuoi marcatori di inizio e di fine per ripetere una sezione, o con `videoGetFrameCount` per accorgerti della fine di una clip da riprodurre una volta sola.
|
||
|
||
*Restituisce:* numero intero del fotogramma.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<videoseek,videoSeek>>, <<videogetframecount,videoGetFrameCount>>, <<videodraw,videoDraw>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Restituisci il controllo al giocatore quando il briefing è stato riprodotto una volta per intero.
|
||
function onOverlayUpdate()
|
||
if briefingRunning then
|
||
videoDraw(briefing, 0, 0, false)
|
||
if videoGetFrame(briefing) >= videoGetFrameCount(briefing) - 1 then
|
||
briefingRunning = false
|
||
videoPause(briefing)
|
||
end
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#videogetframecount]
|
||
==== videoGetFrameCount
|
||
|
||
[source,text]
|
||
----
|
||
count = videoGetFrameCount(id)
|
||
----
|
||
|
||
Restituisce il numero totale di fotogrammi di un video caricato, contati dalla tabella dei fotogrammi del file quando è stato indicizzato. I fotogrammi vanno da `0` a `count - 1`. Usalo per le barre di avanzamento, per far ricominciare un loop o per accorgerti di quando una clip da riprodurre una volta sola sta per finire.
|
||
|
||
*Restituisce:* conteggio intero dei fotogrammi.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<videogetframe,videoGetFrame>>, <<videoseek,videoSeek>>, <<videoisplaying,videoIsPlaying>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Linea di avanzamento sotto una clip di caricamento: un riquadro di contorno e un riempimento che cresce.
|
||
function drawProgress()
|
||
local total = videoGetFrameCount(loadingClip)
|
||
local done = videoGetFrame(loadingClip) / (total - 1)
|
||
colorForeground(255, 255, 255, 255)
|
||
overlayBox(BAR_X - 1, BAR_Y - 1, BAR_X + BAR_W + 1, BAR_Y + 1)
|
||
overlayLine(BAR_X, BAR_Y, BAR_X + BAR_W * done, BAR_Y)
|
||
end
|
||
----
|
||
|
||
[#videogetheight]
|
||
==== videoGetHeight
|
||
|
||
[source,text]
|
||
----
|
||
height = videoGetHeight(id)
|
||
----
|
||
|
||
Restituisce l'altezza dei fotogrammi di un video caricato in pixel, la dimensione nativa del file e non quella a cui viene disegnato. Combinala con `videoScale` per far stare una clip in un riquadro mantenendone le proporzioni.
|
||
|
||
*Restituisce:* altezza intera in pixel.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<videogetwidth,videoGetWidth>>, <<videoscale,videoScale>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fai stare la clip in una finestra 320x240 senza deformarla.
|
||
local fitX = 320 / videoGetWidth(clip)
|
||
local fitY = 240 / videoGetHeight(clip)
|
||
local fit = math.min(fitX, fitY)
|
||
videoScale(clip, fit)
|
||
----
|
||
|
||
[#videogetlanguage]
|
||
==== videoGetLanguage
|
||
|
||
[source,text]
|
||
----
|
||
code = videoGetLanguage(id, track)
|
||
----
|
||
|
||
Restituisce l'etichetta della lingua memorizzata nel file per una traccia audio di un video caricato. L'etichetta è un codice ISO 639 di tre lettere come `"eng"` o `"jpn"`; una traccia senza un'etichetta di tre lettere restituisce `"unk"`. Una traccia non compresa tra `0` e `videoGetAudioTracks(id) - 1` termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle del video.
|
||
* `track` -- indice della traccia audio, a base zero.
|
||
|
||
*Restituisce:* stringa con il codice della lingua.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<videogetlanguagedescription,videoGetLanguageDescription>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videosetaudiotrack,videoSetAudioTrack>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fai corrispondere il doppiaggio del filmato alla lingua scelta per il disco.
|
||
local wanted = discGetLanguage(discGetAudioTrack())
|
||
for track = 0, videoGetAudioTracks(cutscene) - 1 do
|
||
if videoGetLanguage(cutscene, track) == wanted then
|
||
videoSetAudioTrack(cutscene, track)
|
||
break
|
||
end
|
||
end
|
||
----
|
||
|
||
[#videogetlanguagedescription]
|
||
==== videoGetLanguageDescription
|
||
|
||
[source,text]
|
||
----
|
||
name = videoGetLanguageDescription(code)
|
||
----
|
||
|
||
Cerca il nome inglese di un codice di lingua. A differenza delle altre funzioni `video*` non accetta un handle; è una pura ricerca in tabella che accetta i codici di due lettere ISO 639-1 e quelli di tre lettere ISO 639-2 (sia la forma T sia la B), confrontati senza distinguere tra maiuscole e minuscole. I codici non riconosciuti, compreso il `"unk"` restituito per le tracce senza etichetta, danno `"Unknown"`. `Framework.singe` la rende disponibile anche con l'alias `discGetLanguageDescription`, per simmetria con l'API del disco.
|
||
|
||
*Parametri:*
|
||
|
||
* `code` -- codice di lingua come `"en"`, `"eng"` o `"jpn"`.
|
||
|
||
*Restituisce:* stringa con il nome della lingua, oppure `"Unknown"`.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<videogetlanguage,videoGetLanguage>>, <<discgetlanguage,discGetLanguage>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Costruisci un menu di scelta della lingua dalle tracce di un video.
|
||
options = {}
|
||
for track = 0, videoGetAudioTracks(cutscene) - 1 do
|
||
local code = videoGetLanguage(cutscene, track)
|
||
local name = videoGetLanguageDescription(code)
|
||
table.insert(options, { track = track, label = name })
|
||
end
|
||
----
|
||
|
||
[#videogetvolume]
|
||
==== videoGetVolume
|
||
|
||
[source,text]
|
||
----
|
||
left, right = videoGetVolume(id)
|
||
volume = videoGetVolume()
|
||
----
|
||
|
||
Restituisce il volume stereo di un video caricato come due percentuali. Un video appena caricato è al livello di `--volume_nonvldp` su entrambi i canali (predefinito `100`). Nota che tornano due valori, prima il sinistro.
|
||
|
||
Chiamata senza argomenti è un'estensione di Hypseus che risponde invece con il volume del laserdisc stesso, come un unico numero sulla scala da `0` a `64` di Hypseus anziché come percentuale. I suoi giochi lo raddoppiano per <<musicsetvolume,`musicSetVolume`>>, il cui intervallo va da `0` a `128`. Il livello segue `--volume_vldp`.
|
||
|
||
*Restituisce:* due interi, sinistro e destro, ciascuno da `0` a `100`; oppure, senza argomenti, un intero da `0` a `64`.
|
||
|
||
*Disponibile da:* 2.00, la forma senza argomenti per il disco 3.00
|
||
*Vedi anche:* <<videosetvolume,videoSetVolume>>, <<videoload,videoLoad>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Abbassa il volume del filmato mentre suona una voce fuori campo, poi ripristinalo.
|
||
function playVoiceOver(sound)
|
||
savedLeft, savedRight = videoGetVolume(cutscene)
|
||
videoSetVolume(cutscene, savedLeft / 4, savedRight / 4)
|
||
soundPlay(sound)
|
||
duckingCutscene = true
|
||
end
|
||
----
|
||
|
||
[#videogetwidth]
|
||
==== videoGetWidth
|
||
|
||
[source,text]
|
||
----
|
||
width = videoGetWidth(id)
|
||
----
|
||
|
||
Restituisce la larghezza dei fotogrammi di un video caricato in pixel, la dimensione nativa del file e non quella a cui viene disegnato.
|
||
|
||
*Restituisce:* larghezza intera in pixel.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<videogetheight,videoGetHeight>>, <<videoscale,videoScale>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Centra in cima all'overlay una clip disegnata alla dimensione nativa.
|
||
local x = (overlayGetWidth() - videoGetWidth(clip)) / 2
|
||
function onOverlayUpdate()
|
||
videoDraw(clip, x, 16, false)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#videoisplaying]
|
||
==== videoIsPlaying
|
||
|
||
[source,text]
|
||
----
|
||
playing = videoIsPlaying(id)
|
||
----
|
||
|
||
Indica se un video caricato è nello stato di riproduzione, cioè se `videoPlay` è stata chiamata dopo l'ultimo `videoPause`. Un video in riproduzione avanza comunque solo mentre viene disegnato. I video partono in pausa dopo `videoLoad`.
|
||
|
||
*Restituisce:* booleano.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<videoplay,videoPlay>>, <<videopause,videoPause>>, <<videogetframe,videoGetFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un solo tasto avvia e ferma la clip picture-in-picture.
|
||
function onInputPressed(what)
|
||
if what == SWITCH_BUTTON2 then
|
||
if videoIsPlaying(pipVideo) then
|
||
videoPause(pipVideo)
|
||
else
|
||
videoPlay(pipVideo)
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
[#videoload]
|
||
==== videoLoad
|
||
|
||
[source,text]
|
||
----
|
||
id = videoLoad(filename)
|
||
----
|
||
|
||
Apre un file video e ne restituisce l'handle. È accettato qualsiasi contenitore e codec che l'FFmpeg incluso sappia decodificare. Il motore costruisce o legge un indice dei fotogrammi in una cartella dei dati che prende il nome dalla posizione del video sotto la radice dei dati di Singe, quindi il primo caricamento di un file grande può richiedere un momento; non riuscire a creare quella cartella termina lo script. Il video parte in pausa sul fotogramma `0`, con la traccia audio impostata da `--audio` o dalla voce `AUDIO_TRACK` di `games.dat` (predefinita `0`) quando il file ha abbastanza tracce, ed entrambi i canali al livello di `--volume_nonvldp`. I fotogrammi vengono decodificati in RGB perché si possano disegnare sull'overlay.
|
||
|
||
*Parametri:*
|
||
|
||
* `filename` -- percorso del file video, relativo alla cartella dei giochi o dentro un gioco impacchettato.
|
||
|
||
*Restituisce:* handle intero del video.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<videoplay,videoPlay>>, <<videodraw,videoDraw>>, <<videounload,videoUnload>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Carica la clip della modalità attract del gioco selezionato e posizionala all'inizio del loop.
|
||
function loadGameAssets()
|
||
attractVideo = videoLoad(GAME_LIST[GAME_SELECTED].ATTRACT)
|
||
if GAME_LIST[GAME_SELECTED].AUDIO_TRACK then
|
||
videoSetAudioTrack(attractVideo, GAME_LIST[GAME_SELECTED].AUDIO_TRACK)
|
||
end
|
||
videoPlay(attractVideo)
|
||
videoSeek(attractVideo, GAME_LIST[GAME_SELECTED].ATTRACT_START)
|
||
end
|
||
----
|
||
|
||
[#videopause]
|
||
==== videoPause
|
||
|
||
[source,text]
|
||
----
|
||
videoPause(id)
|
||
----
|
||
|
||
Mette in pausa un video caricato. Il suo orologio si ferma, la sua traccia audio va in pausa e `videoDraw` continua a disegnare il fotogramma su cui si è fermato. Mettere in pausa un video già in pausa non fa danni.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<videoplay,videoPlay>>, <<videoisplaying,videoIsPlaying>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Congela la clip della modalità attract mentre è aperta la schermata di calibrazione.
|
||
function calibrationBegin()
|
||
calibrating = true
|
||
if attractVideo then
|
||
videoPause(attractVideo)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#videoplay]
|
||
==== videoPlay
|
||
|
||
[source,text]
|
||
----
|
||
videoPlay(id)
|
||
----
|
||
|
||
Avvia o riprende un video caricato dal suo fotogramma corrente. L'orologio riparte da quel fotogramma, quindi un video ripreso dopo una lunga pausa continua da dove si era fermato invece di saltare avanti, e la traccia audio riprende con lui. Il video avanza solo mentre viene disegnato con `videoDraw` o usato come materiale.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<videopause,videoPause>>, <<videoseek,videoSeek>>, <<videodraw,videoDraw>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Avvia il briefing sopra il disco in pausa.
|
||
function startBriefing()
|
||
discPause()
|
||
videoSeek(briefing, 0)
|
||
videoPlay(briefing)
|
||
briefingRunning = true
|
||
end
|
||
----
|
||
|
||
[#videoquality]
|
||
==== videoQuality
|
||
|
||
[source,text]
|
||
----
|
||
videoQuality(id, smooth)
|
||
----
|
||
|
||
Sceglie il filtro usato quando la forma a quattro argomenti di `videoDraw` scala o ruota il video. Qualsiasi valore diverso da zero seleziona `RENDER_SMOOTH` (bilineare), zero seleziona `RENDER_PIXELATED` (nearest-neighbor), il predefinito. Un cambio segna il fotogramma trasformato da ricostruire al disegno successivo. Non ha effetto sulla forma stirata a cinque argomenti, che usa sempre il nearest-neighbor, né su un video non trasformato.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle del video.
|
||
* `smooth` -- `RENDER_PIXELATED` (`0`) o `RENDER_SMOOTH` (`1`).
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<videoscale,videoScale>>, <<videorotate,videoRotate>>, <<videodraw,videoDraw>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una clip fotografica rimpicciolita in un angolo sta meglio filtrata.
|
||
pipVideo = videoLoad("videos/copilot.mkv")
|
||
videoScale(pipVideo, 0.4)
|
||
videoQuality(pipVideo, RENDER_SMOOTH)
|
||
videoPlay(pipVideo)
|
||
----
|
||
|
||
[#videorotate]
|
||
==== videoRotate
|
||
|
||
[source,text]
|
||
----
|
||
videoRotate(id, degrees)
|
||
----
|
||
|
||
Imposta la rotazione che applica la forma a quattro argomenti di `videoDraw`, in gradi in senso orario, con la stessa convenzione di `spriteRotate`. L'angolo viene ridotto modulo `360` e mantiene il segno, quindi `450` e `90` sono la stessa cosa e `-90` ruota di un quarto di giro in senso antiorario. La scala impostata da `videoScale` viene mantenuta. La rotazione avviene attorno al centro del fotogramma, quindi disegna i video ruotati con `centered` impostato a `true`. Un angolo cambiato segna il fotogramma trasformato da ricostruire al disegno successivo.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle del video.
|
||
* `degrees` -- rotazione in gradi, in senso orario.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<videorotateandscale,videoRotateAndScale>>, <<videoscale,videoScale>>, <<videodraw,videoDraw>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Fai girare lentamente il segnale del radar mentre viene riprodotto.
|
||
function onOverlayUpdate()
|
||
radarAngle = radarAngle + 0.5
|
||
videoRotate(radarVideo, radarAngle)
|
||
videoDraw(radarVideo, RADAR_CENTER_X, RADAR_CENTER_Y, true)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#videorotateandscale]
|
||
==== videoRotateAndScale
|
||
|
||
[source,text]
|
||
----
|
||
videoRotateAndScale(id, degrees, scale)
|
||
videoRotateAndScale(id, degrees, scaleX, scaleY)
|
||
----
|
||
|
||
Imposta rotazione e scala in un'unica chiamata, con le stesse regole di `videoRotate` e `videoScale`: gradi in senso orario ridotti modulo `360`, e fattori di scala che devono essere maggiori di `0`. Usala quando entrambe cambiano a ogni fotogramma, così il fotogramma trasformato viene ricostruito una volta invece di due.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle del video.
|
||
* `degrees` -- rotazione in gradi, in senso orario.
|
||
* `scale` -- fattore di scala uniforme, `1.0` per la dimensione nativa.
|
||
* `scaleX`, `scaleY` -- fattori orizzontale e verticale separati.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<videorotate,videoRotate>>, <<videoscale,videoScale>>, <<videodraw,videoDraw>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una clip ruzzola e si rimpicciolisce mentre viene chiusa.
|
||
function onOverlayUpdate()
|
||
if dismissing then
|
||
dismissTime = dismissTime + 1
|
||
videoRotateAndScale(popupVideo, dismissTime * 6, 1.0 - dismissTime / 60)
|
||
videoDraw(popupVideo, POPUP_CENTER_X, POPUP_CENTER_Y, true)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#videoscale]
|
||
==== videoScale
|
||
|
||
[source,text]
|
||
----
|
||
videoScale(id, scale)
|
||
videoScale(id, scaleX, scaleY)
|
||
----
|
||
|
||
Imposta la scala che applica la forma a quattro argomenti di `videoDraw`, come fattore della dimensione nativa; `1.0` significa nessuna scala. La rotazione impostata da `videoRotate` viene mantenuta. Una scala cambiata segna il fotogramma trasformato da ricostruire al disegno successivo. Fattori di `0` o meno non si possono rappresentare e interrompono il motore al `videoDraw` successivo, quindi limita le scale animate sopra lo zero. La forma stirata a cinque argomenti di `videoDraw` ignora questa impostazione.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle del video.
|
||
* `scale` -- fattore di scala uniforme, maggiore di `0`.
|
||
* `scaleX`, `scaleY` -- fattori orizzontale e verticale separati, ciascuno maggiore di `0`.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<videorotateandscale,videoRotateAndScale>>, <<videoquality,videoQuality>>, <<videogetwidth,videoGetWidth>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Mostra il filmato a un quarto della sua dimensione nell'angolo mentre il gioco continua.
|
||
pipVideo = videoLoad("videos/copilot.mkv")
|
||
videoScale(pipVideo, 0.25)
|
||
videoPlay(pipVideo)
|
||
function onOverlayUpdate()
|
||
videoDraw(pipVideo, PIP_X, PIP_Y, false)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#videoseek]
|
||
==== videoSeek
|
||
|
||
[source,text]
|
||
----
|
||
videoSeek(id, frame)
|
||
----
|
||
|
||
Posiziona un video caricato su `frame` e vi fa ripartire il suo orologio. Lo stato di riproduzione o di pausa non cambia: un video in riproduzione continua dal nuovo fotogramma al disegno successivo, uno in pausa lo mostra. Il numero del fotogramma viene ridotto modulo il numero di fotogrammi, quindi `-1` è l'ultimo fotogramma e un valore oltre la fine ricade dentro la clip.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle del video.
|
||
* `frame` -- fotogramma di destinazione, a base zero.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<videogetframe,videoGetFrame>>, <<videogetframecount,videoGetFrameCount>>, <<videoplay,videoPlay>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ripeti la clip della modalità attract tra i due marcatori di games.dat.
|
||
function onOverlayUpdate()
|
||
videoDraw(attractVideo, VIDEO_X, VIDEO_Y, VIDEO_X + VIDEO_W - 1, VIDEO_Y + VIDEO_H - 1)
|
||
if videoGetFrame(attractVideo) > GAME_LIST[GAME_SELECTED].ATTRACT_END then
|
||
videoSeek(attractVideo, GAME_LIST[GAME_SELECTED].ATTRACT_START)
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#videosetaudiotrack]
|
||
==== videoSetAudioTrack
|
||
|
||
[source,text]
|
||
----
|
||
videoSetAudioTrack(id, track)
|
||
----
|
||
|
||
Passa un video caricato a un'altra delle sue tracce audio. L'audio in coda della vecchia traccia viene scartato e la riproduzione si riallinea, così il cambio si sente quasi subito. Selezionare la traccia già in uso non fa nulla. Una traccia non compresa tra `0` e `videoGetAudioTracks(id) - 1` termina lo script, il che per una clip muta significa qualsiasi valore.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle del video.
|
||
* `track` -- indice della traccia audio, a base zero.
|
||
|
||
*Disponibile da:* 2.10
|
||
*Vedi anche:* <<videogetaudiotrack,videoGetAudioTrack>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videogetlanguage,videoGetLanguage>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Il menu memorizza per ogni gioco una traccia audio per la sua clip di attract.
|
||
attractVideo = videoLoad(GAME_LIST[GAME_SELECTED].ATTRACT)
|
||
local wanted = GAME_LIST[GAME_SELECTED].AUDIO_TRACK
|
||
if wanted and wanted < videoGetAudioTracks(attractVideo) then
|
||
videoSetAudioTrack(attractVideo, wanted)
|
||
end
|
||
videoPlay(attractVideo)
|
||
----
|
||
|
||
[#videosetvolume]
|
||
==== videoSetVolume
|
||
|
||
[source,text]
|
||
----
|
||
videoSetVolume(id, left, right)
|
||
videoSetVolume(volume)
|
||
----
|
||
|
||
Imposta il volume stereo di un video caricato in percentuale. I valori vengono limitati tra `0` e `100` senza protestare. L'impostazione viene ricordata anche per una clip muta e viene riportata da `videoGetVolume`.
|
||
|
||
Chiamata con un solo argomento è un'estensione di Hypseus che imposta invece il volume del laserdisc stesso, sulla scala da `0` a `64` di Hypseus anziché in percentuale: la gemella della <<videogetvolume,`videoGetVolume()`>> senza argomenti. Un livello fuori dall'intervallo da `0` a `64` viene ignorato invece di essere fatale, ed è ciò che fa Hypseus in quel caso.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle del video.
|
||
* `left`, `right` -- volumi dei canali, da `0` (muto) a `100` (pieno).
|
||
* `volume` -- il volume del laserdisc, da `0` (muto) a `64` (pieno).
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<videogetvolume,videoGetVolume>>, <<discaudio,discAudio>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Sfuma l'audio del filmato in meno di mezzo secondo mentre il controllo torna al giocatore.
|
||
function onOverlayUpdate()
|
||
if fadingOut then
|
||
fadeLevel = fadeLevel - 4
|
||
videoSetVolume(cutscene, fadeLevel, fadeLevel)
|
||
if fadeLevel <= 0 then
|
||
fadingOut = false
|
||
videoPause(cutscene)
|
||
end
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#videounload]
|
||
==== videoUnload
|
||
|
||
[source,text]
|
||
----
|
||
videoUnload(id)
|
||
----
|
||
|
||
Ferma il decoder e rilascia tutto ciò che un video caricato trattiene: decoder, traccia audio, fotogrammi in cache e la superficie trasformata. Dopo, l'handle non è più valido e passarlo a qualsiasi funzione `video*` termina lo script, quindi azzera la variabile che lo conteneva. I video ancora caricati quando lo script termina vengono rilasciati dal motore.
|
||
|
||
*Parametri:*
|
||
|
||
* `id` -- handle del video.
|
||
|
||
*Disponibile da:* 2.00
|
||
*Vedi anche:* <<videoload,videoLoad>>, <<videopause,videoPause>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Rilascia le risorse del gioco selezionato prima di caricare il successivo.
|
||
function unloadGameAssets()
|
||
if attractVideo then
|
||
videoUnload(attractVideo)
|
||
attractVideo = nil
|
||
end
|
||
end
|
||
----
|
||
|
||
[#vldp]
|
||
=== VLDP
|
||
|
||
Lo spazio dei nomi `vldp*` è precedente a Singe 2.00 ed esiste per far funzionare gli script più vecchi. `vldpGetHeight` e `vldpGetWidth` sono gli stessi binding di `discGetHeight` e `discGetWidth`, `vldpSetVerbose` non fa nulla, e `vldpGetPixel` è l'unico membro senza un equivalente moderno: legge un pixel del fotogramma corrente del disco in coordinate dell'overlay ed è ancora il modo standard di rilevare i colpi sul video preregistrato. Senza disco restituisce nero (vedi <<withoutadisc,Giochi senza disco>>).
|
||
|
||
WARNING: *Storico.* Nel codice nuovo preferisci <<discgetheight,discGetHeight>> e <<discgetwidth,discGetWidth>>. `vldpGetPixel` resta attuale.
|
||
|
||
Hypseus Singe ha aggiunto altri undici membri, tenuti sotto questo prefisso perché i suoi giochi funzionino senza modifiche: `vldpFocusArea` e `vldpResetFocus` ritagliano l'immagine del disco, `vldpGetScale` e `vldpSetScale` riportano e cambiano il fattore di scala della visualizzazione, `vldpGetRotate` e `vldpSetRotate` fanno lo stesso per la rotazione della presentazione, `vldpGetYUVPixel` legge i campioni grezzi, e `vldpFlash`, `vldpSetBlend`, `vldpSetLuma` e `vldpSetMonochrome` cambiano l'aspetto dell'immagine stessa. Ognuno è indicato più sotto come «Estensione di Hypseus». La scala e la rotazione sono due degli ingressi di xref:Manual.adoc#videorect[Il rettangolo video].
|
||
|
||
I quattro controlli dell'immagine lavorano sul fotogramma decodificato, sui suoi piani di luma e di croma, prima che arrivi allo schermo: `vldpSetBlend` e `vldpSetLuma` vengono applicati uno dopo l'altro mentre ogni fotogramma viene caricato, `vldpSetMonochrome` sostituisce la croma, e `vldpFlash` sostituisce del tutto l'immagine per un fotogramma. Costano un passaggio sul piano della luma per fotogramma mentre sono attivi e niente del tutto mentre sono spenti, lasciano che `vldpGetPixel` e `vldpGetYUVPixel` leggano l'immagine così come è stata decodificata, e con un framefile seguono la riproduzione da un segmento all'altro. Tutti e quattro tornano spenti quando lo script si ricarica (tranne il monocromatico, che torna a ciò che chiedeva `--monochrome`) e nessuno tocca un video disegnato con `videoDraw`.
|
||
|
||
[#vldpflash]
|
||
==== vldpFlash
|
||
|
||
[source,text]
|
||
----
|
||
vldpFlash()
|
||
----
|
||
|
||
Estensione di Hypseus. Mette un'unica immagine bianca sul disco, come lampeggia un vero lettore laserdisc durante un salto. Appare subito e il successivo aggiornamento del disco la toglie di nuovo, quindi dura un fotogramma disegnato, che il disco sia in riproduzione, in pausa o fermo.
|
||
|
||
Il bianco è il valore con cui lampeggia Hypseus, luma al 90 per cento con croma neutra, non bianco puro: è questo, in effetti, il bianco di un segnale video. L'overlay, le GUI e la grafica del bezel vengono disegnati sopra come al solito, quindi un HUD resta leggibile durante il lampo.
|
||
|
||
Senza disco, o con un disco il cui primo fotogramma non è ancora stato decodificato, la chiamata non fa nulla.
|
||
|
||
*Parametri:* nessuno.
|
||
|
||
*Restituisce:* nulla.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<discsearch,discSearch>>, <<vldpsetluma,vldpSetLuma>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Sottolinea un colpo.
|
||
vldpFlash()
|
||
----
|
||
|
||
[#vldpfocusarea]
|
||
==== vldpFocusArea
|
||
|
||
[source,text]
|
||
----
|
||
vldpFocusArea(x, y, width, height)
|
||
vldpFocusArea()
|
||
----
|
||
|
||
Estensione di Hypseus. Mostra solo un rettangolo del fotogramma del disco, dato in pixel del disco, stirato nello spazio che di solito occupa l'intero fotogramma, con la consueta scalatura morbida del renderer; le proporzioni seguono ciò che danno `width` e `height`. Il rettangolo viene tenuto dentro il fotogramma, e uno che ne sta interamente fuori termina lo script. Senza argomenti viene mostrato di nuovo l'intero fotogramma (Hypseus ignora quella chiamata e usa `vldpResetFocus`; Singe accetta entrambe). L'inquadratura è solo un effetto di visualizzazione: `vldpGetPixel`, `vldpGetYUVPixel`, la mappatura del mouse e il disco su un materiale 3D continuano tutti a usare le coordinate dell'immagine non inquadrata, quindi il rilevamento dei colpi su un'immagine ingrandita deve essere scalato da chi chiama. Senza disco la chiamata non fa nulla. Si azzera quando lo script si ricarica.
|
||
|
||
*Parametri:*
|
||
|
||
* `x`, `y` -- angolo in alto a sinistra in pixel del disco.
|
||
* `width`, `height` -- dimensioni del rettangolo in pixel del disco.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<vldpresetfocus,vldpResetFocus>>, <<vldpgetwidth,vldpGetWidth>>, <<vldpgetpixel,vldpGetPixel>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Ingrandisci l'80% centrale dell'immagine durante l'inseguimento.
|
||
local w, h = vldpGetWidth(), vldpGetHeight()
|
||
vldpFocusArea(w * 0.1, h * 0.1, w * 0.8, h * 0.8)
|
||
|
||
function onChaseEnded()
|
||
vldpFocusArea()
|
||
end
|
||
----
|
||
|
||
[#vldpgetheight]
|
||
==== vldpGetHeight
|
||
|
||
[source,text]
|
||
----
|
||
height = vldpGetHeight()
|
||
----
|
||
|
||
Restituisce l'altezza del video del disco in pixel, o l'altezza del canvas senza disco. È lo stesso binding di `discGetHeight`, registrato con il suo nome precedente alla 2.00.
|
||
|
||
*Restituisce:* altezza intera in pixel.
|
||
|
||
*Note:*
|
||
|
||
WARNING: *Alias storico.* Usa <<discgetheight,discGetHeight>> nel codice nuovo.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discgetheight,discGetHeight>>, <<vldpgetwidth,vldpGetWidth>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Uno script 1.x che dimensiona il suo overlay in base al disco.
|
||
overlaySetResolution(vldpGetWidth(), vldpGetHeight())
|
||
centerX = vldpGetWidth() / 2
|
||
centerY = vldpGetHeight() / 2
|
||
----
|
||
|
||
[#vldpgetpixel]
|
||
==== vldpGetPixel
|
||
|
||
[source,text]
|
||
----
|
||
r, g, b = vldpGetPixel(x, y)
|
||
----
|
||
|
||
Legge il colore di un pixel del fotogramma del disco attualmente sullo schermo. Le coordinate sono coordinate dell'overlay: il motore le divide per la scala tra overlay e video e le tronca, così passi gli stessi numeri con cui disegni. Restituisce `0, 0, 0` quando non c'è un disco, quando non è ancora stato decodificato nessun fotogramma o quando le coordinate scalate cadono fuori dal video. I giochi che la usano di solito marcano i bersagli nel filmato sorgente con un colore distintivo e, quando si preme il grilletto, controllano il pixel sotto il mirino invece di creare delle hitbox per ogni fotogramma.
|
||
|
||
*Parametri:*
|
||
|
||
* `x`, `y` -- coordinate dell'overlay; sono accettati valori frazionari.
|
||
|
||
*Restituisce:* tre interi, rosso, verde e blu, ciascuno da `0` a `255`.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discgetframe,discGetFrame>>, <<mousegetposition,mouseGetPosition>>, <<overlaysetresolution,overlaySetResolution>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Controllo del colpo della pistola ottica: i bersagli sono dipinti in magenta puro nei fotogrammi con i nemici.
|
||
function onInputPressed(what)
|
||
if what == SWITCH_BUTTON3 then
|
||
local x, y = mouseGetPosition(0)
|
||
local r, g, b = vldpGetPixel(x, y)
|
||
if r > 200 and g < 50 and b > 200 then
|
||
scoreHit()
|
||
else
|
||
scoreMiss()
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
[#vldpgetrotate]
|
||
==== vldpGetRotate
|
||
|
||
[source,text]
|
||
----
|
||
degrees = vldpGetRotate()
|
||
----
|
||
|
||
Estensione di Hypseus. Restituisce la rotazione della presentazione in gradi in senso orario: l'opzione `--rotate`, o ciò che `vldpSetRotate` ha impostato per ultima, e `0` quando nessuna delle due l'ha fatto. Vengono restituiti solo `0`, `90`, `180` e `270`, perché sono gli unici angoli a cui Singe gira.
|
||
|
||
*Restituisce:* intero, `0`, `90`, `180` o `270`.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<vldpsetrotate,vldpSetRotate>>, <<vldpgetscale,vldpGetScale>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un cabinato verticale vuole il pannello dei punteggi lungo il lato lungo.
|
||
if vldpGetRotate() == 90 or vldpGetRotate() == 270 then
|
||
layoutPanel(PANEL_TALL)
|
||
else
|
||
layoutPanel(PANEL_WIDE)
|
||
end
|
||
----
|
||
|
||
[#vldpgetscale]
|
||
==== vldpGetScale
|
||
|
||
[source,text]
|
||
----
|
||
scale = vldpGetScale()
|
||
----
|
||
|
||
Estensione di Hypseus. Restituisce il fattore di scala della visualizzazione in percentuale: l'opzione `--scalefactor`, o ciò che `vldpSetScale` ha applicato per ultima, e `100` quando nessuna delle due è stata data. I giochi di Hypseus dividono `100` per questo valore per adattare la geometria del loro overlay all'immagine rimpicciolita. Non ha niente a che fare con `vldpFocusArea` né con la risoluzione dell'overlay.
|
||
|
||
*Restituisce:* intero, da `25` a `100`.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<vldpsetscale,vldpSetScale>>, <<vldpfocusarea,vldpFocusArea>>, <<overlaygetwidth,overlayGetWidth>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Codice di un gioco Hypseus: i mirini della pistola scalano con l'immagine.
|
||
if vldpGetScale ~= nil then
|
||
gunScale = 100 / vldpGetScale()
|
||
else
|
||
gunScale = 1
|
||
end
|
||
----
|
||
|
||
[#vldpgetshift]
|
||
==== vldpGetShift
|
||
|
||
[source,text]
|
||
----
|
||
x, y = vldpGetShift()
|
||
----
|
||
|
||
Lo scostamento dell'immagine dentro la finestra in percentuale, le opzioni
|
||
`--shiftx` e `--shifty` o ciò che <<vldpsetshift,vldpSetShift>> ha applicato
|
||
per ultima, e `0, 0` quando nessuna delle due è stata data. All'estremo uno
|
||
scostamento riempie esattamente lo spazio lasciato da `--scalefactor`, quindi
|
||
`100` con la scala a `100` non sposta nulla.
|
||
|
||
*Restituisce:* due interi, ciascuno da `-100` a `100`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vldpsetshift,vldpSetShift>>, <<vldpgetscale,vldpGetScale>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Riporta dove il gestore ha messo l'immagine.
|
||
local x, y = vldpGetShift()
|
||
fontPrint(10, 10, string.format("shift %d, %d", x, y))
|
||
----
|
||
|
||
[#vldpgetwidth]
|
||
==== vldpGetWidth
|
||
|
||
[source,text]
|
||
----
|
||
width = vldpGetWidth()
|
||
----
|
||
|
||
Restituisce la larghezza del video del disco in pixel, o la larghezza del canvas senza disco. È lo stesso binding di `discGetWidth`, registrato con il suo nome precedente alla 2.00.
|
||
|
||
*Restituisce:* larghezza intera in pixel.
|
||
|
||
*Note:*
|
||
|
||
WARNING: *Alias storico.* Usa <<discgetwidth,discGetWidth>> nel codice nuovo.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<discgetwidth,discGetWidth>>, <<vldpgetheight,vldpGetHeight>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Uno script 1.x che dimensiona il suo overlay in base al disco.
|
||
overlaySetResolution(vldpGetWidth(), vldpGetHeight())
|
||
centerX = vldpGetWidth() / 2
|
||
centerY = vldpGetHeight() / 2
|
||
----
|
||
|
||
[#vldpgetyuvpixel]
|
||
==== vldpGetYUVPixel
|
||
|
||
[source,text]
|
||
----
|
||
y, u, v = vldpGetYUVPixel(x, y)
|
||
----
|
||
|
||
Estensione di Hypseus. Legge i campioni grezzi di un pixel del fotogramma del disco attualmente mostrato, prima di qualsiasi conversione del colore: la luma e i due campioni di croma, ciascuno da `0` a `255`, con la croma alla risoluzione 4:2:0 dell'immagine decodificata (il neutro è `128`). Le coordinate sono coordinate dell'overlay, scalate e troncate esattamente come fa `vldpGetPixel`. Restituisce `-1, -1, -1` quando non c'è un disco, quando non è ancora stato decodificato nessun fotogramma o quando le coordinate scalate cadono fuori dal video. Non risente di `vldpFocusArea` né di `vldpSetMonochrome`. Costa meno di `vldpGetPixel` quando conta solo la luminosità.
|
||
|
||
*Parametri:*
|
||
|
||
* `x`, `y` -- coordinate dell'overlay; sono accettati valori frazionari.
|
||
|
||
*Restituisce:* tre interi, `Y`, `U` e `V`, ciascuno da `0` a `255`, oppure `-1, -1, -1`.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<vldpgetpixel,vldpGetPixel>>, <<discgetframe,discGetFrame>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- La strada è scura; qualunque cosa chiara sotto l'auto è un ostacolo.
|
||
function carHitsSomething()
|
||
local y = vldpGetYUVPixel(carX + carWidth / 2, carY + carHeight)
|
||
return y >= 120
|
||
end
|
||
----
|
||
|
||
[#vldpresetfocus]
|
||
==== vldpResetFocus
|
||
|
||
[source,text]
|
||
----
|
||
vldpResetFocus()
|
||
----
|
||
|
||
Estensione di Hypseus. Mostra di nuovo l'intero fotogramma del disco dopo `vldpFocusArea`, esattamente come fa `vldpFocusArea()` senza argomenti. È il nome che Hypseus stesso dà all'azzeramento, mantenuto perché i suoi giochi funzionino senza modifiche.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<vldpfocusarea,vldpFocusArea>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onChaseEnded()
|
||
vldpResetFocus()
|
||
end
|
||
----
|
||
|
||
[#vldpsetblend]
|
||
==== vldpSetBlend
|
||
|
||
[source,text]
|
||
----
|
||
vldpSetBlend(enabled)
|
||
----
|
||
|
||
Estensione di Hypseus. Ammorbidisce l'immagine del disco da una riga all'altra: ogni campione di luma diventa la media di sé stesso e dei campioni immediatamente sopra e sotto. È il filtro di fusione a tre tap di Hypseus, ed esiste per togliere gli spigoli duri a un riversamento interlacciato senza ammorbidire l'immagine in orizzontale.
|
||
|
||
I piani di croma restano come sono, quindi l'ammorbidimento cambia il dettaglio della luminosità e non il colore. Spegnerlo rimette l'immagine esattamente com'era stata decodificata.
|
||
|
||
Senza disco la chiamata non fa nulla ma viene comunque ricordata, quindi ha effetto se un disco viene caricato più tardi. Un argomento non booleano termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
|
||
*Restituisce:* nulla.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<vldpsetluma,vldpSetLuma>>, <<vldpsetmonochrome,vldpSetMonochrome>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un interruttore della schermata di servizio per un riversamento rumoroso.
|
||
vldpSetBlend(SETTINGS.softenVideo)
|
||
----
|
||
|
||
[#vldpsetluma]
|
||
==== vldpSetLuma
|
||
|
||
[source,text]
|
||
----
|
||
vldpSetLuma(enabled [, level])
|
||
----
|
||
|
||
Estensione di Hypseus. Schiarisce o scurisce l'immagine del disco. `level` va da 0 a 8 e 4, il predefinito, non cambia nulla: la distanza da 4 viene aggiunta a ogni campione di luma in ottavi, quindi 0 dimezza la luminosità, 8 la aumenta della metà, e i passi intermedi sono uniformi. Un livello fuori dall'intervallo da 0 a 8 termina lo script.
|
||
|
||
Dato che cambia solo la luma, il colore resta dov'è: alzare il livello schiarisce l'immagine senza sbiadire il colore, che è ciò che fa il controllo dell'immagine di un lettore vero e ciò che un moltiplicatore del colore non farebbe. Spegnerlo, o chiedere il livello 4, rimette l'immagine esattamente com'era stata decodificata.
|
||
|
||
`vldpSetLuma(true)` senza un livello quindi non fa nulla di visibile, ed è anche il comportamento di Hypseus.
|
||
|
||
Senza disco la chiamata non fa nulla ma viene comunque ricordata, quindi ha effetto se un disco viene caricato più tardi.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
* `level` -- numero facoltativo; da 0 a 8, con 4 neutro. Predefinito 4.
|
||
|
||
*Restituisce:* nulla.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<vldpsetblend,vldpSetBlend>>, <<vldpsetmonochrome,vldpSetMonochrome>>, <<vldpflash,vldpFlash>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una scena notturna che il giocatore non riesce a vedere; lascia che la schiarisca.
|
||
vldpSetLuma(true, 6)
|
||
----
|
||
|
||
[#vldpsetmonochrome]
|
||
==== vldpSetMonochrome
|
||
|
||
[source,text]
|
||
----
|
||
vldpSetMonochrome(enabled)
|
||
----
|
||
|
||
Estensione di Hypseus. Mostra il disco solo in luma: i piani di croma vengono sostituiti da campioni neutri mentre ogni fotogramma viene caricato, quindi un disco in pausa cambia subito. Con un framefile l'impostazione segue la riproduzione da un segmento all'altro. `vldpGetPixel` e `vldpGetYUVPixel` continuano a restituire i campioni del colore, e i video aggiuntivi disegnati con `videoDraw` non ne risentono. Abbinala a `overlaySetMonochrome` per una modalità in bianco e nero. Senza disco la chiamata non fa nulla; si azzera quando lo script si ricarica. Un argomento non booleano termina lo script.
|
||
|
||
*Parametri:*
|
||
|
||
* `enabled` -- booleano.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<overlaysetmonochrome,overlaySetMonochrome>>, <<vldpgetpixel,vldpGetPixel>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Un dip switch sceglie tra colore e bianco e nero.
|
||
vldpSetMonochrome(not dipChroma)
|
||
overlaySetMonochrome(not dipChroma)
|
||
----
|
||
|
||
[#vldpsetrotate]
|
||
==== vldpSetRotate
|
||
|
||
[source,text]
|
||
----
|
||
applied = vldpSetRotate(degrees)
|
||
----
|
||
|
||
Estensione di Hypseus. Gira in senso orario l'intera presentazione: il disco, l'overlay, le GUI, la scena 3D e le particelle girano tutti insieme, e le posizioni del mouse e della pistola ottica vengono girate all'indietro così che un clic arrivi ancora dove è disegnato. Un quarto di giro scambia la forma dell'area presentata, così una finestra verticale viene riempita invece di ridurre l'immagine a una striscia tra bande nere.
|
||
|
||
Hypseus accetta qualsiasi angolo da `0` a `359`; Singe accetta solo `0`, `90`, `180` e `270`, e per qualsiasi altro risponde `false` invece di terminare lo script. Un angolo arbitrario lascerebbe la mappatura del mouse e il bordo Sinden senza un significato sensato, e nessun gioco della libreria di Hypseus ne chiede uno.
|
||
|
||
La rotazione costa un render target delle dimensioni dello schermo e un blit in più per fotogramma mentre non è zero, e niente del tutto quando lo è. Torna al valore di `--rotate` quando lo script si ricarica.
|
||
|
||
*Parametri:*
|
||
|
||
* `degrees` -- intero, `0`, `90`, `180` o `270`.
|
||
|
||
*Restituisce:* booleano, vero quando la rotazione è stata applicata.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<vldpgetrotate,vldpGetRotate>>, <<vldpsetscale,vldpSetScale>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una sezione da sparatutto verticale su un monitor girevole.
|
||
if not vldpSetRotate(90) then
|
||
debugPrint("This build will not turn the screen.")
|
||
end
|
||
----
|
||
|
||
[#vldpsetscale]
|
||
==== vldpSetScale
|
||
|
||
[source,text]
|
||
----
|
||
applied = vldpSetScale(percent)
|
||
----
|
||
|
||
Estensione di Hypseus. Rimpicciolisce l'immagine attorno al suo centro fino a `percent` della finestra, l'equivalente a gioco in corso di `--scalefactor`, e ricostruisce il rettangolo video così che il disco, l'overlay, le GUI, la scena 3D, le particelle, un eventuale bordo Sinden e la mappatura del mouse lo seguano tutti. I valori fuori dall'intervallo da `25` a `100` rispondono `false` e non cambiano nulla, invece di terminare lo script.
|
||
|
||
I cambi sono limitati a uno ogni 15 millisecondi, come Hypseus limita il proprio zoom a tasto premuto; una chiamata dentro quell'intervallo risponde `false`, quindi uno script che guida la scala da un tasto tenuto premuto deve continuare a chiamarla finché non ha effetto. Torna al valore di `--scalefactor` quando lo script si ricarica.
|
||
|
||
*Parametri:*
|
||
|
||
* `percent` -- intero, da `25` a `100`.
|
||
|
||
*Restituisce:* booleano, vero quando la scala è stata applicata.
|
||
|
||
*Disponibile da:* 3.00, come estensione di Hypseus Singe.
|
||
*Vedi anche:* <<vldpgetscale,vldpGetScale>>, <<vldpsetrotate,vldpSetRotate>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Rimpicciolisci di un passo ogni volta che il giocatore preme il tasto di regolazione.
|
||
function onInputPressed(input)
|
||
if input == SWITCH_BUTTON3 then
|
||
vldpSetScale(vldpGetScale() - 5)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#vldpsetshift]
|
||
==== vldpSetShift
|
||
|
||
[source,text]
|
||
----
|
||
vldpSetShift(x, y)
|
||
----
|
||
|
||
Sposta l'immagine dentro la finestra, l'equivalente a gioco in corso di
|
||
`--shiftx` e `--shifty`, e ricostruisce il rettangolo video così che il disco,
|
||
l'overlay, le GUI, la scena 3D, le particelle, un eventuale bordo Sinden e la
|
||
mappatura del mouse lo seguano tutti. I valori sono percentuali dello spazio
|
||
lasciato da `--scalefactor`, quindi con la scala a `100` non c'è dove
|
||
spostarsi. Un valore fuori dall'intervallo da `-100` a `100` termina lo script,
|
||
a differenza di <<vldpsetscale,vldpSetScale>>, che risponde `false`.
|
||
|
||
*Parametri:*
|
||
|
||
* `x` -- intero, da `-100` a `100`.
|
||
* `y` -- intero, da `-100` a `100`.
|
||
|
||
*Restituisce:* nulla.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<vldpgetshift,vldpGetShift>>, <<vldpsetscale,vldpSetScale>>, <<singesavegeometry,singeSaveGeometry>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Sposta un po' l'immagine a sinistra ogni volta che il gestore preme il tasto.
|
||
function onInputPressed(input)
|
||
if input == SWITCH_LEFT then
|
||
local x, y = vldpGetShift()
|
||
vldpSetShift(math.max(-100, x - 1), y)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#vldpsetverbose]
|
||
==== vldpSetVerbose
|
||
|
||
[source,text]
|
||
----
|
||
vldpSetVerbose(...)
|
||
----
|
||
|
||
Non fa nulla. Un tempo attivava e disattivava il log nel lettore video originale; la chiamata accetta qualsiasi argomento, scrive una riga nella traccia e ritorna. La traccia del motore e quella dello script ora sono opzioni da riga di comando (`--program` e `--trace`).
|
||
|
||
*Note:*
|
||
|
||
WARNING: *Non implementata.* Mantenuta solo per compatibilità con le versioni precedenti. Usa `--trace` sulla riga di comando per registrare le chiamate all'API.
|
||
|
||
*Disponibile da:* 1.x
|
||
*Vedi anche:* <<debugprint,debugPrint>>, <<discgetstate,discGetState>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Codice di avvio portato da 1.x. La chiamata viene ignorata; avvia invece Singe con --trace.
|
||
vldpSetVerbose(true)
|
||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||
discSearch(TITLE_FRAME)
|
||
----
|
||
|
||
[#voice]
|
||
=== Voce
|
||
|
||
I pezzi con cui si costruisce una chat vocale, senza la rete: Opus per rendere
|
||
una voce abbastanza piccola da inviarla e per riportarla com'era, un processore
|
||
che ripulisce un microfono prima che venga inviato, e un jitter buffer per
|
||
riprodurre nell'ordine giusto ciò che arriva in ritardo e fuori ordine. Inviare
|
||
i pacchetti e trovare gli altri giocatori sono affari del gioco stesso, o di una
|
||
versione successiva di Singe.
|
||
|
||
I campioni sono stringhe di valori a 16 bit, due byte ciascuno, little endian,
|
||
come li dà <<micread,`micRead`>>; con due canali si alternano sinistro e
|
||
destro. Un *frame* è una fetta fissa di tempo: 20 millisecondi, 960 campioni a
|
||
48000, è quella abituale per una voce.
|
||
|
||
Ogni oggetto qui è un handle. <<voicefree,`voiceFree`>> ne rilascia uno;
|
||
vengono rilasciati tutti quando il gioco termina.
|
||
|
||
.Un microfono, ripulito, codificato e decodificato di nuovo
|
||
[source,lua]
|
||
----
|
||
micOpen()
|
||
local processor = voiceProcessorNew()
|
||
local encoder = opusEncoderNew()
|
||
local decoder = opusDecoderNew()
|
||
local pending = ""
|
||
|
||
function onOverlayUpdate()
|
||
pending = pending .. micRead()
|
||
while #pending >= 960 * 2 do
|
||
local frame = pending:sub(1, 960 * 2)
|
||
pending = pending:sub(960 * 2 + 1)
|
||
local cleaned, voiced = voiceProcess(processor, frame)
|
||
if voiced then
|
||
local packet = opusEncode(encoder, cleaned) -- questo è ciò che invii
|
||
local heard = opusDecode(decoder, packet) -- e questo è ciò che riproduce l'altro capo
|
||
end
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#jitterget]
|
||
==== jitterGet
|
||
|
||
[source,text]
|
||
----
|
||
packet, status = jitterGet(jitter [, span])
|
||
----
|
||
|
||
Il pacchetto da riprodurre adesso, e il buffer avanzato di un passo. Chiamala
|
||
una volta per ogni frame di tempo, qualunque cosa sia arrivata.
|
||
|
||
*Parametri:*
|
||
|
||
* `span` -- numero, i campioni da prendere; il passo, se non è indicato.
|
||
|
||
*Restituisce:* il pacchetto, o `nil`, e uno stato: `"ok"`, `"missing"` (non c'è
|
||
niente in scadenza: passa `nil` a <<opusdecode,`opusDecode`>> per coprire il
|
||
buco) oppure `"insertion"` (il buffer sta crescendo per assorbire i pacchetti in
|
||
ritardo: riproduci un po' di silenzio).
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#jitternew]
|
||
==== jitterNew
|
||
|
||
[source,text]
|
||
----
|
||
jitter = jitterNew([step])
|
||
----
|
||
|
||
Un jitter buffer: i pacchetti entrano per marca temporale man mano che arrivano,
|
||
ed escono in ordine, un passo alla volta, un po' indietro così che ce la faccia
|
||
anche uno arrivato tardi.
|
||
|
||
*Parametri:*
|
||
|
||
* `step` -- numero, i campioni di cui avanza ogni <<jitterget,`jitterGet`>>;
|
||
`960` se non indicato.
|
||
|
||
*Restituisce:* l'handle del jitter buffer.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#jitterput]
|
||
==== jitterPut
|
||
|
||
[source,text]
|
||
----
|
||
jitterPut(jitter, packet, timestamp [, span])
|
||
----
|
||
|
||
Un pacchetto che è arrivato. Il buffer ne tiene una copia propria.
|
||
|
||
*Parametri:*
|
||
|
||
* `packet` -- stringa, da 1 a 4000 byte.
|
||
* `timestamp` -- numero, dove inizia il pacchetto, in campioni: li conta il
|
||
mittente, `0`, `960`, `1920` e così via.
|
||
* `span` -- numero, i campioni che il pacchetto copre; il passo se non indicato.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#opusdecode]
|
||
==== opusDecode
|
||
|
||
[source,text]
|
||
----
|
||
samples = opusDecode(decoder, packet | nil)
|
||
----
|
||
|
||
Un pacchetto riportato a campioni. `nil` per un pacchetto mai arrivato chiede a
|
||
Opus di coprire il buco con qualcosa di plausibile, lungo quanto l'ultimo
|
||
pacchetto.
|
||
|
||
*Restituisce:* i campioni.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#opusdecodernew]
|
||
==== opusDecoderNew
|
||
|
||
[source,text]
|
||
----
|
||
decoder = opusDecoderNew([rate [, channels]])
|
||
----
|
||
|
||
*Parametri:*
|
||
|
||
* `rate` -- numero: 8000, 12000, 16000, 24000 o 48000; `48000` se non indicato.
|
||
* `channels` -- numero, `1` o `2`; `1` se non indicato.
|
||
|
||
*Restituisce:* l'handle del decoder.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#opusencode]
|
||
==== opusEncode
|
||
|
||
[source,text]
|
||
----
|
||
packet = opusEncode(encoder, samples)
|
||
----
|
||
|
||
Un frame di campioni come un unico pacchetto. Un frame dura 2,5, 5, 10, 20, 40
|
||
o 60 millisecondi di campioni; qualsiasi altra durata viene rifiutata.
|
||
|
||
*Restituisce:* il pacchetto, una stringa.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#opusencodernew]
|
||
==== opusEncoderNew
|
||
|
||
[source,text]
|
||
----
|
||
encoder = opusEncoderNew([rate [, channels [, application]]])
|
||
----
|
||
|
||
*Parametri:*
|
||
|
||
* `rate` -- numero: 8000, 12000, 16000, 24000 o 48000; `48000` se non indicato.
|
||
* `channels` -- numero, `1` o `2`; `1` se non indicato.
|
||
* `application` -- stringa: `"voip"` per il parlato, regolato perché sia capito;
|
||
`"audio"` per la musica; `"lowdelay"` per il minimo ritardo al prezzo di un po'
|
||
di qualità. `"voip"` se non indicato.
|
||
|
||
*Restituisce:* l'handle dell'encoder.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#opusencoderset]
|
||
==== opusEncoderSet
|
||
|
||
[source,text]
|
||
----
|
||
opusEncoderSet(encoder, bitrate [, complexity])
|
||
----
|
||
|
||
*Parametri:*
|
||
|
||
* `bitrate` -- numero, bit al secondo; `0` lo lascia com'è. Una voce è chiara
|
||
tra 16000 e 24000.
|
||
* `complexity` -- numero, da `0` a `10`: quanto lavoro spendere per la qualità.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#voicefree]
|
||
==== voiceFree
|
||
|
||
[source,text]
|
||
----
|
||
voiceFree(handle)
|
||
----
|
||
|
||
Rilascia un encoder o un decoder Opus, un processore della voce o un jitter buffer.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#voiceprocess]
|
||
==== voiceProcess
|
||
|
||
[source,text]
|
||
----
|
||
cleaned, voiced = voiceProcess(processor, mic [, speaker])
|
||
----
|
||
|
||
Un frame del microfono, ripulito: tolta l'eco di ciò che stavano suonando gli
|
||
altoparlanti, soppresso il rumore di fondo e livellato il volume.
|
||
|
||
*Parametri:*
|
||
|
||
* `mic` -- stringa, un frame di campioni dal microfono.
|
||
* `speaker` -- stringa, lo stesso frame di ciò che hanno suonato gli
|
||
altoparlanti, se c'è: le voci degli altri giocatori, per esempio. È la sua eco
|
||
a essere tolta.
|
||
|
||
*Restituisce:* il frame ripulito, e `true` quando sembra una voce -- il modo
|
||
per non inviare nulla mentre nessuno parla.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#voiceprocessornew]
|
||
==== voiceProcessorNew
|
||
|
||
[source,text]
|
||
----
|
||
processor = voiceProcessorNew([frame [, rate [, tailMs]]])
|
||
----
|
||
|
||
*Parametri:*
|
||
|
||
* `frame` -- numero, campioni per chiamata; `960` se non indicato.
|
||
* `rate` -- numero; `48000` se non indicato.
|
||
* `tailMs` -- numero, l'eco più lunga da cancellare, in millisecondi: quanto a
|
||
lungo un suono risuona nella stanza. `200` se non indicato.
|
||
|
||
*Restituisce:* l'handle del processore.
|
||
|
||
*Disponibile da:* 3.00.
|
||
|
||
[#frameworkhelpers]
|
||
=== Funzioni di utilità del framework
|
||
|
||
`Framework.singe` definisce cinque piccole funzioni di utilità sue, globali come
|
||
il resto dell'API, che usano i giochi scritti per le versioni precedenti. Il
|
||
codice nuovo può preferire `lume` e `inspect`, tra
|
||
<<_included_libraries,le librerie incluse>>.
|
||
|
||
[cols="1,2",options="header"]
|
||
|===
|
||
| Funzione | Che cosa fa
|
||
| `utilDeepCopy(value)` | Una copia di `value` fino in fondo: una tabella è copiata chiave per chiave, e le sue chiavi, i suoi valori e la sua metatabella sono copiati a loro volta; tutto il resto torna così com'è.
|
||
| `utilDump(value)` | `value` come testo da stampare. Una tabella diventa `{ ["key"] = value,}`, le tabelle annidate tra parentesi graffe proprie; tutto il resto passa da `tostring`. Per una tabella che valga la pena di leggere, `require("inspect")` fa di meglio.
|
||
| `utilGetTableSize(t)` | Quante chiavi contiene `t`, tutte, non solo la sequenza che misura `#t`.
|
||
| `utilOffsetTable(source, offset)` | Una copia profonda di una tabella con la forma di `SCANCODE`, `{ NAME = { name = NAME, value = N } }`, con `offset` aggiunto a ogni `value`.
|
||
| `utilTrim(s)` | `s` senza gli spazi bianchi alle due estremità.
|
||
|===
|
||
|
||
[#enginecallbacks]
|
||
=== Callback del motore
|
||
|
||
Sono funzioni che lo script definisce e che Singe chiama quando avviene l'evento corrispondente; sono tutte facoltative, e una non definita viene saltata. Le callback di input scattano man mano che gli eventi vengono letti, `onOverlayUpdate` gira al massimo una volta ogni 15 millisecondi, e nessuna di esse gira mentre il tasto di pausa tiene il gioco congelato (vedi <<pausing,La pausa>>). Un errore dentro una callback termina il gioco con un traceback. Nel modello a thread `singeMain` prende il posto della maggior parte della logica per fotogramma (vedi <<threaded,A thread>>): le altre callback scattano comunque, ma `Framework.singe` installa un proprio `onOverlayUpdate` per far avanzare la coroutine `singeMain`, quindi un gioco a thread definisce `singeMain`, `onShutdown` e le callback di input che vuole.
|
||
|
||
[#oncontrollermoved]
|
||
==== onControllerMoved
|
||
|
||
[source,text]
|
||
----
|
||
function onControllerMoved(axis, value, which)
|
||
end
|
||
----
|
||
|
||
Chiamata per ogni evento di movimento di un asse da un controller collegato, compreso il movimento dentro la zona morta, quindi una levetta sotto un pollice scatta molte volte al secondo. Le direzioni oltre `SINGE_DEAD_ZONE` funzionano anche da interruttori quando `controls.cfg` le assegna, quindi un gioco a cui basta una sterzata digitale può assegnare `GAMEPAD_0.AXIS_LEFT_X_L` e `GAMEPAD_0.AXIS_LEFT_X_R` e ignorare questa callback. Il valore viene memorizzato per `controllerGetAxis` prima che la callback giri.
|
||
|
||
*Parametri:*
|
||
|
||
* `axis` -- da `0` a `5`, corrispondente alle costanti `GAMEPAD_AXIS_*`.
|
||
* `value` -- posizione grezza, da `-32768` a `32767`; i grilletti a riposo stanno a `0`.
|
||
* `which` -- indice del controller, da `0` a `3`, lo stesso indice che usano `controllerGetAxis` e le tabelle `GAMEPAD_N`.
|
||
|
||
*Vedi anche:* <<controllergetaxis,controllerGetAxis>>, <<controllergetbutton,controllerGetButton>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Sterzata analogica solo dalla levetta sinistra del primo controller.
|
||
function onControllerMoved(axis, value, which)
|
||
if which ~= 0 or axis ~= GAMEPAD_AXIS_LEFT_X then
|
||
return
|
||
end
|
||
if math.abs(value) < SINGE_DEAD_ZONE then
|
||
steering = 0
|
||
else
|
||
steering = value / 32768
|
||
end
|
||
end
|
||
----
|
||
|
||
[#oninputpressedoninputreleased]
|
||
==== onInputPressed / onInputReleased
|
||
|
||
[source,text]
|
||
----
|
||
function onInputPressed(what)
|
||
end
|
||
|
||
function onInputReleased(what)
|
||
end
|
||
----
|
||
|
||
Chiamate quando un input logico viene premuto o rilasciato. Ciò che arriva dipende dalla modalità della tastiera. In `MODE_NORMAL`, `what` è un valore `SWITCH_*`: tasti, pulsanti dei controller, direzioni degli assi oltre la zona morta, pulsanti del mouse e rotella del mouse arrivano tutti così, tradotti tramite `controls.cfg`, ogni pressione una volta sola senza ripetizione dei tasti, e gli input non assegnati a nessun interruttore vengono ignorati. `SWITCH_PAUSE` arriva solo dopo `singeSetPauseKeyEnabled(false)`; altrimenti il motore congela il gioco su quel tasto e lo script non lo vede mai. In `MODE_FULL`, `what` è il keysym del tasto (il valore del carattere, non lo scancode), le ripetizioni vengono consegnate, e i pulsanti dei controller e del mouse passano `0`; ogni evento arriva anche a `onKeyPressed` e `onKeyReleased`, che portano lo scancode o il codice del pulsante. Quando il tasto di pausa congela il gioco, ogni interruttore che lo script tiene premuto viene prima rilasciato tramite `onInputReleased`, e allo scongelamento ciò che è ancora fisicamente premuto viene premuto di nuovo, quindi un gioco non vede mai un pulsante in uno stato non aggiornato. Le pressioni entro il primo secondo dall'avvio dello script, o da quando la finestra riprende il focus, sono trattate come rimaste da prima e vengono ingoiate insieme al loro rilascio. Un interruttore non arriva da nessun controllo: `SWITCH_MOUSE_DISCONNECT` viene generato dal motore quando un mouse o una pistola ottica viene scollegato, una volta come pressione e subito dopo una volta come rilascio, così un gioco con pistola può accorgersi che la sua pistola se ne va (vedi xref:Manual.adoc#mousedevices[Mouse, pistole e chi sceglie]).
|
||
|
||
*Vedi anche:* <<keyboardsetmode,keyboardSetMode>>, <<onkeypressedonkeyreleased,onKeyPressed>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<controllergetbutton,controllerGetButton>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onInputPressed(what)
|
||
if what == SWITCH_COIN1 then
|
||
credits = credits + 1
|
||
soundPlay(coinClip)
|
||
elseif what == SWITCH_START1 and credits > 0 then
|
||
startGame()
|
||
elseif what == SWITCH_BUTTON3 then
|
||
fireShot(cursorX, cursorY)
|
||
elseif what == SWITCH_QUIT then
|
||
singeQuit()
|
||
end
|
||
end
|
||
|
||
function onInputReleased(what)
|
||
if what == SWITCH_BUTTON1 then
|
||
thrusting = false
|
||
end
|
||
end
|
||
----
|
||
|
||
[#onkeypressedonkeyreleased]
|
||
==== onKeyPressed / onKeyReleased
|
||
|
||
[source,text]
|
||
----
|
||
function onKeyPressed(keysym, scancode)
|
||
end
|
||
|
||
function onKeyReleased(keysym, scancode)
|
||
end
|
||
----
|
||
|
||
Chiamate solo in `MODE_FULL`, per ogni tasto che viene premuto o rilasciato, con sia il keysym logico (il carattere che il tasto produce con il layout corrente) sia lo scancode fisico (la posizione del tasto, da confrontare con la tabella `SCANCODE`). In questa modalità i tasti tenuti premuti si ripetono, ed è ciò che serve per inserire del testo. Arrivano anche i pulsanti dei controller e del mouse, con `keysym` `0` e il loro codice `GAMEPAD_N` o `MOUSE_N` come `scancode`. Per il testo usa `keysym`; per i controlli in cui un tasto fa da pulsante usa `scancode`, così l'assegnazione è la stessa con ogni layout. Le assegnazioni da tastiera degli interruttori del motore non agiscono in questa modalità, quindi un gioco in `MODE_FULL` deve offrire un proprio modo per uscire.
|
||
|
||
*Vedi anche:* <<keyboardsetmode,keyboardSetMode>>, <<keyboardgetmodifiers,keyboardGetModifiers>>, <<oninputpressedoninputreleased,onInputPressed>>
|
||
|
||
[#ontextinput]
|
||
==== onTextInput
|
||
|
||
[source,text]
|
||
----
|
||
function onTextInput(text)
|
||
end
|
||
----
|
||
|
||
Chiamata solo in `MODE_FULL`, con il testo prodotto dalla pressione di un tasto -- un carattere o alcuni, in UTF-8 -- dopo che sono stati applicati lo stato di Maiusc e il layout della tastiera, così una maiuscola, una virgoletta o una lettera di un layout non inglese arriva come l'ha digitata la persona. `onKeyPressed` dà la stessa pressione come keysym senza Maiusc e scancode, ed è quella da usare per i tasti che sono comandi; questa è quella da usare per i tasti che sono testo. Una GUI con un campo di testo che ha il focus prende il testo per prima, e lo script non ne sente nulla.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<onkeypressedonkeyreleased,onKeyPressed>>, <<keyboardsetmode,keyboardSetMode>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Iniziali del punteggio record: le lettere come testo; Invio e Backspace per posizione.
|
||
function onTextInput(text)
|
||
if text:match("^%a$") and #initials < 3 then
|
||
initials = initials .. text:upper()
|
||
end
|
||
end
|
||
|
||
function onKeyPressed(keysym, scancode)
|
||
if scancode == SCANCODE.RETURN.value then
|
||
finishInitials()
|
||
elseif scancode == SCANCODE.BACKSPACE.value then
|
||
initials = initials:sub(1, -2)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#onmidimessage]
|
||
==== onMidiMessage
|
||
|
||
[source,text]
|
||
----
|
||
function onMidiMessage(status, data1, data2, bytes)
|
||
end
|
||
----
|
||
|
||
Chiamata per ogni messaggio che arriva sulla porta di ingresso MIDI aperta da
|
||
<<midiopeninput,`midiOpenInput`>>, una volta per messaggio, insieme al resto
|
||
dell'input del fotogramma. I tre numeri sono il messaggio come lo invia un
|
||
dispositivo: `status` porta il tipo nei suoi quattro bit alti e il canale nei
|
||
quattro bassi, contando da `0`, quindi una nota sul primo canale arriva come `0x90`.
|
||
`bytes` è l'intero messaggio come stringa, ed è ciò che serve a un messaggio
|
||
system exclusive e ciò da cui va letto un messaggio più lungo di tre byte.
|
||
|
||
I messaggi che arrivano mentre nessuna porta è aperta non vengono messi in coda;
|
||
non viene consegnato nulla di precedente all'apertura della porta.
|
||
|
||
*Parametri:*
|
||
|
||
* `status` -- numero; il byte di stato, da `0x80` a `0xFF`.
|
||
* `data1` -- numero; il primo byte di dati, o `0` quando il messaggio non ne ha.
|
||
* `data2` -- numero; il secondo byte di dati, o `0` quando il messaggio non ne ha.
|
||
* `bytes` -- stringa; l'intero messaggio, per quanto lungo sia.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<midiopeninput,midiOpenInput>>, <<midisend,midiSend>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Una tastiera MIDI suona il gioco: dal do centrale in su ci sono i quattro pulsanti di fuoco.
|
||
local MIDI_NOTE_ON = 0x90
|
||
|
||
function onMidiMessage(status, data1, data2, bytes)
|
||
-- Il tipo sono i quattro bit alti; il canale nei quattro bassi qui viene ignorato.
|
||
if (status & 0xF0) == MIDI_NOTE_ON and data2 > 0 then
|
||
local button = data1 - 60
|
||
if button >= 0 and button <= 3 then
|
||
fire(button)
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
[#onmousemoved]
|
||
==== onMouseMoved
|
||
|
||
[source,text]
|
||
----
|
||
function onMouseMoved(x, y, xRelative, yRelative, which)
|
||
end
|
||
----
|
||
|
||
Chiamata per ogni evento di movimento del mouse, con la posizione e il movimento dall'evento precedente in coordinate dell'overlay. In modalità `MOUSE_SINGLE` la posizione del puntatore della finestra viene convertita in coordinate dell'overlay e `which` è sempre `0`. In modalità `MOUSE_MANY` il movimento relativo di ogni dispositivo viene integrato nella sua posizione, limitata all'area del video, e `which` è l'indice del dispositivo; un dispositivo assoluto come una tavoletta riporta la sua posizione mappata con `xRelative` e `yRelative` a `0`. La posizione viene memorizzata per `mouseGetPosition` prima che la callback giri, quindi un gioco può ignorare questa callback e interrogare invece lo stato. Il disegno deve comunque aspettare `onOverlayUpdate`: qui tieni l'ultima posizione in variabili globali e disegna il cursore lì.
|
||
|
||
*Parametri:*
|
||
|
||
* `x`, `y` -- posizione in coordinate dell'overlay.
|
||
* `xRelative`, `yRelative` -- movimento dall'evento precedente, in coordinate dell'overlay.
|
||
* `which` -- indice del mouse, da `0` a `3`.
|
||
|
||
*Vedi anche:* <<mousegetposition,mouseGetPosition>>, <<mousesetmode,mouseSetMode>>, <<mousesetenabled,mouseSetEnabled>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
cursorX = overlayGetWidth() // 2
|
||
cursorY = overlayGetHeight() // 2
|
||
|
||
function onMouseMoved(x, y, xRelative, yRelative, which)
|
||
cursorX = x
|
||
cursorY = y
|
||
end
|
||
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
spriteDraw(reticle, cursorX, cursorY, true)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#oncollision]
|
||
==== onCollision
|
||
|
||
[source,text]
|
||
----
|
||
function onCollision(nodeA, nodeB, x, y, z, speed)
|
||
end
|
||
----
|
||
|
||
Chiamata una volta per ogni nuovo contatto tra due corpi dopo un passo della fisica, e mai più per una coppia che resta a contatto. Una cassa che atterra, una palla che colpisce il cabinato, l'auto del giocatore che sfiora un muro. I contatti con i trigger vengono riportati invece da `onTrigger`. Gli eventi di un passo vengono consegnati tutti insieme dopo il passo, prima che il fotogramma venga disegnato.
|
||
|
||
*Parametri:*
|
||
|
||
* `nodeA`, `nodeB` -- i nodi dei due corpi.
|
||
* `x`, `y`, `z` -- dove si sono toccati, in unità del mondo.
|
||
* `speed` -- quanto velocemente si sono incontrati, in unità del mondo al secondo.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<ontrigger,onTrigger>>, <<bodynew,bodyNew>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Gli atterraggi violenti fanno rumore dove avvengono.
|
||
function onCollision(nodeA, nodeB, x, y, z, speed)
|
||
if speed > 4 and (nodeA == crate or nodeB == crate) then
|
||
local channel = soundPlay(thudClip)
|
||
if channel >= 0 then
|
||
soundSetPosition(channel, x, y, z)
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
[#ontrigger]
|
||
==== onTrigger
|
||
|
||
[source,text]
|
||
----
|
||
function onTrigger(trigger, other, entered)
|
||
end
|
||
----
|
||
|
||
Chiamata quando un corpo entra in un trigger creato con `bodySetTrigger` o ne esce, una volta all'entrata e una all'uscita, quali che siano le forme coinvolte (una forma mesh o composta che tocca in più punti conta comunque come un solo corpo). I trigger non spingono mai niente, quindi questo è il modo per accorgersi che un corpo attraversa una porta, raggiunge un checkpoint o cade in un volume mortale.
|
||
|
||
*Parametri:*
|
||
|
||
* `trigger` -- il nodo del corpo trigger.
|
||
* `other` -- il nodo che vi è entrato o ne è uscito.
|
||
* `entered` -- `true` all'entrata, `false` all'uscita.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<bodysettrigger,bodySetTrigger>>, <<oncollision,onCollision>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onTrigger(trigger, other, entered)
|
||
if trigger == finishLine and other == playerCar and entered then
|
||
lap = lap + 1
|
||
soundPlay(lapClip)
|
||
elseif trigger == pit and entered then
|
||
respawn(other)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#onnavarrived]
|
||
==== onNavArrived
|
||
|
||
[source,text]
|
||
----
|
||
function onNavArrived(agent)
|
||
end
|
||
----
|
||
|
||
Chiamata quando un agente di navigazione raggiunge la destinazione che gli ha dato `navAgentMoveTo`, una volta per arrivo, dopo l'aggiornamento della navigazione del fotogramma. Manda l'agente da un'altra parte, o riproduci la sua animazione da fermo. Uno script che preferisce interrogare lo stato può chiamare invece `navAgentIsArrived`.
|
||
|
||
*Parametri:*
|
||
|
||
* `agent` -- l'handle restituito da `navAgentNew`.
|
||
|
||
*Disponibile da:* 3.00.
|
||
*Vedi anche:* <<navagentmoveto,navAgentMoveTo>>, <<navagentisarrived,navAgentIsArrived>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Pattuglia tra i punti di passaggio all'infinito.
|
||
function onNavArrived(agent)
|
||
local guard = guards[agent]
|
||
if guard then
|
||
guard.waypoint = guard.waypoint % #guard.route + 1
|
||
local next = guard.route[guard.waypoint]
|
||
navAgentMoveTo(agent, next.x, next.y, next.z)
|
||
end
|
||
end
|
||
----
|
||
|
||
[#onoverlayupdate]
|
||
==== onOverlayUpdate
|
||
|
||
[source,text]
|
||
----
|
||
function onOverlayUpdate()
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
L'unica callback in cui è permesso disegnare. Singe la chiama al massimo una volta ogni 15 millisecondi mentre il gioco non è congelato, contando da quando è iniziata la chiamata precedente, così il tempo che impiega lo script stesso rientra in quei 15 millisecondi invece di aggiungersi a essi; qualsiasi chiamata `overlay*`, `spriteDraw`, `videoDraw`, `fontPrint` o simile fatta altrove non comparirà sullo schermo e può corrompere l'overlay. Restituisci `OVERLAY_UPDATED` (`1`) quando l'overlay è cambiato, così Singe lo ricompone sopra il video, oppure `OVERLAY_NOT_UPDATED` (`0`) quando non è cambiato nulla, così viene riusata la composizione precedente; non restituire nulla conta come `OVERLAY_NOT_UPDATED`. Lo schermo viene ridisegnato comunque ogni volta che arriva un nuovo fotogramma video o è attiva una scena 3D, quindi il valore restituito è un'ottimizzazione per le schermate statiche. Dopo che è tornata, il motore azzera i valori dietro `keyboardGetLastDown` e `keyboardGetLastUp`. Nel modello a thread `Framework.singe` definisce questa funzione da sé per riprendere `singeMain` e restituisce sempre `OVERLAY_UPDATED`; non definirla anche tu.
|
||
|
||
*Restituisce:* `OVERLAY_UPDATED` o `OVERLAY_NOT_UPDATED`.
|
||
|
||
*Vedi anche:* <<singemain,singeMain>>, <<keyboardgetlastdown,keyboardGetLastDown>>, <<onshutdown,onShutdown>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
if not hudDirty then
|
||
return OVERLAY_NOT_UPDATED
|
||
end
|
||
overlayClear()
|
||
fontPrint(20, 20, "SCORE " .. score)
|
||
fontPrint(20, 44, "LIVES " .. lives)
|
||
spriteDraw(reticle, cursorX, cursorY, true)
|
||
hudDirty = false
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#onshutdown]
|
||
==== onShutdown
|
||
|
||
[source,text]
|
||
----
|
||
function onShutdown()
|
||
end
|
||
----
|
||
|
||
Chiamata una volta quando lo script termina, che sia per `singeQuit`, per l'interruttore di uscita, per la chiusura della finestra, per `scriptExecute` o per `scriptPush`, dopo l'ultimo fotogramma e prima che lo stato Lua venga scartato. Non viene chiamata da `singeReload`. Qui salva le impostazioni e libera gli sprite, i suoni, i font e i video che lo script ha caricato; dopo, il motore ferma ogni suono e libera ciò che resta, quindi un handle dimenticato non è una perdita di memoria, ma scaricare esplicitamente lascia l'ordine di chiusura sotto il controllo dello script. Non chiamare `singeQuit` da qui.
|
||
|
||
*Vedi anche:* <<singequit,singeQuit>>, <<soundunload,soundUnload>>, <<scriptpush,scriptPush>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function onShutdown()
|
||
saveHighScores()
|
||
spriteUnload(reticle)
|
||
fontUnload(hudFont)
|
||
soundUnload(shotClip)
|
||
soundUnload(coinClip)
|
||
end
|
||
----
|
||
|
||
[#onsoundcompleted]
|
||
==== onSoundCompleted
|
||
|
||
[source,text]
|
||
----
|
||
function onSoundCompleted(channel)
|
||
end
|
||
----
|
||
|
||
Chiamata con il numero del canale ogni volta che un canale si ferma: quando la sua clip finisce, quando termina l'ultimo loop e quando `soundStop` o `soundFullStop` lo fermano, quindi uno script che da qui concatena i suoni deve sapere quali canali ha fermato da sé. Il mixer annota l'arresto sul proprio thread e il motore consegna la callback sul thread del gioco tra un fotogramma e l'altro; mentre il tasto di pausa tiene il gioco congelato le notifiche aspettano, e vengono scartate quando uno script termina o si ricarica. Tra un fotogramma e l'altro vengono messi in coda fino a 64 completamenti; quelli in più in un solo fotogramma vanno persi.
|
||
|
||
*Parametri:*
|
||
|
||
* `channel` -- il numero del canale restituito dalla chiamata `soundPlay`.
|
||
|
||
*Vedi anche:* <<soundplay,soundPlay>>, <<soundstop,soundStop>>, <<soundisplaying,soundIsPlaying>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
-- Riproduci la playlist musicale in ordine, all'infinito.
|
||
function onSoundCompleted(channel)
|
||
if channel == musicChannel and not musicStopped then
|
||
track = track % #playlist + 1
|
||
musicChannel = soundPlay(playlist[track])
|
||
end
|
||
end
|
||
----
|
||
|
||
[#singemain]
|
||
==== singeMain
|
||
|
||
[source,text]
|
||
----
|
||
function singeMain()
|
||
while true do
|
||
singeYield()
|
||
end
|
||
end
|
||
----
|
||
|
||
Il punto di ingresso del modello a thread. Quando uno script la definisce, `Framework.singe` la avvolge in una coroutine e installa un `onOverlayUpdate` che riprende la coroutine una volta per fotogramma, così la funzione gira come una linea retta di logica di gioco che chiama `singeYield()` (un alias di `coroutine.yield`) dovunque voglia lasciar passare un fotogramma. Tutto ciò che viene disegnato prima di un yield finisce nell'overlay di quel fotogramma. Uscire da `singeMain` chiama `singeQuit`; un errore al suo interno termina il gioco con un traceback. Le callback di input, del suono, della fisica e della navigazione continuano a scattare tra una ripresa e l'altra, quindi un gioco a thread può mescolarle con l'interrogazione dello stato.
|
||
|
||
*Vedi anche:* <<onoverlayupdate,onOverlayUpdate>>, <<keyboardgetlastdown,keyboardGetLastDown>>, <<singequit,singeQuit>>
|
||
|
||
.Esempio
|
||
[source,lua]
|
||
----
|
||
function singeMain()
|
||
showTitle()
|
||
while credits == 0 do
|
||
singeYield()
|
||
end
|
||
for level = 1, #levels do
|
||
playLevel(levels[level])
|
||
if lives == 0 then
|
||
break
|
||
end
|
||
end
|
||
showGameOver()
|
||
end
|
||
----
|
||
|
||
[#_hypseus_compatibility]
|
||
=== Compatibilità con Hypseus
|
||
|
||
IMPORTANT: Tutto ciò che c'è in questa sezione esiste perché un gioco scritto per
|
||
Hypseus Singe funzioni senza modifiche. *Niente di tutto ciò è consigliato per un
|
||
gioco nuovo.* Preferisci la chiamata di Singe che ogni voce indica: è quella che
|
||
continuerà ad acquisire funzionalità, e non si porta dietro le scale, gli ordini
|
||
degli argomenti o le risposte fisse di un altro motore.
|
||
|
||
Hypseus Singe registra nel suo motore alcuni nomi a cui Singe risponde invece da
|
||
`Singe/Framework.singe`, perché un gioco scritto per Hypseus funzioni senza
|
||
modifiche. Ognuno viene definito solo quando il motore non lo fornisce già,
|
||
quindi una futura implementazione nel motore ha la precedenza.
|
||
`keyboardCatchQuit` è documentata in <<keyboard,Tastiera>>;
|
||
`setOverlayLinearScale` e `setOverlayOnTop` sono vere chiamate del motore in
|
||
<<overlay,Overlay>>; `mainBezelLoaded` e le sette chiamate `scoreBezel` sono vere
|
||
chiamate del motore in <<bezelref,Bezel>>; `getFValue`, `ratioGetX` e
|
||
`ratioGetY` sono vere chiamate del motore in <<commandlineref,Riga di comando>>;
|
||
`joyMouseEnable` è una vera chiamata del motore in <<joystick,Joystick>>;
|
||
`allowSocketCall` e `getUserString` sono vere chiamate del motore in
|
||
<<singe,Singe>>, e `drawTransparent` in <<sprite,Sprite>>; e l'intera famiglia
|
||
`music` è ora un insieme di vere chiamate del motore in <<musicref,Musica>>. I
|
||
sei nomi `spriteAnim` qui sotto sono semplici alias impostati dopo l'avvolgimento
|
||
storico degli sprite, quindi accettano l'ordine degli argomenti che sta usando il
|
||
resto della famiglia degli sprite: quello di Hypseus (handle per ultimo) con
|
||
`LEGACY_SPRITE_ARGS`, e quello di Singe 3.00 (handle per primo) senza.
|
||
|
||
[cols="1,1",options="header"]
|
||
|===
|
||
| Funzione | Fornita da Framework.singe come
|
||
| `setOverlaySize(preset [, width, height])` | `overlaySetResolution` con una dimensione scelta: `1` è la dimensione del video del disco, `3` è `360x240`, `4` è la dimensione data (ignorata quando manca, come in Hypseus) e qualsiasi altro valore, `2` compreso, è metà del disco su entrambi gli assi. Senza disco `discGetWidth` e `discGetHeight` riportano il canvas, che è ciò in cui disegna un gioco senza disco.
|
||
| `setOverlayResolution(width, height)` | Un alias di <<overlaysetresolution,`overlaySetResolution`>>. Hypseus registra un'unica funzione con entrambi i nomi.
|
||
| `rewriteStatus()` | Stub che restituisce `false`. In Hypseus riporta se i percorsi delle risorse vengono riscritti per un'installazione `-espath`; il file system virtuale di Singe trova i file di un gioco dove sono stati installati, quindi non viene riscritto nulla e il gioco mantiene la propria cartella delle risorse.
|
||
| `hypseusGetHeight()` | Un alias di <<singegetheight,`singeGetHeight`>>.
|
||
| `hypseusGetWidth()` | Un alias di <<singegetwidth,`singeGetWidth`>>.
|
||
| `takeScreenshot()` | Un alias di <<singescreenshot,`singeScreenshot`>>. Hypseus rifiuta un secondo screenshot entro cinque secondi; Singe non ha questo limite.
|
||
| `mouseHowManyReal()` | Un alias di <<mousehowmany,`mouseHowMany`>>. Hypseus conta i mouse veri a parte quelli che ricava da un controller; Singe non ha mouse ricavati, quindi il suo conteggio è già quello vero.
|
||
| `spriteAnimGetFrame(spr)` | Un alias di <<spritegetframe,`spriteGetFrame`>>.
|
||
| `spriteAnimIsPlaying(spr)` | Un alias di <<spriteisplaying,`spriteIsPlaying`>>.
|
||
| `spriteAnimLoop(loop, spr)` | Un alias di <<spriteloop,`spriteLoop`>>.
|
||
| `spriteAnimPause(spr)` | Un alias di <<spritepause,`spritePause`>>.
|
||
| `spriteAnimPlay(spr)` | Un alias di <<spriteplay,`spritePlay`>>.
|
||
| `spriteAnimSetFrame(frame, spr)` | Un alias di <<spritesetframe,`spriteSetFrame`>>.
|
||
|===
|
||
|
||
[#unimplementedandlegacy]
|
||
=== Non implementate e storiche
|
||
|
||
Alcune funzioni dell'API sono mantenute per la compatibilità a livello di script, e quelle segnate «Non fa nulla» non fanno niente. Si possono chiamare senza rischi; scrivono semplicemente un messaggio nella traccia e ritornano. Nel codice nuovo preferisci i sostituti indicati.
|
||
|
||
[cols="1,1,1",options="header"]
|
||
|===
|
||
| Funzione | Stato | Sostituto
|
||
| `discChangeSpeed` | Non fa nulla | -- (serviva per la riproduzione a velocità variabile sul vero hardware LD)
|
||
| `discSearchBlanking` | Non fa nulla | -- (serviva per oscurare lo schermo durante il salto sul vero hardware LD)
|
||
| `discSkipBlanking` | Non fa nulla | -- (stesso motivo)
|
||
| `discSetFPS` | Non fa nulla | La frequenza dei fotogrammi viene letta dal file video.
|
||
| `vldpGetHeight` | Funziona | <<discgetheight,`discGetHeight`>>
|
||
| `vldpGetWidth` | Funziona | <<discgetwidth,`discGetWidth`>>
|
||
| `vldpSetVerbose` | Non fa nulla | --
|
||
| `colorBackground(r,g,b)` | Funziona | Preferisci la forma a 4 argomenti, `colorBackground(r, g, b, a)`.
|
||
| `colorForeground(r,g,b)` | Funziona | Preferisci la forma a 4 argomenti, `colorForeground(r, g, b, a)`.
|
||
| `daphneGetWidth` / `daphneGetHeight` / `daphneScreenshot` | Alias in `Framework.singe` | Preferisci gli equivalenti `singe*`.
|
||
| `discPauseAtFrame` | Funziona (alias) | <<discsearch,`discSearch`>>.
|
||
|===
|
||
|
||
Inoltre `Framework.singe` ridefinisce `random = { new = math.random }` perché gli script precedenti alla 2.00 che usano `random.new()` funzionino ancora. Il codice nuovo dovrebbe usare direttamente `math.random`.
|