Costruire giochi e applicazioni in rete in tempo reale può essere una sfida. Questo tutorial ti mostrerà come connettere i client flash usando Cirrus e presentarti alcune tecniche vitali.
Diamo un'occhiata al risultato finale su cui lavoreremo. Fai clic sul pulsante di avvio nel file SWF in alto per creare una versione "di invio" dell'applicazione. Apri nuovamente questo tutorial in una seconda finestra del browser, copia il nearId dalla prima finestra nella casella di testo, quindi fai clic su Avvia per creare una versione 'di ricezione' dell'applicazione.
Nella versione "ricevente", vedrai due aghi rotanti: uno rosso, uno blu. L'ago blu ruota di propria iniziativa, ad una velocità costante di 90 ° / secondo. L'ago rosso ruota per adattarsi all'angolo inviato dalla versione 'invio'.
(Se l'ago rosso sembra particolarmente lento, prova a spostare le finestre del browser in modo da poter vedere entrambi i file SWF contemporaneamente. Flash Player esegue gli eventi EnterFrame a una velocità molto bassa quando la finestra del browser è sullo sfondo, quindi la finestra di invio trasmette il nuovo angolo molto meno frequentemente.)
Per prima cosa: avete bisogno di una "chiave sviluppatore" di Cirrus, che può essere ottenuta sul sito di Adobe Labs. Questa è una stringa di testo assegnata in modo univoco alla registrazione. Lo userai in tutti i programmi che scrivi per ottenere l'accesso al servizio, quindi potrebbe essere meglio definirlo come una costante in uno dei tuoi file AS, come questo:
const statico pubblico CIRRUS_KEY: String = "";
Si noti che ogni sviluppatore o team di sviluppo ha bisogno della propria chiave, non di ogni utente delle applicazioni che si creano.
Iniziamo creando una connessione di rete usando un'istanza di (l'hai indovinato) NetConnection
classe. Questo si ottiene chiamando il Collegare()
metodo con la chiave menzionata in precedenza e l'URL del server di rendezvous di Cirrus. Dal momento che al momento della scrittura Cirrus utilizza un protocollo chiuso c'è solo un tale server; il suo indirizzo è rtmfp: //p2p.rtmfp.net
public class Cirrus const statico pubblico CIRRUS_KEY: String = ""private static var netConnection: NetConnection; public static function Init (key: String): void if (netConnection! = null) return; netConnection = new NetConnection (); prova netConnection.connect (" rtmfp: //p2p.rtmfp .net ", chiave); catch (e: errore)
Poiché nulla accade istantaneamente nella comunicazione di rete, il netConnection
oggetto ti farà sapere cosa sta facendo sparando eventi, in particolare il NetStatusEvent
. Le informazioni importanti sono contenute nel codice
proprietà dell'evento Informazioni
oggetto.
funzione privata OnStatus (e: NetStatusEvent): void switch (e.info.code) case "NetConnection.Connect.Success": break; // Il tentativo di connessione è riuscito. caso "NetConnection.Connect.Closed": break; // La connessione è stata chiusa correttamente. caso "NetConnection.Connect.Failed": interruzione; // Il tentativo di connessione fallito.
Un tentativo di connessione non riuscito è solitamente dovuto a determinate porte bloccate da un firewall. Se questo è il caso, non hai altra scelta che riportare l'errore all'utente, poiché non si collegheranno a nessuno finché la situazione non cambierà. Il successo, d'altra parte, ti ricompensa con il tuo nearID
. Questa è una proprietà stringa dell'oggetto NetConnection che rappresenta quella particolare NetConnection, su quel particolare Flash Player, su quel particolare computer. Nessun altro oggetto NetConnection nel mondo avrà lo stesso nearID.
Il nearID è come il tuo numero di telefono personale - le persone che vogliono parlarti dovranno saperlo. È vero anche il contrario: non sarai in grado di connetterti a nessun altro senza conoscere il loro nearID. Quando fornisci a qualcun altro il tuo nearID, lo useranno come a farID
: il farID è l'ID del client a cui si sta tentando di connettersi. Se qualcun altro ti fornisce il loro nearID, puoi usarlo come farID per collegarti a loro. Prendilo?
Quindi tutto ciò che dobbiamo fare è connettersi a un cliente e chiedere loro il loro nearID, e poi ... oh aspetta. Come possiamo scoprire il loro nearID (da usare come nostro farID) se non siamo connessi l'uno con l'altro in primo luogo? La risposta, che rimarrai sorpreso di sentire, è che è impossibile. Hai bisogno di una sorta di servizio di terze parti per scambiare gli id. Gli esempi potrebbero essere:
NetGroup
s, che potremmo esaminare in un futuro tutorialLa connessione di rete è puramente concettuale e non ci aiuta molto dopo la configurazione della connessione. Per trasferire effettivamente i dati da un'estremità della connessione a un'altra che usiamo NetStream
oggetti. Se una connessione di rete può essere pensata come la costruzione di una ferrovia tra due città, allora un NetStream è un treno di posta che invia messaggi reali lungo la traccia.
NetStreams sono unidirezionali. Una volta creati, agiscono come un editore (informazioni di invio) o un abbonato (ricevere informazioni). Se si desidera che un singolo client invii e riceva informazioni tramite una connessione, saranno quindi necessari due NetStreams in ciascun client. Una volta creato un NetStream, puoi fare cose fantasiose come streaming audio e video, ma in questo tutorial ci attaccheremo con dati semplici.
Se, e solo se, riceviamo a NetStatusEvent
da NetConnection con un codice di NetConnection.Connect.Success
, possiamo creare un oggetto NetStream per quella connessione. Per un editore, prima costruisci lo stream usando un riferimento all'oggetto netConnection appena creato e il valore speciale predefinito. Secondo, chiama pubblicare()
sul flusso e dargli un nome. Il nome può essere qualsiasi cosa tu voglia, è proprio lì per un abbonato per distinguere tra più flussi provenienti dallo stesso client.
var ns: NetStream = new NetStream (netConnection, NetStream.DIRECT_CONNECTIONS); ns.publish (nome, null);
Per creare un sottoscrittore, si passa nuovamente l'oggetto netConnection nel costruttore, ma questa volta si passa anche il farID del client a cui si desidera connettersi. In secondo luogo, chiama giocare()
con il nome dello stream che corrisponde al nome del flusso di pubblicazione dell'altro cliente. Per dirla in altro modo, se pubblichi uno stream con il nome "Test", l'abbonato dovrà utilizzare il nome "Test" per collegarsi ad esso.
var ns: NetStream = new NetStream (netConnection, farID); ns.play (nome);
Nota come avevamo bisogno di un farID per il sottoscrittore, e non per il publisher. Siamo in grado di creare tutti i flussi di pubblicazione che vogliamo e tutto ciò che faranno è di sederci e aspettare una connessione. Gli abbonati, d'altra parte, hanno bisogno di sapere con esattezza quale computer nel mondo dovrebbero essere abbonati.
Una volta impostato un flusso di pubblicazione, può essere utilizzato per inviare dati. Il netstream Inviare
Il metodo accetta due argomenti: un nome di "gestore" e un insieme di parametri di lunghezza variabile. Puoi passare qualsiasi oggetto che ti piace come uno di questi parametri, inclusi i tipi di base come Stringa
, int
e Numero
. Gli oggetti complessi sono automaticamente "serializzati", ovvero hanno tutte le loro proprietà registrate sul lato di invio e quindi ricreate sul lato di ricezione. schieramento
s e ByteArray
Anche la copia va bene.
Il nome del gestore corrisponde direttamente al nome di una funzione che verrà eventualmente chiamata sul lato ricevente. L'elenco dei parametri variabili corrisponde direttamente agli argomenti con cui verrà chiamata la funzione di ricezione. Quindi se viene effettuata una chiamata come:
var i: int = 42; netStream.send ("Test", "C'è qualcuno lì?", i);
Il ricevitore deve avere un metodo con lo stesso nome e una firma corrispondente:
funzione pubblica Test (message: String, num: int): void trace (message + num);
Su quale oggetto dovrebbe essere definito questo metodo di ricezione? Qualsiasi oggetto che ti piace. L'istanza NetStream ha una proprietà chiamata cliente
che può accettare qualsiasi oggetto che gli si assegna. Questo è l'oggetto su cui Flash Player cercherà un metodo con il nome di invio corrispondente. Se non c'è alcun metodo con quel nome, o se il numero di parametri non è corretto, o se uno qualsiasi dei tipi di argomento non può essere convertito nel tipo di parametro, un AsyncErrorEvent
sarà licenziato per il mittente.
Consolidiamo le cose che abbiamo imparato finora inserendo tutto in una sorta di struttura. Ecco cosa vogliamo includere:
Per ricevere i dati abbiamo bisogno di un modo per passare un oggetto nel framework che ha funzioni membro che possono essere chiamate in risposta alle corrispondenti chiamate di invio. Piuttosto che un parametro oggetto arbitrario, ho intenzione di codificare un'interfaccia specifica. Inserirò inoltre nell'interfaccia alcuni richiami per i vari eventi di errore che Cirrus può inviare, in questo modo non posso ignorarli.
package import flash.events.ErrorEvent; import flash.events.NetStatusEvent; importazione flash.net.NetStream; interfaccia pubblica ICirrus function onPeerConnect (subscriber: NetStream): Boolean; function onStatus (e: NetStatusEvent): void; function onError (e: ErrorEvent): void;
Voglio che la mia classe Cirrus sia il più facile da usare possibile, quindi voglio nascondere i dettagli di base degli stream e delle connessioni dall'utente. Invece, avrò una classe che funge da mittente o ricevitore e che connette automaticamente Flash Player al servizio Cirrus se un'altra istanza non l'ha già fatto.
package import flash.events.AsyncErrorEvent; import flash.events.ErrorEvent; import flash.events.EventDispatcher; import flash.events.IOErrorEvent; import flash.events.NetStatusEvent; import flash.events.SecurityErrorEvent; import flash.net.NetConnection; importazione flash.net.NetStream; classe pubblica Cirrus private static var netConnection: NetConnection; funzione pubblica get nc (): NetConnection return netConnection; // Connetti al servizio cirrus, o se l'oggetto netConnection non è nullo // presume che siamo già connessi alla funzione statica pubblica Init (key: String): void if (netConnection! = Null) return; netConnection = new NetConnection (); prova netConnection.connect ("rtmfp: //p2p.rtmfp.net", chiave); catch (e: errore) // Impossibile connettersi per motivi di sicurezza, nessun tentativo di riprovare. funzione pubblica Cirrus (chiave: String, iCirrus: ICirrus) Init (chiave); this.iCirrus = iCirrus; netConnection.addEventListener (AsyncErrorEvent.ASYNC_ERROR, OnError); netConnection.addEventListener (IOErrorEvent.IO_ERROR, OnError); netConnection.addEventListener (SecurityErrorEvent.SECURITY_ERROR, OnError) netConnection.addEventListener (NetStatusEvent.NET_STATUS, OnStatus); if (netConnection.connected) netConnection.dispatchEvent (new NetStatusEvent (NetStatusEvent.NET_STATUS, false, false, code: "NetConnection.Connect.Success")); private var iCirrus: ICirrus; public var ns: NetStream = null;
Avremo un metodo per trasformare il nostro oggetto Cirrus in un editore e un altro per trasformarlo in un mittente:
public function Publish (name: String, wrapSendStream: NetStream = null): void if (wrapSendStream! = null) ns = wrapSendStream; else try ns = new NetStream (netConnection, NetStream.DIRECT_CONNECTIONS); catch (e: errore) return; ns.addEventListener (NetStatusEvent.NET_STATUS, OnStatus); ns.addEventListener (AsyncErrorEvent.ASYNC_ERROR, OnError); ns.addEventListener (IOErrorEvent.IO_ERROR, OnError); ns.client = iCirrus; ns.publish (nome, null); public function Play (farId: String, name: String): void try ns = new NetStream (netConnection, farId); catch (e: errore) return; ns.addEventListener (NetStatusEvent.NET_STATUS, OnStatus); ns.addEventListener (AsyncErrorEvent.ASYNC_ERROR, OnError); ns.addEventListener (IOErrorEvent.IO_ERROR, OnError); ns.client = iCirrus; prova ns.play.apply (ns, [nome]); catch (e: errore)
Infine, dobbiamo passare gli eventi all'interfaccia che abbiamo creato:
funzione privata OnError (e: ErrorEvent): void iCirrus.onError (e); funzione privata OnStatus (e: NetStatusEvent): void iCirrus.onStatus (e);
Si consideri il seguente scenario che coinvolge due applicazioni Flash. La prima app ha un ago che ruota costantemente attorno a un cerchio (come una mano sul quadrante di un orologio). Su ogni frame dell'app, la mano viene ruotata un po 'oltre, e anche la nuova angolazione viene inviata su Internet all'app ricevente. L'app ricevente ha un ago, il cui angolo è impostato esclusivamente dall'ultimo messaggio ricevuto dall'app trasmittente. Ecco una domanda: entrambi gli aghi (l'ago per l'app di invio e l'ago per l'app di ricezione) puntano sempre alla stessa posizione? Se hai risposto "sì", ti consiglio vivamente di continuare a leggere.
Costruiamolo e vediamo. Disegneremo un ago semplice come una linea che emana dall'origine (coordinate (0,0)). In questo modo, ogni volta che impostiamo la proprietà di rotazione della forma, l'ago ruoterà sempre come se fosse fissata una estremità, e inoltre potremmo facilmente posizionare la forma dove dovrebbe essere il centro di rotazione:
funzione privata CreateNeedle (x: Number, y: Number, length: Number, col: uint, alpha: Number): Shape var shape: Shape = new Shape (); shape.graphics.lineStyle (2, col, alpha); shape.graphics.moveTo (0, 0); shape.graphics.lineTo (0, -length); // disegna verso l'alto shape.graphics.lineStyle (); shape.x = x; shape.y = y; forma di ritorno;
È scomodo dover installare due computer l'uno accanto all'altro, quindi sul ricevitore useremo due aghi. Il primo (ago rosso) agirà esattamente come nella descrizione sopra, impostando il suo angolo puramente dall'ultimo messaggio ricevuto; il secondo (ago blu) otterrà la sua posizione iniziale dal primo messaggio di rotazione ricevuto, ma poi ruoterà automaticamente nel tempo senza ulteriori messaggi, proprio come fa l'ago mittente. In questo modo, possiamo vedere qualsiasi discrepanza tra dove dovrebbe essere l'ago e dove i messaggi di rotazione ricevuti dicono che dovrebbe essere, tutto avviando entrambe le app e quindi visualizzando solo l'app ricevente.
private var first: Boolean = true; // Chiamato dal netstream ricevente quando viene inviato un messaggio public function Data (value: Number): void shapeNeedleB.rotation = value; if (first) shapeNeedleA.rotation = value; primo = falso; private var dateLast: Date = null; funzione privata OnEnterFrame (e: Event): void if (dateLast == null) dateLast = new Date (); // Calcola la quantità di tempo trascorso dall'ultimo fotogramma. var dateNow: Date = new Date (); var s: Number = (dateNow.time - dateLast.time) / 1000; dateLast = dateNow; // L'ago A è sempre avanzato su ciascun fotogramma. // Ma se è collegato un flusso di ricezione, // trasmette anche il valore della rotazione. shapeNeedleA.rotation + = 360 * (s / 4); if (cirrus.ns.peerStreams.length! = 0) cirrus.ns.send ("Data", shapeNeedleA.rotation);
Avremo un campo di testo sull'app che consente all'utente di inserire un farID a cui connettersi. Se l'app viene avviata senza inserire un farID, si imposterà come editore. Questo copre praticamente la creazione dell'app che vedi nella parte superiore della pagina. Se apri due finestre del browser, puoi copiare l'ID da una finestra all'altra e impostare un'app per iscriverti all'altra. Funzionerà in realtà per due computer collegati a Internet, ma avrai bisogno di un modo per copiare sul prossimoID dell'abbonato.
Se si eseguono sia il mittente che il ricevente sullo stesso computer, le informazioni sulla rotazione dell'ago non hanno molto da percorrere. Infatti, i pacchetti di dati inviati dal mittente non devono nemmeno toccare la rete locale perché sono destinati alla stessa macchina. In condizioni reali i dati devono fare molti salti da computer a computer e con ogni hop introdotto, aumenta la probabilità di problemi.
La latenza è uno di questi problemi. Più i dati devono fisicamente viaggiare, più tempo ci vorrà per arrivare. Per un computer con sede a Londra, i dati impiegheranno meno tempo per arrivare da New York (un quarto del mondo) rispetto a Sydney (a metà strada in tutto il mondo). Anche la congestione della rete è un problema. Quando un dispositivo su Internet funziona al punto di saturazione e gli viene chiesto di trasferire un altro pacchetto, non può fare altro che scartarlo. Il software che utilizza Internet deve quindi rilevare il pacchetto perso e chiedere al mittente un'altra copia, il tutto che aggiunge lag al sistema. A seconda di ogni estremità della posizione della connessione nel mondo, ora del giorno e larghezza di banda disponibile, la qualità della connessione varierà ampiamente.
Quindi, come speri di testare tutti questi diversi scenari? L'unica risposta pratica è di non uscire e cercare di trovare tutte queste condizioni diverse, ma di ricreare una determinata condizione come richiesto. Questo può essere ottenuto usando qualcosa chiamato "emulatore WAN".
Un emulatore WAN (Wide Area Network) è un software che interferisce con il traffico di rete che viaggia verso e dalla macchina su cui è in esecuzione, in modo tale da tentare di ricreare condizioni di rete diverse. Ad esempio, semplicemente scartando i pacchetti di rete trasmessi da una macchina, è possibile emulare la perdita di pacchetti che potrebbe verificarsi in qualche fase della trasmissione dei dati nel mondo reale. Ritardando i pacchetti di una certa quantità prima che vengano inviati dalla scheda di rete, è possibile simulare vari livelli di latenza.
Esistono vari emulatori WAN, per varie piattaforme (Windows, Mac, Linux), tutti concessi in licenza in vari modi. Per il resto di questo articolo userò l'emulatore di connessione Softperfect per Windows per due motivi: è facile da usare e ha una prova gratuita.
(L'autore e Tuts + non sono in alcun modo affiliati con il prodotto menzionato.Uso a proprio rischio.)
Una volta installato e in esecuzione l'emulatore WAN, è possibile testarlo facilmente scaricando qualche tipo di streaming (ad esempio radio Internet o streaming video) e aumentando gradualmente la quantità di perdita di pacchetti. Inevitabilmente la riproduzione si interromperà quando la perdita di pacchetti raggiunge un valore critico che dipende dalla larghezza di banda e dalla dimensione del flusso.
Oh, e ti preghiamo di notare i seguenti punti:
Nello stato normale vedrai gli aghi rosso e blu puntare quasi alla stessa posizione, forse con l'ago rosso che ogni tanto guizza quando cade dietro, poi improvvisamente si riprende. Ora se imposti il tuo emulatore WAN al 2% di perdita di pacchetti vedrai l'effetto diventare molto più pronunciato: all'incirca ogni secondo vedrai lo stesso sfarfallio. Questo è letteralmente ciò che accade quando il pacchetto che trasporta le informazioni di rotazione viene perso: l'ago rosso si siede e attende il prossimo pacchetto. Immagina come sarebbe se l'applicazione non stesse trasferendo la rotazione dell'ago, ma la posizione di qualche altro giocatore in una partita multiplayer: il personaggio balbettava ogni volta che si spostava in una nuova posizione.
In condizioni avverse ci si può aspettare (e quindi dovrebbe progettare) fino al 10% di perdita di pacchetti. Prova questo con il tuo emulatore WAN e potresti intravedere un secondo fenomeno. Chiaramente l'effetto di balbuzie è più pronunciato - ma se guardi da vicino, noterai che quando l'ago scende molto indietro, in realtà non si aggancia alla posizione corretta ma deve "riavvolgere" velocemente di nuovo.
Nell'esempio del gioco questo è indesiderabile per due ragioni. In primo luogo, sembrerà strano vedere un personaggio non solo balbuziente, ma poi uno zoom positivo verso la sua posizione prevista. Secondo, se tutto ciò che vogliamo vedere è un personaggio giocatore nella sua posizione corrente, allora non ci interessano tutte quelle posizioni intermedie: vogliamo solo la posizione più recente quando il pacchetto viene perso e poi ritrasmesso. Tutte le informazioni tranne la più recente sono una perdita di tempo e larghezza di banda.
Imposta la perdita di pacchetti a zero e vedremo la latenza. È improbabile che, nelle condizioni del mondo reale, tu possa migliorare ancora con una latenza di 30 ms, quindi imposta il tuo emulatore WAN. Quando si attiva l'emulazione si noterà che l'ago si arresta in un certo senso, poiché ciascun endpoint si riconfigura automaticamente alla nuova velocità di rete. Quindi, l'ago si riavvicinerà fino a quando non sarà costantemente indietro rispetto a dove dovrebbe essere. In effetti i due aghi sembreranno solidi come roccia: leggermente distanti l'uno dall'altro mentre ruotano. Impostando diverse quantità di latenza, 30ms, 60ms, 90ms, puoi praticamente controllare quanto distano gli aghi.
Immagina di nuovo il gioco per computer con il personaggio del giocatore sempre distante da dove dovrebbero essere. Ogni volta che miri al giocatore e prendi un colpo che ti mancherà, perché ogni volta che schieri il tiro, stai guardando dove si trovava il giocatore, e non dove sono ora. Più è grave la latenza, più è evidente il problema. I giocatori con connessioni Internet scadenti potrebbero essere, per tutti gli scopi, invulnerabili!
Non ci sono molte soluzioni rapide nella vita quindi è un piacere mettere in relazione il seguente. Quando abbiamo esaminato la perdita di pacchetti, abbiamo visto come l'ago si muoverebbe notevolmente in avanti mentre raggiungeva la rotazione prevista dopo una perdita di informazioni. La ragione di ciò è che dietro le quinte di ogni pacchetto inviato era associato un numero seriale che indicava il suo ordine.
In altre parole, se il mittente dovesse inviare 4 pacchetti ...
A, B, C, D
E se uno, diciamo che "B" è perso nella trasmissione in modo che il ricevitore ottenga ...
A, C, D
... lo stream di ricezione può passare immediatamente "A" all'app, ma deve informare il mittente di questo pacchetto mancante, attendere che venga nuovamente ricevuto, quindi passare la "copia ritrasmessa di B", "C", " D'. Il vantaggio di questo sistema è che i messaggi verranno sempre ricevuti nell'ordine in cui sono stati inviati e che qualsiasi informazione mancante viene compilata automaticamente. Lo svantaggio è che la perdita di un singolo pacchetto causa ritardi relativamente grandi nella trasmissione.
Nell'esempio del gioco al computer discusso (dove stiamo aggiornando la posizione del personaggio del giocatore in tempo reale), nonostante non voglia realmente perdere informazioni, è meglio aspettare che arrivi il prossimo pacchetto piuttosto che prendersi il tempo per dire al mittente e attendere la ritrasmissione. Al momento dell'arrivo del pacchetto "B" sarà già stato sostituito dai pacchetti "C" e "D", e i dati contenuti saranno stantii.
A partire da Flash Player 10.1, una proprietà è stata aggiunta alla classe NetStream per controllare solo questo tipo di comportamento. È usato così:
funzione pubblica SetRealtime (ns: NetStream): void ns.dataReliable = false; ns.bufferTime = 0;
In particolare è il dataReliable
proprietà che è stata aggiunta, ma per ragioni tecniche dovrebbe essere sempre utilizzata in concomitanza con l'impostazione bufferTime
proprietà a zero. Se modifichi il codice per impostare i flussi di invio e ricezione in questo modo ed esegui un altro test sulla perdita di pacchetti, noterai che l'effetto di avvolgimento scompare.
Questo è un inizio, ma lascia comunque un ago molto nervoso. Il problema è che la posizione dell'ago ricevente è completamente in balia dei messaggi ricevuti. Anche a una perdita di pacchetti pari al 10% la maggior parte delle informazioni viene ancora ricevuta, tuttavia dal momento che graficamente l'app dipende tanto da un flusso regolare e regolare di messaggi, qualsiasi leggera discrepanza si manifesta immediatamente.
Sappiamo come dovrebbe apparire la rotazione; perché non "riempire" semplicemente le informazioni mancanti per riempire le crepe? Inizieremo con una classe come la seguente che ha due metodi, uno per l'aggiornamento con la rotazione più corrente, uno per la lettura della rotazione corrente:
public class Msg public function Write (valore: Number, date: Date): void public function Read (): Number
Ora il processo è stato "disaccoppiato". Ogni frame possiamo chiamare il Leggere()
metodo e aggiornare la rotazione della forma. Come e quando arrivano nuovi messaggi, possiamo chiamare il Scrivi()
metodo per aggiornare la classe con le informazioni più recenti. Regoleremo anche l'app in modo che non riceva solo la rotazione, ma il momento in cui è stata inviata la rotazione.
Viene chiamato il processo di compilazione dei valori mancanti da quelli noti interpolazione. L'interpolazione è un argomento vasto che assume molte forme, quindi tratteremo un sottoinsieme chiamato Interpolazione lineare, o "Lerping". A livello di programmazione, assomiglia a questo:
funzione pubblica Lerp (a: Number, b: Number, x: Number): Number return a + ((b - a) * x);
A e B sono due valori qualsiasi; X è solitamente un valore compreso tra zero e uno. Se X è zero, il metodo restituisce A. Se X è uno, il metodo restituisce B. Per i valori frazionari tra zero e uno, il metodo restituisce valori part-way tra A e B - quindi un valore X di 0.25 restituisce un valore 25% del modo da A a B.
In altre parole, se alle 13:00 hanno guidato 5 miglia e alle 2:00 ho guidato 60 miglia, poi alle 13:30 ho guidato Lerp (5, 60, 0.5)
miglia. Accade che potrei aver accelerato, rallentato e aspettato nel traffico in varie parti del viaggio, ma la funzione di interpolazione non può tener conto di ciò in quanto ha solo due valori su cui lavorare. Pertanto il risultato è un'approssimazione lineare e non una risposta esatta.
// Tenere 2 valori recenti da interpolare. valore var privatoA: Number = NaN; valore var privatoB: Number = NaN; // E le istanze nel tempo a cui i valori si riferiscono. private var secA: Number = NaN; private var secB: Number = NaN; funzione pubblica Scrivi (valore: numero, data: data): void var secC: Number = date.time / 1000.0; // Se il nuovo valore è ragionevolmente distante dall'ultimo //, imposta a come b e b come nuovo valore. if (isNaN (secB) || secC -secB> 0.1) valoreA = valoreB; secA = secB; valoreB = valore; secB = secC; public function Read (): Number if (isNaN (valueA)) restituisce valoreB; var secC: Number = new Date (). time / 1000.0; var x: Number = (secC-secA) / (secB-secA); return Lerp (valoreA, valoreB, x);
Se si implementa il codice sopra, si noterà che funziona quasi correttamente, ma sembra avere una sorta di problema tecnico: ogni volta che l'ago esegue una rotazione, appare improvvisamente indietro nella direzione opposta. Ci siamo persi qualcosa? La documentazione per la proprietà di rotazione del DisplayObject
la classe rivela quanto segue:
Indica la rotazione dell'istanza DisplayObject, in gradi, dal suo orientamento originale. I valori da 0 a 180 rappresentano la rotazione in senso orario; i valori da 0 a -180 rappresentano la rotazione in senso antiorario. I valori al di fuori di questo intervallo vengono aggiunti o sottratti da 360 per ottenere un valore compreso nell'intervallo.
È stato ingenuo - abbiamo ipotizzato una singola linea numerica dalla quale potremmo scegliere due punti e interpolare. Invece non abbiamo a che fare con una linea ma con a cerchio di valori. Se superiamo +180, torniamo a -180. Ecco perché l'ago si stava comportando in modo strano. Dobbiamo ancora interpolare, ma abbiamo bisogno di una forma di interpolazione che possa avvolgere correttamente attorno a un cerchio.
Immagina di guardare due immagini separate di qualcuno in sella a una bicicletta. Nella prima immagine i pedali sono posizionati verso la parte superiore della bici; nella seconda immagine i pedali sono posizionati verso la parte anteriore della bici. Da queste due immagini e senza ulteriori conoscenze non è possibile capire se il ciclista sta pedalando avanti o indietro. I pedali avrebbero potuto avanzare di un quarto di cerchio in avanti, o di tre quarti di un cerchio all'indietro. Come succede, nell'app che abbiamo creato, gli aghi stanno sempre "pedalando" in avanti, ma vorremmo codice per il caso generale.
Il modo standard per risolverlo è quello di presumere che la distanza più breve attorno al cerchio sia la direzione corretta e sperare anche che gli aggiornamenti arrivino abbastanza velocemente da far sì che ci sia meno di mezzo cerchio di differenza tra ogni aggiornamento. Potresti aver avuto l'esperienza di giocare a un gioco di guida multiplayer in cui l'auto di un altro giocatore ha momentaneamente ruotato in un modo apparentemente impossibile - questo è il motivo per cui.
var min: Number = -180; var max: Number = +180; // Possiamo 'aggiungere' o 'sottrarre' il nostro cerchio attorno al cerchio // dando due diverse misure di distanza var difAdd: Number = (b> a)? b-a: (max-a) + (b-min); var difSub: Number = (b < a)? a-b : (a-min) + (max-b);
Se 'difAdd' è più piccolo di 'difSub', inizieremo da 'a' e aggiungiamo ad esso un'interpolazione lineare dell'importo X. Se 'difSub' è la distanza minore, inizieremo da 'a' e sottrai da è un'interpolazione lineare della quantità X. Potenzialmente questo potrebbe dare un valore che è al di fuori della gamma 'min' e 'max', quindi useremo qualche aritmetica modulare per ottenere un valore che è di nuovo nel range. L'insieme completo di calcoli ha il seguente aspetto:
// Una funzione che fornisce un risultato simile all'operatore% // mod, ma per il valore float. funzione pubblica Mod (val: Number, div: Number): Number return (val - Math.floor (val / div) * div); // Garantisce che i valori al di fuori dell'intervallo minimo / massimo // avvolg