Het bouwen van real-time netwerkgames en -toepassingen kan een uitdaging zijn. In deze zelfstudie leert u hoe u met Cirrus flash-clients kunt verbinden en kunt u kennismaken met enkele vitale technieken.
Laten we eens kijken naar het uiteindelijke resultaat waar we naartoe zullen werken. Klik op de startknop in de bovenstaande SWF om een verzendingsversie van de toepassing te maken. Open deze zelfstudie opnieuw in een tweede browservenster, kopieer de nearId van het eerste venster naar het tekstvak en klik vervolgens op Start om een 'ontvangende' versie van de applicatie te maken.
In de 'ontvangende' versie zie je twee roterende naalden: één rood, één blauw. De blauwe naald roteert uit zichzelf, met een constante snelheid van 90 ° / seconde. De rode naald roteert om overeen te komen met de hoek die wordt verzonden door de verzendingsversie.
(Als de rode naald bijzonder laggy lijkt, probeer dan de browservensters te verplaatsen zodat u beide SWF's tegelijk kunt zien. Flash Player voert EnterFrame-gebeurtenissen veel lager uit wanneer het browservenster op de achtergrond staat, dus verzendt het venster 'verzenden' de nieuwe hoek veel minder vaak.)
Allereerst: u hebt een ontwikkelaarsleutel van Cirrus nodig, die u kunt vinden op de Adobe Labs-site. Dit is een tekstreeks die u bij registratie uniek is toegewezen. Je zult dit gebruiken in alle programma's die je schrijft om toegang te krijgen tot de service, dus het kan het beste zijn om het als een constante te definiëren in een van je AS-bestanden, zoals dit:
public static const CIRRUS_KEY: String = "";
Merk op dat het elk ontwikkelaar- of ontwikkelingsteam is dat zijn eigen sleutel nodig heeft, niet elke gebruiker van de applicaties die u maakt.
We beginnen met het maken van een netwerkverbinding met behulp van een instantie van de (je raadt het al) NetConnection
klasse. Dit wordt bereikt door de aansluiten()
methode met uw eerder genoemde sleutel, en de URL van een Cirrus 'rendezvous' server. Aangezien op het moment van schrijven Cirrus een gesloten protocol gebruikt, is er slechts één zo'n server; zijn adres is RTMFP: //p2p.rtmfp.net
public class Cirrus public static const CIRRUS_KEY: String = ""private static var netConnection: NetConnection; public static function Init (key: String): void if (netConnection! = null) return; netConnection = new NetConnection (); try netConnection.connect (" rtmfp: //p2p.rtmfp .net ", sleutel); catch (e: Error)
Omdat er niets onmiddellijk gebeurt in netwerkcommunicatie, is de netConnection
object zal je laten weten wat het doet door gebeurtenissen te schieten, met name de NetStatusEvent
. De belangrijke informatie wordt bewaard in de code
eigendom van de evenementen info
voorwerp.
privéfunctie OnStatus (e: NetStatusEvent): void switch (e.info.code) case "NetConnection.Connect.Success": pauze; // De verbindingspoging is geslaagd. case "NetConnection.Connect.Closed": pauze; // De verbinding is succesvol afgesloten. case "NetConnection.Connect.Failed": pauze; // De verbindingspoging is mislukt.
Een mislukte verbindingspoging is meestal te wijten aan het feit dat bepaalde poorten worden geblokkeerd door een firewall. Als dit het geval is, hebt u geen andere keuze dan de fout te melden aan de gebruiker, omdat deze geen verbinding met iemand zal maken totdat de situatie verandert. Succes, aan de andere kant, beloont je met de jouwe nearID
. Dit is een tekenreekseigenschap van het NetConnection-object dat die specifieke NetConnection vertegenwoordigt, op die specifieke Flash Player, op die specifieke computer. Geen enkel ander NetConnection-object in de wereld heeft hetzelfde nearID.
Het nearID is als je eigen persoonlijke telefoonnummer - mensen die met je willen praten, moeten het weten. Het omgekeerde is ook waar: je kunt geen verbinding maken met iemand anders zonder de nearID te kennen. Wanneer u iemand anders uw nearID verstrekt, zullen ze deze gebruiken als een farID
: de farID is de ID van de client waarmee u verbinding probeert te maken. Als iemand anders je bijna-id geeft, kun je het als een farID gebruiken om verbinding mee te maken. Snap je?
Dus alles wat we moeten doen is verbinding maken met een klant en hen om hun nearID vragen, en dan ... oh wacht. Hoe komen we hun nearID (om te gebruiken als onze farID) te weten als we niet met elkaar verbonden zijn? Het antwoord, dat je zult verbazen om te horen, is dat het onmogelijk is. U hebt een soort van service van derden nodig om de id's om te wisselen. Voorbeelden zijn:
NetGroup
s, waar we misschien naar kijken in een toekomstige tutorialDe netwerkverbinding is puur conceptueel en helpt ons niet veel nadat de verbinding tot stand is gebracht. Om daadwerkelijk gegevens van het ene uiteinde van de verbinding over te zetten naar een ander dat we gebruiken NetStream
voorwerpen. Als een netwerkverbinding kan worden gezien als het bouwen van een spoorlijn tussen twee steden, dan is een NetStream een mailtrein die echte berichten over de baan vervoert.
NetStreams zijn eenrichtingsverkeer. Eenmaal aangemaakt, fungeren ze als een uitgever (verzenden van informatie) of een abonnee (ontvangen van informatie). Als u wilt dat een enkele client zowel informatie verzendt als ontvangt via een verbinding, hebt u daarom in elke client twee NetStreams nodig. Eenmaal gemaakt, kan een NetStream mooie dingen doen zoals audio en video streamen, maar in deze tutorial houden we ons aan eenvoudige gegevens.
Als, en alleen als, ontvangen we een NetStatusEvent
van de NetConnection met een code van NetConnection.Connect.Success
, we kunnen een NetStream-object maken voor die verbinding. Voor een publisher bouwt u eerst de stream op met behulp van een verwijzing naar het netConnection-object dat we zojuist hebben gemaakt, en de speciale vooraf gedefinieerde waarde. Ten tweede, bel publiceren()
op de stroom en geef het een naam. De naam kan alles zijn wat je wilt, het is er alleen voor een abonnee om te differentiëren tussen meerdere streams afkomstig van dezelfde klant.
var ns: NetStream = nieuwe NetStream (netConnection, NetStream.DIRECT_CONNECTIONS); ns.publish (naam, null);
Om een abonnee te maken, geeft u opnieuw het netConnection-object door aan de constructor, maar deze keer geeft u ook de farID door van de client waarmee u verbinding wilt maken. Ten tweede, bel spelen()
met de naam van de stream die overeenkomt met de naam van de publicatiestroom van de andere client. Om het anders uit te drukken, als u een stream publiceert met de naam 'Test', moet de abonnee de naam 'Test' gebruiken om er verbinding mee te maken.
var ns: NetStream = nieuwe NetStream (netConnection, farID); ns.play (name);
Merk op hoe we een farID nodig hadden voor de abonnee en niet voor de uitgever. We kunnen zoveel publicatiestreams maken als we willen en alles wat ze doen, is daar zitten en wachten op een verbinding. Abonnees moeten daarentegen precies weten op welke computer ze zich moeten abonneren.
Zodra een publicatiestream is ingesteld, kan deze worden gebruikt om gegevens te verzenden. De netstream Sturen
methode neemt twee argumenten: een 'handler'-naam en een reeks parameters met variabele lengte. U kunt elk gewenst object als een van deze parameters doorgeven, inclusief basistypen zoals Draad
, int
en Aantal
. Complexe objecten worden automatisch 'geserialiseerd' - dat wil zeggen, ze hebben al hun eigenschappen vastgelegd aan de verzendende kant en vervolgens opnieuw gemaakt aan de ontvangende kant. reeks
s en ByteArray
s kopie ook prima.
De naam van de handler komt rechtstreeks overeen met de naam van een functie die uiteindelijk aan de ontvangende kant wordt gebeld. De variabele parameterlijst komt direct overeen met de argumenten waarmee de ontvangende functie zal worden aangeroepen. Dus als een oproep wordt gedaan, zoals:
var i: int = 42; netStream.send ("Test", "Is er iemand?", i);
De ontvanger moet een methode hebben met dezelfde naam en een bijbehorende handtekening:
openbare functie Test (bericht: String, num: int): void trace (message + num);
Op welk object moet deze ontvangstmethode worden gedefinieerd? Elk object dat je leuk vindt. De NetStream-instantie heeft een eigenschap genaamd cliënt
die elk voorwerp kan accepteren dat je eraan toewijst. Dat is het object waarop de Flash Player zoekt naar een methode voor de overeenkomstige verzendnaam. Als er geen methode met die naam is of als het aantal parameters onjuist is of als een van de argumenttypen niet naar het parametertype kan worden geconverteerd, AsyncErrorEvent
zal worden afgevuurd voor de afzender.
Laten we de dingen consolideren die we tot nu toe hebben geleerd door alles in een soort kader te plaatsen. Dit is wat we willen opnemen:
Om gegevens te ontvangen, hebben we een manier nodig om een object door te geven in het raamwerk dat lidfuncties heeft die kunnen worden aangeroepen als antwoord op de bijbehorende uitgaande oproepen. In plaats van een willekeurige objectparameter, ga ik een specifieke interface coderen. Ik ga ook enkele callbacks in de interface plaatsen voor de verschillende foutgebeurtenissen die Cirrus kan verzenden - op die manier kan ik ze niet negeren.
pakket import flash.events.ErrorEvent; import flash.events.NetStatusEvent; import flash.net.NetStream; openbare interface ICirrus function onPeerConnect (subscriber: NetStream): Boolean; function onStatus (e: NetStatusEvent): void; function onError (e: ErrorEvent): void;
Ik wil dat mijn Cirrus-klasse zo gebruiksvriendelijk mogelijk is, dus ik wil de basisgegevens van streams en verbindingen van de gebruiker verbergen. In plaats daarvan heb ik een klasse die fungeert als een afzender of ontvanger en die de Flash Player automatisch verbindt met de Cirrus-service als een ander exemplaar dat nog niet heeft gedaan.
pakket 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; import flash.net.NetStream; public class Cirrus private static var netConnection: NetConnection; public function get nc (): NetConnection return netConnection; // Verbinding maken met de cirrus-service of als het netConnection-object niet nul is // aannemen dat we al verbonden zijn public static-functie Init (key: String): void if (netConnection! = Null) return; netConnection = nieuwe NetConnection (); probeer netConnection.connect ("rtmfp: //p2p.rtmfp.net", sleutel); catch (e: Error) // Kan geen verbinding maken om veiligheidsredenen, geen punt opnieuw proberen. openbare functie Cirrus (key: String, iCirrus: ICirrus) Init (key); 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 (nieuwe NetStatusEvent (NetStatusEvent.NET_STATUS, false, false, code: "NetConnection.Connect.Success")); private var iCirrus: ICirrus; public var ns: NetStream = null;
We zullen één methode hebben om van ons Cirrus-object een uitgever te maken, en een ander om er een afzender van te maken:
public function Publish (naam: String, wrapSendStream: NetStream = null): void if (wrapSendStream! = null) ns = wrapSendStream; else try ns = nieuwe NetStream (netConnection, NetStream.DIRECT_CONNECTIONS); catch (e: Error) return; ns.addEventListener (NetStatusEvent.NET_STATUS, OnStatus); ns.addEventListener (AsyncErrorEvent.ASYNC_ERROR, OnError); ns.addEventListener (IOErrorEvent.IO_ERROR, OnError); ns.client = iCirrus; ns.publish (naam, null); public function Play (farId: String, name: String): void try ns = new NetStream (netConnection, farId); catch (e: Error) return; ns.addEventListener (NetStatusEvent.NET_STATUS, OnStatus); ns.addEventListener (AsyncErrorEvent.ASYNC_ERROR, OnError); ns.addEventListener (IOErrorEvent.IO_ERROR, OnError); ns.client = iCirrus; probeer ns.play.apply (ns, [naam]); catch (e: Error)
Ten slotte moeten we de gebeurtenissen doorgeven aan de interface die we hebben gemaakt:
persoonlijke functie OnError (e: ErrorEvent): void iCirrus.onError (e); private functie OnStatus (e: NetStatusEvent): void iCirrus.onStatus (e);
Overweeg het volgende scenario met twee Flash-toepassingen. De eerste app heeft een naald die gestaag ronddraait in een cirkel (zoals een wijzer op een wijzerplaat). Op elk frame van de app wordt de hand iets verder gedraaid en wordt de nieuwe hoek ook via internet naar de ontvangende app verzonden. De ontvangende app heeft een naald, waarvan de hoek puur is ingesteld op basis van het laatste bericht dat is ontvangen van de verzendende app. Hier is een vraag: verwijzen beide naalden (de naald voor de verzendende app en de naald voor de ontvangende app) altijd naar dezelfde positie? Als je 'ja' hebt geantwoord, raad ik je aan om verder te lezen.
Laten we het bouwen en zien. We tekenen een eenvoudige naald als een lijn die van de oorsprong afkomstig is (coördinaten (0,0)). Op deze manier, telkens wanneer we de rotatie-eigenschap van de vorm instellen, zal de naald altijd roteren alsof een uiteinde vast is, en we kunnen de vorm ook gemakkelijk positioneren op de plek waar het rotatiecentrum moet zijn:
persoonlijke functie 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, -lengte); // teken naar boven gericht shape.graphics.lineStyle (); shape.x = x; shape.y = y; vorm teruggeven;
Het is lastig om twee computers naast elkaar te moeten plaatsen, dus op de ontvanger gebruiken we eigenlijk twee naalden. De eerste (rode naald) werkt net zoals in de bovenstaande beschrijving, waarbij de hoek zuiver is ingesteld op het laatste ontvangen bericht; de tweede (blauwe naald) krijgt zijn beginpositie vanaf het ontvangen eerste rotatiebericht, maar roteert vervolgens automatisch in de loop van de tijd zonder verdere berichten, net als de zendnaald. Op deze manier kunnen we een verschil zien tussen waar de naald moet zijn en waar de ontvangen rotatieberichten zeggen dat het moet zijn, allemaal door beide apps te starten en alleen de ontvangende app te bekijken.
private var first: Boolean = true; // Wordt aangeroepen door de ontvangende netwerkstroom wanneer een bericht wordt verzonden openbare functie Data (waarde: Number): void shapeNeedleB.rotation = value; if (eerste) shapeNeedleA.rotation = waarde; first = false; private var dateLast: Date = null; private function OnEnterFrame (e: Event): void if (dateLast == null) dateLast = new Date (); // Bereken de hoeveelheid tijd die is verstreken sinds het laatste frame. var dateNow: Date = new Date (); var s: Number = (dateNow.time - dateLast.time) / 1000; dateLast = dateNow; // Naald A wordt altijd op elk frame vooruit geschoven. // Maar als er een ontvangststroom is bijgevoegd, // verzendt ook de waarde van de rotatie. shapeNeedleA.rotation + = 360 * (s / 4); if (cirrus.ns.peerStreams.length! = 0) cirrus.ns.send ("Data", shapeNeedleA.rotation);
We hebben een tekstveld op de app waarmee de gebruiker een farID kan invoeren om verbinding mee te maken. Als de app wordt gestart zonder een farID in te voeren, zal deze zichzelf als publisher instellen. Dat komt neer op het maken van de app die je bovenaan de pagina ziet. Als u twee browservensters opent, kunt u de id van het ene venster naar het andere kopiëren en één app instellen om u te abonneren op het andere. Het werkt eigenlijk voor elke twee computers die op het internet zijn aangesloten, maar je hebt een manier nodig om te kopiëren via de nearID van de abonnee.
Als u zowel de zender als de ontvanger op dezelfde computer uitvoert, hoeft de rotatie-informatie voor de naald niet ver te reizen. De gegevenspakketten die door de afzender worden verzonden, hoeven zelfs helemaal niet het lokale netwerk aan te raken omdat ze voor dezelfde machine zijn bestemd. In de praktijk moeten de gegevens veel hops van computer naar computer maken en bij elke introductie wordt de kans op problemen groter..
Latency is zo'n probleem. Hoe verder de gegevens fysiek moeten reizen, hoe langer het duurt om aan te komen. Voor een computer in Londen kost het minder tijd om vanuit New York (een kwart van de wereld) rond te komen dan vanuit Sydney (halverwege de wereld). Netwerkcongestie is ook een probleem. Wanneer een apparaat op internet op verzadigingspunt werkt en wordt gevraagd om nog een ander pakket over te zetten, kan het niets anders doen dan het weggooien. Software die internet gebruikt, moet vervolgens het verloren pakket detecteren en de afzender om een ander exemplaar vragen, wat allemaal vertraging in het systeem toevoegt. Afhankelijk van elk uiteinde van de locatie van de verbinding in de wereld, het tijdstip en de beschikbare bandbreedte, varieert de kwaliteit van de verbinding sterk..
Dus hoe hoop je te testen voor al deze verschillende scenario's? Het enige praktische antwoord is om niet uit te gaan en al deze verschillende condities te vinden, maar om een gegeven conditie naar behoefte opnieuw te creëren. Dit kan worden bereikt met behulp van iets dat een 'WAN-emulator' wordt genoemd.
Een WAN-emulator (Wide Area Network) is software die interfereert met het netwerkverkeer dat reist van en naar de computer waarop het wordt uitgevoerd, op een manier die probeert verschillende netwerkomstandigheden na te bootsen. Door netwerkpakketten die worden verzonden vanaf een machine eenvoudigweg te verwijderen, kan deze bijvoorbeeld het pakketverlies emuleren dat zich op enig moment in de real-world overdracht van de gegevens kan voordoen. Door pakketten met een bepaalde hoeveelheid te vertragen voordat ze door de netwerkkaart worden verzonden, kan het verschillende niveaus van latentie simuleren.
Er zijn verschillende WAN-emulators voor verschillende platforms (Windows, Mac, Linux), allemaal met een licentie op verschillende manieren. Voor de rest van dit artikel ga ik de Softperfect Connection Emulator voor Windows gebruiken om twee redenen: het is eenvoudig te gebruiken en het heeft een gratis proefversie.
(De auteur en Tuts + zijn op geen enkele manier verbonden aan het genoemde product. Gebruik op eigen risico.)
Zodra uw WAN-emulator is geïnstalleerd en wordt uitgevoerd, kunt u deze eenvoudig testen door een soort van stream (zoals internetradio of streaming video) te downloaden en de hoeveelheid pakketverlies geleidelijk te verhogen. Het is onvermijdelijk dat het afspelen stopt als het pakketverlies een of andere kritische waarde bereikt die afhangt van je bandbreedte en de grootte van de stream.
Oh, en let op de volgende punten:
In de normale staat zie je dat de rode en blauwe naalden vrijwel op dezelfde positie wijzen, bijvoorbeeld als de rode naald af en toe flikkert wanneer deze valt en dan plotseling weer inhaalt. Als je nu je WAN-emulator instelt op 2% pakketverlies, zie je dat het effect veel duidelijker wordt: ongeveer elke seconde zie je hetzelfde flikkeren. Dit is letterlijk wat er gebeurt als het pakket met de rotatie-informatie verloren gaat: de rode naald zit gewoon en wacht op het volgende pakket. Stel je voor hoe het eruit zou zien als de app de naaldrotatie niet overdroeg, maar de positie van een andere speler in een multiplayer-spel - het karakter zou elke keer dat het naar een nieuwe positie bewoog stotteren.
In ongunstige omstandigheden die u mag verwachten (en daarom zou moeten ontwerpen voor) tot 10% pakketverlies. Probeer dit met je WAN-emulator en je kunt een glimp opvangen van een tweede fenomeen. Het stottereffect is duidelijk meer uitgesproken - maar als je goed kijkt, zie je dat wanneer de naald een heel eind komt, hij niet echt terug in de juiste positie springt, maar weer snel naar voren moet 'winden'.
In het spelvoorbeeld is dit om twee redenen onwenselijk. Ten eerste zal het vreemd lijken om te zien dat een personage niet alleen stottert maar vervolgens positief naar de beoogde positie zoomt. Ten tweede, als alles wat we willen zien een spelerskarakter is op zijn huidige positie, dan geven we niet om al die tussenposities: we willen alleen de meest recente positie als het pakket verloren gaat en vervolgens opnieuw wordt verzonden. Alle informatie, behalve de meest recente, is verspilling van tijd en bandbreedte.
Stel uw pakketverlies opnieuw in op nul en we zullen kijken naar latency. Het is onwaarschijnlijk dat in realistische omstandigheden je ooit beter zult worden dan ongeveer 30 ms latency, dus stel je WAN Emulator daarvoor in. Wanneer u de emulatie activeert, merkt u dat de naald behoorlijk terugvalt, omdat elk eindpunt zich opnieuw configureert naar de nieuwe netwerksnelheid. De naald zal dan weer inhalen totdat deze consistent ver achterblijft op de plaats waar hij hoort te zijn. In feite zien de twee naalden er rotsvast uit: net iets uit elkaar als ze roteren. Door verschillende hoeveelheden latency in te stellen, 30ms, 60ms, 90ms, kunt u praktisch bepalen hoe ver de naalden zich van elkaar bevinden.
Breng het computerspel opnieuw in beeld met het personage van de speler altijd op enige afstand achter waar ze zouden moeten zijn. Elke keer dat je op de speler richt en een foto neemt, mis je, want elke keer als je de foto maakt, kijk je waar de speler was en niet waar ze nu zijn. Hoe slechter de latentie, hoe duidelijker het probleem. Spelers met slechte internetverbindingen kunnen voor alle doeleinden onkwetsbaar zijn!
Er zijn niet veel snelle oplossingen in het leven, dus het is een genoegen om de volgende te vertellen. Toen we naar pakketverlies keken, zagen we hoe de naald merkbaar naar voren zou spoelen omdat deze na verlies van informatie de beoogde rotatie opliep. De reden hiervoor is dat achter de schermen aan elk pakket een serienummer was toegewezen dat de volgorde aangeeft.
Met andere woorden, als de afzender 4 pakketten zou verzenden ...
A, B, C, D
En als er één is, laten we zeggen dat 'B' bij verzending verloren gaat, zodat de ontvanger ...
A, C, D
... de ontvangende stroom kan 'A' onmiddellijk doorgeven aan de app, maar moet de afzender dan informeren over dit ontbrekende pakket, wachten tot het opnieuw wordt ontvangen en vervolgens 'opnieuw verzonden exemplaar van B', 'C', 'doorgeven' D'. Het voordeel van dit systeem is dat berichten altijd worden ontvangen in de volgorde waarin ze zijn verzonden en dat ontbrekende informatie automatisch wordt ingevuld. Het nadeel is dat het verlies van een enkel pakket relatief grote vertragingen in de transmissie veroorzaakt.
In het besproken computergame-voorbeeld (waar we de positie van het spelerpersonage in realtime bijwerken), is het eigenlijk beter om gewoon te wachten tot het volgende pakket langskomt om de afzender te vertellen. wacht op hertransmissie. Tegen de tijd dat pakket 'B' arriveert, is het al vervangen door de pakketten 'C' en 'D' en de gegevens die het bevat zijn oudbakken.
Vanaf Flash Player 10.1 is een eigenschap aan de NetStream-klasse toegevoegd om precies dit gedrag te regelen. Het wordt als volgt gebruikt:
public function SetRealtime (ns: NetStream): void ns.dataReliable = false; ns.bufferTime = 0;
Concreet is het de dataReliable
eigenschap die is toegevoegd, maar om technische redenen moet deze altijd worden gebruikt in combinatie met het instellen van de buffertijd
eigendom tot nul. Als u de code wijzigt om de verzendende en ontvangende streams op deze manier in te stellen en een andere test op pakketverlies uit te voeren, zult u merken dat het kronkelende effect verdwijnt.
Dat is een begin, maar het laat nog steeds een zeer zenuwachtige naald achter. Het probleem is dat de positie van de ontvangende naald volledig overgeleverd is aan de ontvangen berichten. Bij zelfs 10% pakketverlies wordt de overgrote meerderheid van de informatie nog steeds ontvangen, maar omdat de app zo sterk afhankelijk is van een vloeiende en regelmatige berichtenstroom, wordt elk klein verschil onmiddellijk zichtbaar.
We weten hoe de rotatie eruit zou moeten zien; waarom niet gewoon de ontbrekende informatie 'invullen' over de scheuren? We beginnen met een klasse zoals de volgende die twee methoden heeft, een voor het bijwerken met de meest recente rotatie, een voor het aflezen van de huidige rotatie:
public class Msg public function Write (waarde: Number, date: Date): void public function Read (): Number
Nu is het proces 'ontkoppeld'. Elk frame kunnen we de Lezen()
methode en werk de rotatie van de vorm bij. Wanneer nieuwe berichten binnenkomen, kunnen we de Schrijven()
methode om de klas bij te werken met de nieuwste informatie. We passen de app ook zo aan dat deze niet alleen de rotatie ontvangt, maar ook de tijd waarop de rotatie is verzonden.
Het proces van het invullen van ontbrekende waarden van bekende wordt genoemd interpolatie. Interpolatie is een groot onderwerp dat vele vormen aanneemt, dus we zullen een subset behandelen die genoemd wordt Lineaire interpolatie, of 'Lerping'. Programmatisch ziet het er als volgt uit:
openbare functie Lerp (a: Number, b: Number, x: Number): Number return a + ((b - a) * x);
A en B zijn twee waarden; X is meestal een waarde tussen nul en één. Als X nul is, retourneert de methode A. Als X één is, retourneert de methode B. Voor fractionele waarden tussen nul en één retourneert de methode waarden halfweg tussen A en B - dus een X-waarde van 0,25 retourneert een waarde van 25% van de weg van A naar B.
Met andere woorden, als ik om 13:00 uur 5 uur heb gereden en ik om 14:00 uur 60 kilometer heb gereden, dan heb ik om 13:30 uur gereden Lerp (5, 60, 0,5)
mijl. Het kan zijn dat ik heb versneld, vertraagd en in het verkeer op verschillende delen van de reis heb gewacht, maar de interpolatiefunctie kan daar geen verklaring voor geven omdat het maar twee waarden heeft om vanaf te werken. Daarom is het resultaat een lineaire benadering en geen exact antwoord.
// Houd 2 recente waarden vast om te interpoleren vanaf. private var waardeA: Number = NaN; persoonlijke var-waardeB: Number = NaN; // En de instanties in de tijd waarnaar de waarden verwijzen. private var secA: Number = NaN; private var secB: Number = NaN; public function Write (waarde: Number, date: Date): void var secC: Number = date.time / 1000.0; // Als de nieuwe waarde redelijk ver verwijderd is van de laatste //, stelt u a in als b en b als de nieuwe waarde. if (isNaN (secB) || secC -secB> 0,1) waardeA = waardeB; secA = secB; waardeB = waarde; secB = secC; openbare functie Read (): Number if (isNaN (valueA)) return valueB; var secC: Number = new Date (). time / 1000.0; var x: Number = (secC-secA) / (secB-secA); return Lerp (waardeA, waardeB, x);
Als u de bovenstaande code implementeert, zult u merken dat deze bijna correct werkt, maar een soort glitch lijkt te hebben. Telkens wanneer de naald één rotatie uitvoert, lijkt het erop dat de muis dan plotseling in de tegenovergestelde richting terugkeert. Hebben we iets gemist? De documentatie voor de rotatie-eigenschap van de DisplayObject
klas onthult het volgende:
Geeft de rotatie van de DisplayObject-instantie in graden aan vanuit de oorspronkelijke oriëntatie. Waarden van 0 tot 180 vertegenwoordigen rechtsdraaiend; waarden van 0 tot -180 vertegenwoordigen tegen de klok in rotatie. Waarden buiten dit bereik worden toegevoegd aan of afgetrokken van 360 om een waarde binnen het bereik te verkrijgen.
Dat was naïef - we veronderstelden een enkele getallenlijn waaruit we twee punten konden kiezen en interpoleren. In plaats daarvan hebben we het niet over een regel, maar met een cirkel van waarden. Als we de +180 overschrijden, worden we opnieuw omgedraaid naar -180. Dat is waarom de naald zich vreemd gedroeg. We moeten nog steeds interpoleren, maar we hebben een vorm van interpolatie nodig die zich correct rond een cirkel kan wikkelen.
Stel je voor dat je naar twee afzonderlijke afbeeldingen kijkt van iemand die op een fiets rijdt. In de eerste afbeelding zijn de pedalen naar de bovenkant van de fiets geplaatst; in het tweede beeld zijn de pedalen naar de voorkant van de fiets geplaatst. Alleen al deze twee afbeeldingen en zonder aanvullende kennis is het niet mogelijk om uit te zoeken of de rijder vooruit of achteruit trapt. De pedalen zouden een kwart van een cirkel naar voren kunnen hebben geschoven, of driekwart van een cirkel achteruit. Het gebeurt namelijk dat in de app die we hebben gebouwd, de naalden altijd naar voren 'trappen', maar we willen graag coderen voor de algemene situatie.
De standaardmanier om dit op te lossen is om te veronderstellen dat de kortste afstand rond de cirkel de juiste richting is en ook hopen dat updates snel genoeg binnenkomen zodat er minder dan een halve cirkel verschil is tussen elke update. Je hebt misschien de ervaring gehad een multiplayer-racegame te spelen waarbij de auto van een andere speler even op een schijnbaar onmogelijke manier is geroteerd - dat is de reden waarom.
var min: Number = -180; var max: Number = +180; // We kunnen de cirkel om ons heen 'toevoegen' of 'aftrekken' // twee verschillende maten van afstand var dif toevoegenAdd: Number = (b> a)? b-a: (max-a) + (b-min); var difSub: Number = (b < a)? a-b : (a-min) + (max-b);
Als 'difAdd' kleiner is dan 'difSub', beginnen we bij 'a' en voegen we er een lineaire interpolatie van de hoeveelheid X aan toe. Als 'difSub' de kleinere afstand is, beginnen we bij 'a' en trekken we af van het is een lineaire interpolatie van de hoeveelheid X. Mogelijk geeft dit een waarde op die buiten het bereik 'min' en 'max' ligt, dus we zullen wat modulaire rekenkunde gebruiken om een waarde te krijgen die weer binnen b