Un tutorial completo sulla fiamma (o su come realizzare giochi con Flutter)

introduzione

Ciao a tutti! Sono Luan e benvenuto in questo primo tutorial completo su Flame.

Flame è un motore di gioco minimalista Flutter che fornisce alcuni moduli per creare un gioco basato su Canvas.

In questo tutorial, creeremo un gioco molto semplice, in cui le caselle cadranno e l'obiettivo è distruggerle prima che colpiscano il fondo dello schermo.

Ecco come apparirà il gioco

Puoi controllare tu stesso il gioco per vedere cosa stiamo facendo per installare questo APK o installarlo dal Play Store.

Questo ci consentirà di coprire tutte le funzionalità fornite dal framework e di dimostrare come eseguire le operazioni di base: rendering, sprite, audio, testo, animazioni e altro.

Perché abbiamo selezionato questo gioco però? Oltre ad essere molto semplice ma completo per l'esplorazione del framework, un buon fattore è che abbiamo le risorse per farlo!

Per questo gioco utilizzeremo le seguenti risorse: uno sprite di cassa, uno sprite di esplosione (con animazione), un suono di esplosione, un suono "miss" (per quando la scatola non viene colpita), una musica di sottofondo e anche un bel carattere per il rendering del punteggio.

È difficile trovare buone risorse disponibili in commercio su Internet, ma abbiamo trovato tutto (tranne il carattere) in questo fantastico sito. Sono davvero fantastici, assolutamente consigliato.

Per ora, i fogli sprite non sono supportati, quindi, prima di tutto, dobbiamo convertire tutte le risorse in formati appropriati. Inoltre, dobbiamo convertire tutto l'audio per MP3 (è supportato anche OGG; WAV no). Fatto ciò, puoi scaricare un pacchetto con tutto ciò di cui avrai bisogno in termini di risorse qui.

Vale anche la pena ricordare che tutto ciò che è stato descritto viene eseguito su GitHub come versione completa completamente funzionale. Puoi sempre dare un'occhiata in caso di dubbio. Inoltre, l'esempio è stato creato seguendo questi esatti passaggi e eseguendo commit frequenti come istantanee. Durante il tutorial, collegherò gli specifici commit che fanno avanzare il repository allo stage in questione. Ciò ti consentirà di effettuare checkpoint e sfogliare il vecchio codice per vedere tutto ciò che non era chiaro.

Un'ultima cosa; puoi consultare la documentazione completa di Flame qui. Se hai domande, suggerimenti, bug, sentiti libero di aprire un problema o di contattarmi.

Oltre a ciò, avrai bisogno anche di Flutter e Dart installati. Se ti va bene, puoi anche avere IntelliJ IDEA, che è un ottimo posto per scrivere il tuo codice. Per installare questa roba, puoi dare un'occhiata a una moltitudine di tutorial là fuori, come questo.

Nozioni di base

Quindi, per ora, suppongo che tu abbia tutto pronto. Quindi, esegui solo quanto segue e aprilo!

La prima cosa che devi fare è aggiungere la dipendenza dalla fiamma. Vai al tuo file pubspec.yaml e assicurati che la chiave delle dipendenze elenchi flame:

Qui stiamo usando l'ultima versione, 0.5.0, ma puoi anche sceglierne una nuova se disponibile.

Ora, il tuo file main.dart ha già molte cose: un metodo "principale" che deve essere mantenuto; e una successiva chiamata al metodo runApp. Questo metodo utilizza Widget e altri componenti Flutter utilizzati per creare schermate delle app. Dato che stiamo realizzando un gioco, disegneremo tutto sulla tela e non useremo questi componenti; quindi spoglia tutto.

Il nostro metodo principale è ora vuoto e aggiungeremo due cose; in primo luogo, alcune configurazioni:

L'importazione di flame.dart dà accesso alla classe statica Flame, che è solo un supporto per diverse altre utili classi. Ne useremo di più in seguito. Per ora, stiamo chiamando due metodi in questa classe Flame.

L'ultimo è autoesplicativo, disabilita parte della registrazione dal plug-in degli audioplayer. Ora, tra poco aggiungeremo l'audio al gioco e, se non funziona, è qui che devi commentare per risolvere i problemi. Ma ci arriveremo alla fine.

La prima riga è più complessa. Fondamentalmente, alcune funzionalità cruciali di Flutter sono assenti perché non stiamo usando il metodo runApp. Questa chiamata enableEvents fa un po 'di soluzione per ottenere ciò che è essenziale per ogni applicazione, senza la necessità di utilizzare i widget.

Infine, dobbiamo iniziare il nostro gioco. Per fare ciò, aggiungeremo un'altra classe all'elenco di importazione, la classe di gioco. Questa classe fornisce l'astrazione necessaria per creare qualsiasi gioco: un loop di gioco. Deve essere suddiviso in sottoclassi in modo da dover implementare la base di qualsiasi gioco: un metodo di aggiornamento, che viene chiamato ogni volta che è conveniente e richiede il tempo trascorso dall'ultimo aggiornamento, e un metodo di rendering, che deve sapere come disegnare il stato attuale del gioco. I meccanismi interni del loop sono lasciati alla classe di gioco da risolvere (puoi dare un'occhiata, ovviamente, è molto semplice), e devi solo chiamare start, beh, start.

Punto di controllo: 599f809

In questo momento, il rendering non fa nulla, quindi, se lo avvii, dovrebbe funzionare, ma ti darà una schermata nera. Dolce! Quindi abbiamo ottenuto un'app funzionale senza widget e quant'altro, e una tela bianca per iniziare a disegnare la nostra app.

Forme di rendering

E come si fa a disegnare? Disegniamo un semplice rettangolo per vederlo in azione. Aggiungi quanto segue al tuo metodo di rendering:

Qui, come puoi vedere, definiamo un rettangolo, basato sulle posizioni dello schermo. L'immagine seguente mostra come orientare le regole. Fondamentalmente, l'origine si trova nell'angolo in alto a sinistra e l'asse aumenta a destra e verso il basso.

Inoltre, nota che la maggior parte dei metodi di disegno richiede un Paint. Una vernice non è solo un singolo colore, ma potrebbe essere un Degradè o altre trame. Normalmente, vorresti un colore solido o vai direttamente a uno Sprite. Quindi abbiamo semplicemente impostato il colore all'interno della vernice su un'istanza di Colore.

Il colore rappresenta un singolo colore ARGB; lo crei con un numero intero che puoi scrivere in esadecimale per una lettura più semplice; è nel formato A (alfa, trasparenza, normalmente 0xFF), quindi due cifre per R, G e B in quell'ordine.

C'è anche una collezione di colori nominati; è all'interno del pacchetto materiale però. Fai attenzione a importare solo il modulo Colori, in modo da non usare accidentalmente qualcos'altro dal pacchetto materiale.

Bene, ora abbiamo un quadrato!

Punto di controllo: 4eff3bf

Sappiamo anche come funzionano i righelli, ma non conosciamo le dimensioni dello schermo! Come disegneremo qualcosa negli altri tre angoli senza queste informazioni? Non temere, poiché Flame ha un metodo per recuperare la dimensione effettiva dello schermo (è perché c'è un problema documentato attorno a questo.

Fondamentalmente, il metodo asincrono

Nota che la parola chiave wait, proprio come JavaScript, può essere utilizzata solo in una funzione asincrona, quindi assicurati di rendere il tuo asincrono principale (Flutter non se ne curerà).

Il prossimo checkpoint recupererà le dimensioni una volta nel metodo principale e le memorizzerà all'interno della nostra classe di gioco, perché ne avremo bisogno ripetutamente.

Punto di controllo: a1f9df3

Sprite di rendering

Infine, sappiamo come disegnare qualsiasi forma, ovunque sullo schermo. Ma vogliamo gli sprite! Il prossimo checkpoint aggiunge alcune delle risorse che useremo nella cartella delle risorse appropriate:

Punto di controllo: 92ebfd9

E il prossimo fa una cosa cruciale che non puoi dimenticare: aggiungi tutto al tuo file pubsepc.yaml. Quando viene creato il codice, Dart raggrupperà solo le risorse specificate lì.

Checkpoint cf5975f

Finalmente, siamo pronti per disegnare il nostro sprite. Il modo di base che Flame ti consente di fare è quello di esporre un metodo Flame.images.load ('percorso dall'interno della cartella delle immagini') che restituisce una promessa per l'immagine caricata, che può quindi essere disegnata con il metodo canvas.drawImage.

Tuttavia, nel caso di disegnare una cassa, è molto più semplice da fare, perché possiamo usare la classe SpriteComponent, in questo modo:

La classe Component astratta è un'interfaccia con due metodi, rendering e aggiornamento, proprio come il nostro gioco. L'idea è che il gioco può essere composto da componenti che hanno i loro metodi di rendering e aggiornamento chiamati all'interno dei metodi del gioco. SpriteComponent è un'implementazione che esegue il rendering di uno sprite, dato il nome e le dimensioni (quadrato o rettangolare), la posizione (x, y) e l'angolo di rotazione. Ridurrà o espanderà opportunamente l'immagine per adattarla alle dimensioni desiderate.

In questo caso, cariciamo il file "crate.png", che deve trovarsi nella cartella assets / images, e abbiamo una classe Crate che disegna caselle di 128x128 pixel, con angolo di rotazione 0.

Quindi aggiungiamo una proprietà Crate al gioco, la creiamo un'istanza nella parte superiore dello schermo, centrandola orizzontalmente e rendendola nel nostro loop di gioco:

Questo renderà la nostra Cassa! Eccezionale! Il codice è piuttosto succinto e anche di facile lettura.

Checkpoint 7603ca4

Aggiornamento dello stato nel Game Loop

La nostra cassa è quasi ferma in aria. Vogliamo spostarlo! Ogni cassa cadrà a velocità costante, verso il basso. Dobbiamo farlo nel nostro metodo di aggiornamento; basta cambiare la posizione Y della singola Cassa che abbiamo:

Questo metodo richiede il tempo (in secondi) impiegato dall'ultimo aggiornamento. Normalmente questo sarà molto piccolo (ordine di 10 ms). Quindi la VELOCITÀ è una costante in quelle unità; nel nostro caso, VELOCITÀ = 100 pixel / secondo.

Punto di controllo: 452dc40

Gestione dell'input

Evviva! Le casse cadono e scompaiono, ma non puoi interagire con loro. Aggiungiamo un metodo per distruggere le casse che tocchiamo. Per questo, useremo un evento window. L'oggetto window è disponibile in ogni progetto Flutter a livello globale e ha alcune proprietà utili. Registreremo sul metodo principale un evento onPointerDataPacket, ovvero quando l'utente tocca lo schermo:

Estraiamo semplicemente la coordinata (x, y) del clic e la passiamo direttamente al nostro Gioco; in questo modo il gioco può gestire il clic senza preoccuparsi dei dettagli degli eventi.

Per rendere le cose più interessanti, rifattiamo anche la classe di gioco in modo da avere un elenco di casse, anziché una singola. Dopotutto, è quello che vogliamo. Sostituiamo i metodi di rendering e aggiornamento con un forEach over the Crates e il nuovo metodo di input diventa:

Punto di controllo: 364a6c2

Rendering di più sprite

C'è un punto cruciale da menzionare qui e riguarda il metodo di rendering. Quando eseguiamo il rendering di una Cassa, lo stato della tela viene tradotto e ruotato arbitrariamente, al fine di consentire il disegno. Dal momento che disegneremo diverse casse, dobbiamo ripristinare la tela tra ogni disegno. Ciò viene fatto con i metodi save, che salva lo stato corrente e ripristina, che ripristina lo stato precedentemente salvato, eliminandolo.

Questa è un'osservazione importante, poiché è la fonte di molti bug strani. Forse dovremmo farlo automaticamente in ogni rendering? Non lo so, cosa ne pensi?

Ora vogliamo più casse! Come farlo? Bene, il metodo di aggiornamento può essere il nostro timer. Quindi vogliamo che una nuova Cassa venga aggiunta all'elenco (generato) ogni secondo. Quindi abbiamo creato un'altra variabile nella classe Game, per accumulare i tempi delta (t) da ogni chiamata di aggiornamento. Quando supera 1, viene ripristinato e viene generata una nuova cassa:

Non dimenticare di mantenere l'aggiornamento precedente, quindi le casse non smettono di cadere. Inoltre, cambiamo la velocità a 250 pixel / secondo, per rendere le cose un po 'più interessanti.

Punto di controllo: 3932372

Rendering di animazioni

Questo dovrebbe essere un GIF, giusto? Stiamo lavorando su una configurazione per schermate e GIF migliori per questo tutorial!

Ora conosciamo le basi di Sprite Handling e Rendering. Passiamo al passaggio successivo: esplosioni! Quale gioco va bene senza di loro? L'esplosione è una bestia di tipo diverso, perché presenta un'animazione. Le animazioni in Flame vengono eseguite semplicemente eseguendo il rendering di diverse cose sul rendering in base al segno di spunta corrente. Allo stesso modo in cui abbiamo aggiunto un timer fatto a mano alle caselle di spawn, aggiungeremo una proprietà lifeTime per ogni esplosione. Inoltre, Explosion non erediterà da SpriteComponent, poiché per quest'ultimo può avere solo uno Sprite. Estenderemo la superclasse, PositionComponent e implementeremo il rendering con Flame.image.load.

Poiché ogni esplosione ha molti frame e devono essere disegnati in modo reattivo, precaricheremo ogni frame una volta e lo salveremo in una variabile statica all'interno della classe Explosion; così:

Nota che cariciamo ognuno dei nostri 7 frame di animazione, in ordine. Quindi, nel metodo di rendering, creiamo una semplice logica per decidere quale frame disegnare:

Nota che stiamo disegnando "a mano", usando drawImageRect, come spiegato in precedenza. Questo codice è simile a quello che SpriteComponent fa sotto il cofano. Si noti inoltre che, se l'immagine non si trova nell'array, non viene disegnato nulla, quindi dopo TIME secondi (impostato su 0,75 o 750 ms), non viene visualizzato nulla.

Va bene e va bene, ma non vogliamo continuare a inquinare il nostro array di esplosioni con esplosioni esplose, quindi aggiungiamo anche un metodo destro () che restituisce, in base al lifeTime, se dovremmo distruggere l'oggetto esplosione.

Infine, aggiorniamo il nostro gioco, aggiungendo un elenco di esplosioni, eseguendo il rendering sul metodo di rendering e aggiornando quindi sul metodo di aggiornamento. Devono essere aggiornati per aumentare la loro durata. Dedichiamo anche questo tempo al refactoring di ciò che era precedentemente nel metodo Game.update, vale a dire che le caselle cadono, per essere all'interno del metodo Crate.update, poiché questa è una responsabilità della Cassa. Ora l'aggiornamento del gioco viene delegato solo ad altri. Infine, nell'aggiornamento, dobbiamo rimuovere dall'elenco ciò che è stato distrutto. Per questo, List fornisce un metodo molto utile, removeWhere:

L'abbiamo già usato sul metodo di input per rimuovere i riquadri toccati dall'array. C'è anche dove creeremo un'esplosione.

Dai un'occhiata al checkpoint per maggiori dettagli.

Punto di controllo: d8c30ad

Riproduzione audio

Nel prossimo commit, finalmente suoneremo un po 'di audio! Per fare ciò, è necessario aggiungere il file nella cartella delle risorse, all'interno delle risorse / audio /. Deve essere un file MP3 o OGG. Quindi, ovunque nel tuo codice, esegui:

Dove nomefile.mp3 è il nome del file all'interno. Nel nostro caso, eseguiremo il suono explore.mp3 quando clicchiamo in una casella.

Inoltre, iniziamo ad assegnare la punteggiatura. Aggiungiamo una variabile punti per contenere il numero attuale di punti. Inizia con zero; riceviamo 10 punti per ogni casella cliccata e ne perdiamo 20 quando la casella colpisce il terreno.

Ora abbiamo l'obbligo di occuparci delle scatole in fuga. Sulla base di ciò che abbiamo fatto alla classe Explosion, aggiungiamo un metodo di distruzione per la Cassa, che restituirà se sono fuori dallo schermo. Questo sta iniziando a diventare un modello! Se distrutto, rimuoviamo dall'array e regoliamo i punti.

Per ora, il punteggio funziona, ma non viene mostrato da nessuna parte; quello arriverà presto.

L'audio non funzionerà nel prossimo checkpoint perché ho dimenticato di aggiungere i file e inserirli in pubspec.yaml; fatto nel seguente commit.

Punto di controllo: 43a7570

Ora vogliamo più suoni! Gli audioplayer (attenzione alla s) lib che Flame utilizza ti consentono di riprodurre più suoni contemporaneamente, come potresti aver già notato se sei andato freneticamente clic, ma ora usiamo questo a nostro vantaggio, suonando un suono miss, quando la casella colpisce il terreno (distruggere il metodo della Cassa) e una musica di sottofondo.

Per riprodurre la musica di sottofondo su un loop, utilizzare il metodo loop, che funziona proprio come prima:

In questo commit, ripariamo anche la condizione di distruzione per le Casse, che abbiamo perso nel precedente commit (perché non c'era modo di saperlo, ora c'è suono).

Punto di controllo: f575150

Rendering del testo

Ora che abbiamo tutto l'audio che vogliamo (sottofondo, musica, effetti sonori, MP3, OGG, loop, contemporaneamente), passiamo al rendering del testo. Dobbiamo vedere quel punteggio, dopo tutto. Il modo in cui questo viene fatto "a mano" è quello di creare un oggetto Paragrafo e utilizzare il DrawParagraph della tela. Richiede molta configurazione ed è un'API piuttosto confusa, ma può essere realizzata in questo modo:

Punto di controllo: e09221e

Questo è disegnato nel carattere predefinito e puoi usare la proprietà fontFamily per specificare un diverso carattere di sistema comune; sebbene, probabilmente, nel tuo gioco, vorrai aggiungerne uno personalizzato.

Quindi mi sono diretto a 1001fonts.com e ho ottenuto questo font Halo gratuito piuttosto commerciale come TTF. Ancora una volta, basta rilasciare il file in asset / caratteri, ma ora deve essere importato in modo diverso nel file pubspec.yaml. Invece di aggiungere un'altra risorsa, esiste un tag font dedicato, che viene commentato per impostazione predefinita con istruzioni complete su come aggiungere i font. Quindi, assegnagli un nome e fai qualcosa del tipo:

Questo ulteriore livello di astrazione proviene dallo stesso Flutter e consente di aggiungere più file allo stesso carattere (per definire grassetto, dimensioni maggiori, ecc.). Ora, tornando al nostro paragrafo, aggiungiamo semplicemente la proprietà fontFamily: 'Halo' al costruttore TextStyle.

Corri e vedrai il bel carattere Halo!

Punto di controllo: 3155bda

Questo metodo descritto ti darà più controllo, ad esempio se desideri più stili nello stesso paragrafo. Ma se vuoi, come in questo caso, un semplice paragrafo in stile singolo, usa l'helper Flame.util.text per crearlo:

Questa riga singola sostituisce quelle precedenti 4 ed espone le funzionalità più importanti. Il testo (primo argomento) è obbligatorio e tutto il resto è facoltativo, con valori predefiniti ragionevoli.

Per il colore, di nuovo stiamo usando l'helper Colors.white, ma possiamo anche usare il nuovo colore (0xFFFFFFFF) se vuoi un colore specifico.

Punto di controllo: 952a9df

E il gioco è fatto! Un gioco completo con rendering sprite, rendering di testo, audio, loop di gioco, eventi e gestione dello stato.

pubblicazione

Il tuo gioco è pronto per essere rilasciato?

Segui questi semplici passaggi dal tutorial di Flutter.

Sono tutti abbastanza semplici, come puoi vedere in questo checkpoint finale, ad eccezione della parte Icone, che potrebbe causare un po 'di mal di testa. La mia raccomandazione è di creare una versione grande (512 o 1024 px) della tua icona e utilizzare il sito Web Crea icona app per generare una zip con tutto ciò di cui hai bisogno (iOS e Android).

Punto di controllo: 2974f29

Cos'altro?

Ti è piaciuto Flame? Se hai suggerimenti, bug, domande, richieste di funzionalità o altro, non esitare a contattarmi!

Vuoi migliorare il tuo gioco e saperne di più? Che ne dici di aggiungere un server con Firebase e Google Accedi? Che ne dici di inserire annunci? Che ne dici di impostare un menu principale e più schermi?

C'è molto da migliorare, ovviamente: questo è solo un esempio di gioco. Ma avrebbe dovuto dare un'idea di base dei concetti chiave dello sviluppo del gioco con Flutter (con o senza Flame).

Spero che sia piaciuto a tutti!

Originariamente pubblicato su GitHub.