singe/docs/it/lessons/14-video.adoc

503 lines
19 KiB
Text

[#_lesson_14_playing_video]
== Lezione 14: Riprodurre video
image::learn/14-video.png[La lezione finita, 480]
Singe sa riprodurre un filmato e far girare il tuo gioco al suo interno.
L'immagine diventa il mondo, e l'overlay su cui disegni dalla prima lezione
viene steso sopra. In questa lezione avvierai un video, scoprirai quale
fotogramma si vede, lo fermerai e salterai a qualunque momento ti piaccia.
Prima di tutto questo, c'è una cosa da dire chiaramente, perché decide se devi
leggere questa parte del libro.
[#_a_game_plays_video_only_if_it_asks_to]
=== Un gioco riproduce video solo se lo chiede
Singe è un motore di gioco. Tutto ciò che hai costruito nella prima e nella
seconda parte -- gli sprite, il suono, le collisioni, il punteggio, la cartella
del gioco -- è Singe per intero, e niente di tutto ciò riguarda il video. Quello
che la terza parte aggiunge è un extra: un gioco può chiedere che dietro di lui
scorra un filmato, e allora il mondo è il filmato invece di un canvas nero.
Non cambia nient'altro. `onOverlayUpdate` viene ancora chiamata sessanta volte
al secondo. `overlayPrint` conta ancora in celle di carattere. I tuoi sprite si
disegnano dove si disegnavano prima. Un gioco con il video e uno senza si
scrivono allo stesso modo, con le stesse funzioni, e condividono lo stesso
libro.
Quindi se il gioco che vuoi fare non ha video, puoi chiudere adesso questa parte
e passare alla quarta. Niente nella quarta parte dipende da qualcosa della
terza, e le uniche lezioni che hanno bisogno di questa sono le quattro che la
seguono.
Ancora qui? Bene.
[#_you_already_have_a_video]
=== Hai già un video
Non devi trovarne uno, comprarne uno o farne uno. La prima volta che hai
eseguito Singe nella prima lezione, nella tua cartella di lavoro è comparsa una
cartella chiamata `Singe`. Aprila e guarda: tra i file che il motore ha estratto
c'è `menuBackground.mkv`. È lo sfondo che usa il menu del motore stesso, è di
720 per 480 pixel, ed è lungo 420 fotogrammi -- circa quattordici secondi.
È il video di questa lezione e delle due successive.
Crea una cartella chiamata `movie` nella tua cartella di lavoro, accanto alle
cartelle delle altre lezioni, e dentro un file chiamato `movie.singe`. Non c'è
nient'altro da preparare. Avvii Singe dalla cartella di lavoro, come fai dalla
prima lezione, quindi la cartella `Singe`, video compreso, è proprio lì accanto
al tuo gioco, e `Singe/menuBackground.mkv` è il nome che fra un momento darai al
motore. Non c'è niente da copiare.
[#_type_this_in_2]
=== Scrivi questo
[source,lua]
----
dofile("Singe/Framework.singe")
function onOverlayUpdate()
overlayClear()
overlayPrint(2, 2, "Frame " .. discGetFrame())
return OVERLAY_UPDATED
end
discPlay()
----
Salvalo.
[#_run_it_2]
=== Avvialo
Il video non fa parte del tuo script, quindi non lo carichi nel tuo script. Lo
passi al motore sulla riga di comando, con `-v`:
----
Singe -R -v Singe/menuBackground.mkv movie
----
`-v` è l'abbreviazione di `--framefile`, e indica il video che il gioco deve
riprodurre. Il nome del tuo script viene per ultimo, come sempre. `-R` è lo
stesso `-R` che usi dalla prima lezione, e fa ancora ripartire il gioco quando
salvi.
Il senso di `-v` sta tutto nel chiedere. Se metti un file video accanto al tuo
script e non dici niente, il motore se ne accorge, lo dice sulla console ed
esegue il tuo gioco senza. Un gioco è un gioco video solo quando qualcuno dice
che lo è.
Si apre una finestra. Una palla di fuoco arancione la riempie, dal fuoco prende
forma un drago, sotto compare la parola `SINGE`, e poi una griglia magenta
scorre via sotto un sole arancione. Nell'angolo in alto a sinistra, sopra tutto
quanto, c'è un numero che sale.
Guardalo fino alla fine. Poi guarda che cosa succede alla fine.
[#_what_just_happened_10]
=== Cos'è appena successo
[source,lua]
----
discPlay()
----
Singe chiama il video *il disco*, e ogni funzione che lo comanda comincia con
`disc`. Il nome è storico: il motore è nato facendo girare giochi che tenevano
il loro filmato su un laserdisc, e il nome è rimasto attaccato all'idea. È un
file video sul tuo disco rigido e niente di più.
Un gioco ha al massimo un disco, quindi nessuna delle funzioni `disc` prende un
argomento che dica quale. Ce n'è uno solo.
Questa riga non sta dentro nessuna funzione. Sta in fondo al file, da sola, e
viene eseguita una volta quando Singe carica il tuo script -- prima del primo
`onOverlayUpdate`, prima di qualunque altra cosa. Singe non ha una callback
`onStartup` perché non gliene serve una: uno script Lua *è* un elenco di
istruzioni, e tutto ciò che scrivi fuori da una funzione avviene mentre il file
viene letto. Per convenzione, il codice di avvio va in fondo, sotto le funzioni.
Ti serve `discPlay()` perché il disco non parte da solo. Quando il gioco
comincia, il motore parcheggia il disco sul fotogramma uno e lo mette in pausa
lì. Non si muove niente finché non lo chiedi.
[source,lua]
----
overlayPrint(2, 2, "Frame " .. discGetFrame())
----
`discGetFrame` risponde a una domanda: quale fotogramma si vede in questo
momento? Non prende argomenti e restituisce un numero.
Nota dove viene disegnato quel numero. Colonna 2, riga 2 -- esattamente dove
andava `Hello!` nella prima lezione, e finisce nello stesso punto dello
schermo. L'overlay su cui disegni è grande la metà del video su ogni lato, cioè
360 per 240 per un video di 720 per 480, che è esattamente la dimensione su cui
disegni da tredici lezioni. Le tue coordinate non si sono
spostate. Se un giorno riproduci un video di un'altra dimensione, l'overlay
cambia dimensione con lui, e `overlaySetResolution` lo imposta a quello che
vuoi; la voce nel Riferimento ha i dettagli.
È tutta qui l'idea dell'overlay. Il video sta sotto. Il tuo disegno sta sopra, e
dovunque non hai disegnato si vede il filmato. Ecco perché `overlayClear`
adesso conta ancora più di prima: pulisci l'overlay e stai guardando il
filmato.
[#_a_frame_is_an_address]
=== Un fotogramma è un indirizzo
Il numero che `discGetFrame` restituisce è un numero di *fotogramma*. Un
fotogramma è un'immagine ferma. Questo video ne contiene 420 e ne mostra circa
trenta al secondo, quindi dura circa quattordici secondi.
I fotogrammi si contano da zero, quindi questo video ha i fotogrammi da 0 a
419. Il conteggio e l'ultimo numero non sono la stessa cosa, e questo frega
tutti una volta. `discGetFrameCount` ti dice il conteggio -- 420 -- e il
fotogramma più alto che puoi chiedere è uno di meno.
La cosa importante di un numero di fotogramma è che non cambia mai e non slitta
mai. Il fotogramma 110 è il momento in cui il drago sputa fuoco, in questa copia
del file, su questa macchina, oggi e l'anno prossimo. I secondi slittano, perché
un video non va sempre esattamente alla velocità che pensi. Un numero di
fotogramma è l'indirizzo di un momento, e gli indirizzi sono la materia di cui è
fatto un gioco video: annoti il fotogramma in cui compare il drago, e da lì in
poi puoi mandarci il filmato quando vuoi.
Trovare quei numeri è un lavoro che fai una volta per ogni video, e lo script
che hai scritto è lo strumento per farlo: fai andare il filmato, guarda il
contatore, annota i fotogrammi in cui succedono le cose. Tutto ciò che c'è nella
sedicesima lezione è costruito su un elenco di numeri trovati esattamente così.
[#_what_happened_at_the_end]
=== Che cosa è successo alla fine
Il contatore è arrivato a 419, poi è tornato a 0 e il fuoco è ricomparso.
Quando il disco arriva in fondo, il motore lo fa ripartire dal fotogramma zero.
Non si ferma, e non te lo dice. Un vero gioco non lo lascia mai arrivare lì:
tiene d'occhio il numero del fotogramma e manda il disco da un'altra parte prima
che arrivi la fine. Dimenticarsene è il motivo per cui un gioco video finito a
metà all'improvviso riproduce la propria sequenza dei titoli nel bel mezzo di un
combattimento.
[#_stopping_jumping_and_asking]
=== Fermare, saltare e chiedere
Aggiungi questa funzione sopra `onOverlayUpdate`:
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
if discGetState() == DISC_PLAYING then
discPause()
else
discPlay()
end
elseif what == SWITCH_LEFT then
discSearch(0)
elseif what == SWITCH_RIGHT then
discSkipForward(30)
elseif what == SWITCH_START1 then
discStop()
end
end
----
Salva, e provali uno per uno. La barra spaziatrice avvia e mette in pausa.
Sinistra torna all'inizio. Destra salta avanti di circa un secondo. Il tasto `1`
ferma del tutto il disco, e quello che ottieni è una schermata blu -- il colore
che mostrava un lettore laserdisc quando era fermo, mantenuto perché moltissimi
giochi ci disegnavano sopra la tabella dei punteggi.
Lì ci sono cinque nuove chiamate, e si dividono in due tipi.
*Chiedere.* `discGetState` restituisce uno di quattro nomi che il motore ha già
definito per te: `DISC_PLAYING`, `DISC_PAUSED`, `DISC_STOPPED` e
`DISC_EJECTED`. Sono come `OVERLAY_UPDATED` -- niente virgolette, perché sono
nomi di valori e non testo. Confrontali con `==` come confrontavi i numeri nella
terza lezione.
*Comandare.* `discPlay` avvia o riprende. `discPause` si blocca sul fotogramma
che si vede e lo lascia sullo schermo. `discStop` è quella più pesante:
l'immagine viene sostituita dalla schermata blu, `discGetFrame` risponde `0`
finché il disco non riparte, e `discPause` e le chiamate di salto vengono
ignorate finché è fermo. Una qualunque tra `discPlay`, `discSearch` e
`discSkipToFrame` lo rimette in moto.
Due chiamate mandano il disco a un fotogramma, e la differenza tra le due è
l'unica cosa di questa lezione che valga la pena imparare a memoria:
* `discSearch(frame)` ci va e lì si *mette in pausa*.
* `discSkipToFrame(frame)` ci va e da lì *riproduce*.
Cerca quando vuoi parcheggiare su un'immagine e aspettare. Salta quando vuoi
che cominci una scena. `discSkipForward(30)` e `discSkipBackward(30)` sono la
stessa idea misurata da dove sei invece che dall'inizio, e lasciano il disco in
riproduzione se era in riproduzione e in pausa se era in pausa.
C'è anche `discPauseAtFrame`, che fa esattamente quello che fa `discSearch` con
un nome più vecchio. Il Riferimento lo segna come alias storico. Lo vedrai negli
script degli altri; nei tuoi scrivi `discSearch`.
[#_waiting_is_harder_than_it_sounds]
=== Aspettare è più difficile di quanto sembri
Ecco la frase di cui è fatto ogni gioco video:
____
Riproduci dal fotogramma 0 al fotogramma 120, poi fai una domanda al giocatore.
____
Prova a scriverla. Non puoi scrivere «riproduci, poi aspetta, poi chiedi»,
perché non hai nessun posto dove scriverlo. Hai `onOverlayUpdate`, che Singe
chiama sessanta volte al secondo e che deve tornare subito, ogni singola volta.
Una funzione che se ne stesse lì ad aspettare fermerebbe il motore di colpo:
niente disegno, niente comandi, niente video.
Quindi non puoi dire «poi». Devi trasformare il «poi» in una variabile che
ricorda a che punto sei arrivato, e controllarla in ognuna di quelle sessanta
chiamate:
[source,lua]
----
local asking = false
function onOverlayUpdate()
overlayClear()
if not asking and discGetFrame() >= 120 then
discPause()
asking = true
end
if asking then
overlayPrint(2, 4, "Left or right?")
end
return OVERLAY_UPDATED
end
----
Funziona. Leggilo e vedi che funziona. Ma nota quanto è costato: una variabile,
il cui unico lavoro è ricordare in quale parte della frase ti trovi, e due
istruzioni `if` per tenerla in riga.
Ora aggiungi il resto della scena. Il giocatore risponde, quindi dopo parte una
di due cose. Ognuna di queste finisce da qualche parte e fa un'altra domanda. Un
modesto gioco a diramazioni ha quaranta di questi momenti, e ognuno vuole la sua
variabile e il suo `if`. Finisci con uno script in cui la storia non è scritta
da nessuna parte -- è sparpagliata su una dozzina di flag, e per scoprire che
cosa succede dopo il drago devi leggerli tutti.
Tieni a mente questo pensiero. La quindicesima lezione non è nient'altro che la
soluzione.
[#_when_there_is_no_video]
=== Quando non c'è il video
Esegui di nuovo il tuo script nel modo semplice, senza `-v`:
----
Singe -R movie
----
Non va in crash niente. Ottieni una finestra nera con `Frame 0` nell'angolo.
Vale la pena capirlo, perché è la metà rassicurante della promessa all'inizio di
questa lezione. Un gioco senza disco esegue comunque ogni chiamata `disc` che
fai; semplicemente non fanno niente di utile. Il motore documenta esattamente
che cosa rispondono, e non cambia mai: `discGetFrame` e `discGetFrameCount`
restituiscono `0`, `discGetState` restituisce `DISC_EJECTED`, `discGetWidth` e
`discGetHeight` restituiscono la dimensione del canvas, e `discPlay`,
`discPause`, `discSearch` e le altre vengono annotate nel log e per il resto
ignorate.
Non puoi rompere uno script chiamando una funzione del disco quando il disco non
c'è.
Quando uno script ha bisogno di *saperlo*, chiede a `SINGE_DISC`. È un altro
nome che il motore definisce prima che parta il tuo script, e vale `true` quando
il gioco ha un video e `false` quando il mondo è un semplice canvas. È il modo
giusto di scrivere un gioco che possa funzionare in tutti e due i modi:
[source,lua]
----
if not SINGE_DISC then
overlayPrint(2, 2, "This game has no video.")
overlayPrint(2, 4, "Run it again with -v Singe/menuBackground.mkv")
return OVERLAY_UPDATED
end
----
`not` capovolge un valore vero o falso, come nella terza lezione.
[#_the_whole_thing_7]
=== Tutto insieme
Messo insieme, con una piccola funzione che trasforma lo stato in parole e un
fotogramma su cui fermarsi, questo è lo script finito. È un cercatore di
fotogrammi: lo strumento che userai ogni volta che incontri un nuovo video.
[source,lua]
----
dofile("Singe/Framework.singe")
local stopAt = 400
local held = false
local function stateName()
local state = discGetState()
if state == DISC_PLAYING then
return "playing"
elseif state == DISC_PAUSED then
return "paused"
elseif state == DISC_STOPPED then
return "stopped"
end
return "not there"
end
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
if discGetState() == DISC_PLAYING then
discPause()
else
discPlay()
end
elseif what == SWITCH_LEFT then
discSearch(0)
held = false
elseif what == SWITCH_RIGHT then
discSkipForward(30)
elseif what == SWITCH_START1 then
discStop()
held = false
end
end
function onOverlayUpdate()
local frame = discGetFrame()
overlayClear()
if not SINGE_DISC then
overlayPrint(2, 2, "This game has no video.")
overlayPrint(2, 4, "Run it again with -v Singe/menuBackground.mkv")
return OVERLAY_UPDATED
end
if not held and frame >= stopAt then
discPause()
held = true
end
overlayPrint(2, 2, "Frame " .. frame .. " of " .. discGetFrameCount())
overlayPrint(2, 4, "The disc is " .. stateName() .. ".")
overlayPrint(2, 6, "Space plays and pauses. Left rewinds.")
overlayPrint(2, 7, "Right jumps on a second. 1 stops the disc.")
if held then
overlayPrint(2, 9, "Holding at frame " .. stopAt .. ".")
end
return OVERLAY_UPDATED
end
discPlay()
----
`held` è lo stesso tipo di variabile di `asking` qui sopra, ed è qui per un
motivo che puoi verificare: toglila, lascia `if frame >= stopAt then discPause()
end`, e poi prova a premere la barra spaziatrice per andare oltre il fotogramma
400. Non ci riesci. Il disco riparte per un sessantesimo di secondo e il
`onOverlayUpdate` successivo lo rimette in pausa, per sempre. `held` è il modo
in cui dici «questo l'ho già fatto una volta».
[#_try_it_14]
=== Prova tu
. *Trova il drago.* Eseguilo, guarda il contatore e annota il fotogramma in cui
il drago compare per la prima volta, quello in cui sputa fuoco e quello in cui
la griglia si prende lo schermo. Questi tre numeri ti serviranno nella
sedicesima lezione.
. *Procedi un passo alla volta.* Cambia `discSkipForward(30)` in
`discSkipForward(1)` e tieni premuto il tasto freccia destra. Ora cambia
`stopAt` in un fotogramma a metà del drago e usa `discSearch` e il tasto
freccia per atterrarci esattamente.
. *Fallo ripetere.* Cambia la fermata in modo che invece di mettere in pausa al
fotogramma 400 chiami `discSkipToFrame(0)`. Hai costruito una modalità
attract: una scena che si ripete per sempre finché qualcuno non preme un
pulsante.
. *Chiedi un fotogramma che non c'è.* Cambia la riga di `SWITCH_RIGHT` in
`discSkipToFrame(100000)`. Non va in crash. Leggi la voce di
`discSkipToFrame` nel Riferimento e trova la frase che dice perché.
. *Giocaci senza il video.* Esegui `Singe -R movie` e poi premi tutti i tasti.
Niente di quello che premi può romperlo.
[#_break_it_on_purpose_13]
=== Rompilo apposta
L'errore che costa più tempo qui non è un errore di Lua. Esegui questo:
----
Singe -R -v menuBackground.mkv movie
----
Il percorso è sbagliato -- parte dalla cartella di lavoro, e lì il video sta
nella cartella `Singe`, non in giro da solo -- e Singe non parte. Quello che fa invece è stampare l'intero elenco
delle sue opzioni da riga di comando, cosa che la prima volta mette in allarme,
e poi questa come ultimissima riga:
----
Error: Unable to locate video.
----
Un muro di opzioni come quello vuol dire sempre la stessa cosa: il motore non è
mai arrivato fino al tuo gioco. Ignora il muro e leggi l'ultima riga.
Quello è facile. La versione meno simpatica è eseguire senza nessun `-v` e
dimenticarsi di averlo fatto. Non c'è nessun errore, perché un gioco senza video
è un gioco perfettamente valido. Ottieni uno schermo nero e un contatore fermo
su `0`, e passi venti minuti a cercare il bug nel tuo script.
È a questo che serve il blocco `SINGE_DISC` nello script finito. Tre righe, e il
gioco ti dice che cosa non va invece di tenere il muso. Metti un controllo come
quello in ogni script che scrivi e che ha bisogno di un video.
[#_what_you_learned_14]
=== Cosa hai imparato
* Un gioco riproduce video solo quando lo chiede, e tutto il resto di Singe
funziona allo stesso modo in entrambi i casi.
* Singe chiama il video il disco. Un gioco ne ha uno, e le funzioni `disc` non
prendono un handle perché non c'è niente tra cui scegliere.
* Il video va sulla riga di comando con `-v`, non nello script.
* L'overlay viene disegnato sopra l'immagine, e il filmato si vede dovunque non
hai disegnato.
* Un fotogramma è un'immagine ferma, contata da zero, e un numero di fotogramma
è l'indirizzo di un momento.
* `discGetFrame` chiede dove si trova il disco; `discGetFrameCount` dice quanti
fotogrammi ci sono; `discGetState` dice che cosa sta facendo il disco.
* `discPlay`, `discPause` e `discStop` lo comandano. `discSearch` va a un
fotogramma e si mette in pausa; `discSkipToFrame` va a un fotogramma e
riproduce.
* Un disco che arriva in fondo riparte dal fotogramma zero senza dirtelo.
* Senza disco ogni chiamata `disc` è innocua, e `SINGE_DISC` è il modo in cui
uno script capisce in che tipo di gioco si trova.
* Non puoi aspettare dentro `onOverlayUpdate`, quindi il «poi» deve essere
scritto per esteso, come variabile.
[#_next_time_14]
=== La prossima volta
Quell'ultimo punto è quello dolente. Un gioco video è un elenco di passi --
riproduci questo, chiedi quello, riproduci l'altro -- e hai appena visto che il
modello a callback non sa dire «poi» senza un flag e un `if` per ogni passo. La
quindicesima lezione ti mostra l'altro modo in cui Singe ti lascia scrivere uno
script, in cui «poi» è una riga nuova.