[#_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.