Wenn Node.js heute in vollem Gange ist, können wir es bereits verwenden, um alle möglichen Dinge zu tun. Vor einiger Zeit nahm UP -Host am Geek Song Event teil. Während dieser Veranstaltung beabsichtigen wir, ein Spiel zu erstellen, das es den "Head-Down-Leuten" ermöglicht, mehr zu kommunizieren. Die Kernfunktion ist die Echtzeit-Multi-Personen-Interaktion des LAN-Party-Konzepts. Das Rennen der Geeks war nur erbärmlich kurz 36 Stunden, wobei alles schnell und schnell sein musste. Unter einer solchen Prämisse erscheint die erste Vorbereitung ein bisschen "natürlich". Plattformübergreifende Anwendungslösung Wir haben Node-Webkit gewählt, was einfach genug ist und unsere Anforderungen entspricht.
Nach Anforderungen kann unsere Entwicklung nach Modulen getrennt durchgeführt werden. Dieser Artikel beschreibt ausdrücklich den Prozess der Entwicklung des Abstandsraums (unserem Echtzeit-Multiplayer-Spiel-Framework), einschließlich einer Reihe von Erkundungen und Versuchen sowie Lösungen für einige Einschränkungen auf Node.js und Webkit-Plattform selbst sowie den Vorschlag von Lösungen.
Erste Schritte
DABERLAGER
Zu Beginn war das Design von Spaceroom definitiv nachgefragt. Wir hoffen, dass dieser Rahmen die folgenden Grundfunktionen liefern kann:
Kann eine Gruppe von Benutzern basierend auf Räumen (oder Kanälen) unterscheiden.
In der Lage, Anweisungen von Benutzern in der Sammlungsgruppe zu erhalten
Bei der Übereinstimmung zwischen jedem Client können die Spieldaten nach dem angegebenen Intervall genau übertragen werden
Kann die Auswirkungen der Netzwerklatenz so weit wie möglich beseitigen
Natürlich haben wir in den späteren Phasen der Codierung mehr Funktionen zur Verfügung gestellt, einschließlich der Pause des Spiels, der generierenden Zufallszahlen zwischen jedem Kunden usw. (natürlich können diese von selbst nach Anforderungen implementiert werden, und es ist nicht erforderlich, den Abstandsraum zu verwenden, einen Framework, der auf Kommunikationsebene funktioniert, was mehr der Kommunikationsebene ist).
Apis
Der Abstandsraum ist in zwei Teile unterteilt: vordere und hintere Ende. Die vom Server erforderlichen Arbeiten umfassen die Wartung einer Raumliste und die Bereitstellung der Funktionen des Erstellens und der Beitritt zu einem Raum. Unsere Kunden -APIs sehen so aus:
Spaceroom.Connect (Adresse, Rückruf) Verbinden Sie eine Verbindung zum Server
Spaceroom.CreateRoom (Callback) Erstellen Sie einen Raum
Spaceroom.JoInroom (Roomid) Verbinden Sie sich einem Raum
Spaceroom.on (Ereignis, Callback) hört auf Ereignisse zu
...
Nachdem der Client eine Verbindung zum Server hergestellt hat, empfängt er verschiedene Ereignisse. Zum Beispiel kann ein Benutzer in einem Raum eine Veranstaltung erhalten, bei der sich ein neuer Spieler anschließt, oder ein Ereignis, bei dem das Spiel beginnt. Wir geben dem Kunden einen "Lebenszyklus", der jederzeit in einem der folgenden Zustände sein wird:
Sie können den aktuellen Status des Kunden über den Abstandsraum erhalten.
Die Verwendung eines serverseitigen Frameworks ist relativ einfach. Wenn Sie die Standardkonfigurationsdatei verwenden, können Sie das serverseitige Framework direkt ausführen. Wir haben eine grundlegende Anforderung: Der Servercode kann direkt im Client ausgeführt werden, ohne dass ein separater Server erforderlich ist. Spieler, die PS oder PSP gespielt haben, sollten wissen, wovon ich spreche. Natürlich ist es auch hervorragend, auf einem speziellen Server auszuführen.
Die Implementierung des logischen Codes ist hier kurz. Die erste Generation von Spaceroom hat die Funktion eines Socket -Servers abgeschlossen, der eine Liste von Räumen, einschließlich des Status des Raums, und der Spielzeit -Kommunikation, die jedem Raum entspricht (Anleitungssammlung, Bucket -Sendung usw.), verwaltet. Für eine bestimmte Implementierung finden Sie im Quellcode.
Synchronalgorithmus
Wie können wir die Dinge, die zwischen jedem Kunden in Echtzeit konsistent angezeigt werden?
Dieses Ding klingt interessant. Denken Sie sorgfältig darüber nach. Was brauchen wir den Server, um uns zu liefern? Natürlich werden Sie sich vorstellen, was möglicherweise logische Inkonsistenzen zwischen verschiedenen Clients verursachen: Benutzeranweisungen. Da die Codes, die sich mit Spiellogik befassen, bei den gleichen Bedingungen gleich sind, führt der Code das gleiche Ergebnis aus. Der einzige Unterschied sind die verschiedenen Spielerbefehle, die während des Spiels erhalten wurden. Richtig, wir brauchen eine Möglichkeit, diese Anweisungen zu synchronisieren. Wenn alle Clients dieselbe Anweisung erhalten können, können alle Kunden theoretisch das gleiche Betriebsergebnis erzielen.
Die Synchronisationsalgorithmen von Online -Spielen sind alle Arten von seltsamen und anwendbaren Szenarien. SpaceRoom verwendet Synchronisationsalgorithmen, die dem Konzept der Rahmensperrung ähnlich sind. Wir teilen die Zeitleiste nacheinander in Intervalle, und jedes Intervall wird als Eimer bezeichnet. Der Eimer wird verwendet, um Anweisungen zu laden und von der Serverseite zu pflegen. Am Ende jedes Bucket -Zeitraums überträgt der Server den Bucket an alle Clients. Nachdem der Client den Eimer erhalten hat, werden die Anweisungen daraus abgerufen und dann nach der Überprüfung ausgeführt.
Um die Auswirkungen der Netzwerkverzögerung zu verringern, wird jeder vom Server vom Client empfangene Befehl gemäß einem bestimmten Algorithmus an den entsprechenden Eimer geliefert, und die folgenden Schritte werden ausdrücklich befolgt:
Lassen Sie Order_Start die Zeit des vom Befehls getragenen Befehls sein, und T ist die Startzeit des Eimers, in dem sich der Order_Start befindet.
Wenn T + Dely_Time <= Startzeit des derzeit sammelnden Eimers, der derzeit den Anweisungen sammelt, liefern
Liefern Sie die Anweisung an den entsprechenden Eimer von T + Dely_Time
Wenn Delay_Time die vereinbarte Serververzögerungszeit ist, die als durchschnittliche Verzögerung zwischen den Clients angesehen werden kann. Der Standardwert im Spaceroom beträgt 80, und der Standardwert der Bucket -Länge beträgt 48. Am Ende jedes Bucket -Zeitraums überträgt der Server diesen Eimer an alle Clients und erhält Anweisungen für den nächsten Bucket. Der Client steuert den Zeitfehler in einem akzeptablen Bereich, wenn die Übereinstimmung in der Logik automatisch auf dem empfangenen Bucket -Intervall durchgeführt wird.
Dies bedeutet, dass der Client unter normalen Umständen einen Eimer erhält, der alle 48 ms vom Server gesendet wird. Wenn der Zeitpunkt, in dem der Eimer verarbeitet werden muss, wird der Kunde ihn entsprechend behandeln. Unter der Annahme, dass der Client FPS = 60 alle 3 Frames oder so einen Eimer erhält und die Logik gemäß diesem Eimer aktualisiert wird. Wenn der Eimer nach Ablauf der Zeit aufgrund von Netzwerkschwankungen nicht eingegangen ist, macht der Kunde die Spiellogik und wartet. Zu einer Zeit innerhalb eines Eimers kann die Logik mit der LERP -Methode aktualisiert werden.
Bei Delay_Time = 80, Bucket_Size = 48, wird eine der Anweisungen um mindestens 96 ms ausgeführt. Ändern Sie diese beiden Parameter beispielsweise im Fall von Delay_Time = 60, Bucket_Size = 32, einer der Anweisungen wird um mindestens 64 ms verzögert.
Ein blutiger Vorfall, der durch einen Timer verursacht wird
Wenn wir uns das Ganze ansehen, muss unser Rahmen einen genauen Timer haben, wenn es ausgeführt wird. Führen Sie die Sendung des Eimers unter einem festen Intervall aus. Natürlich haben wir zunächst darüber nachgedacht, SetInterval () zu verwenden, aber in der nächsten Sekunde wurde uns klar, wie unzuverlässig diese Idee ist: Der freche setInterval () scheint sehr ernsthafte Fehler zu haben. Und was so schlimm ist, ist, dass sich jeder Fehler ansammelt und immer schwerwiegendere Folgen verursacht.
Deshalb dachten wir sofort daran, setTimeout () zu verwenden, um unsere Logik ungefähr stabil um das angegebene Intervall zu gestalten, indem wir die Zeit der nächsten Ankunft dynamisch korrigierten. Zum Beispiel ist SetTimeout () um 5 ms weniger als erwartet, sodass wir das nächste Mal 5 ms im Voraus lassen. Die Testergebnisse sind jedoch nicht zufriedenstellend, und dies ist nicht elegant genug, egal wie Sie es betrachten.
Wir müssen also unser Denken ändern. Ist es möglich, setTimeout () so schnell wie möglich abzufallen, und dann überprüfen wir, ob die aktuelle Zeit die Zielzeit erreicht hat. Zum Beispiel in unserer Schleife mit SetTimeout (Callback, 1), um die Zeit weiter zu überprüfen, was eine gute Idee erscheint.
Enttäuschender Timer
Wir haben sofort einen Code geschrieben, um unsere Ideen zu testen, und die Ergebnisse waren enttäuschend. Führen Sie in der aktuellen neuesten Node.js Stable -Version (v0.10.32) und der Windows -Plattform dieses Code aus:
Die Codekopie lautet wie folgt:
var sum = 0, count = 0;
Funktionstest () {
var nun = Datum.Now ();
setTimeout (function () {
var diff = date.now () - jetzt;
sum += diff;
zählen ++;
prüfen();
});
}
prüfen();
Geben Sie nach einer Zeitspanne Summe/Anzahl in die Konsole ein und Sie können ein Ergebnis sehen, ähnlich wie bei:
Die Codekopie lautet wie folgt:
> Summe / Anzahl
15.624555160142348
Was?! Ich möchte 1MS -Intervall, aber Sie sagen mir, dass das tatsächliche Durchschnittsintervall 15,625 ms beträgt! Dieses Bild ist einfach zu schön. Wir haben den gleichen Test auf Mac durchgeführt und das Ergebnis betrug 1,4 ms. Also waren wir verwirrt: Was zum Teufel ist das? Wenn ich ein Apple -Fan wäre, hätte ich vielleicht zu dem Schluss gekommen, dass Windows zu Müll war und unter Windows aufgegeben wurde. Glücklicherweise war ich ein strenger Front-End-Ingenieur, also begann ich weiterhin über diese Zahl nachzudenken.
Warten Sie, warum ist diese Nummer so vertraut? Wird diese Zahl dem maximalen Timer -Intervall unter Windows zu ähnlich sein? Ich habe sofort einen Clockres zum Testen heruntergeladen, und nachdem ich die Konsole ausgeführt hatte, erhielt ich die folgenden Ergebnisse:
Die Codekopie lautet wie folgt:
Maximaler Timer -Intervall: 15,625 ms
Minimum Timer -Intervall: 0,500 ms
Aktueller Timerintervall: 1,001 ms
Sicher genug! Wenn wir uns das Handbuch von Node.js ansehen, können wir eine Beschreibung von SetTimeout wie folgt sehen:
Die tatsächliche Verzögerung hängt von externen Faktoren wie OS -Timer -Granularität und Systembelastung ab.
Die Testergebnisse zeigen jedoch, dass diese tatsächliche Verzögerung das maximale Timer -Intervall ist (beachten Sie, dass das aktuelle Timerintervall des Systems nur 1,001 ms beträgt), was in jedem Fall nicht akzeptabel ist. Die starke Neugier treibt uns dazu, den Quellcode von Node.js durchzusehen, um einen Blick auf die Wahrheit zu werfen.
Fehler in node.js
Ich glaube, dass die meisten von Ihnen und ich ein gewisses Verständnis des geraden Schleifenmechanismus von Node.js. Wenn wir uns den Quellcode der Timer -Implementierung ansehen, können wir das Implementierungsprinzip des Timers grob verstehen. Beginnen wir mit der Hauptschleife der Ereignisschleife:
Die Codekopie lautet wie folgt:
while (r! = 0 && loop-> stop_flag == 0) {
/* Globale Zeit aktualisieren*/
UV_UPDATE_TIME (Schleife);
/* Überprüfen Sie, ob der Timer abläuft, und führen Sie den entsprechenden Timer -Rückruf*/aus
uv_process_timers (Schleife);
/* Rufen Sie Leerlauf -Rückrufe an, wenn nichts zu tun ist. */
if (Loop-> anhängig_reqs_tail == null &&
Loop-> endgame_handles == null) {
/* Verhindern, dass die Ereignisschleife beendet*//
uv_idle_invoke (Loop);
}
uv_process_reqs (Schleife);
uv_process_endgames (Schleife);
uv_prepare_invoke (Loop);
/* Sammeln Sie IO -Ereignisse*////
(*Umfrage) (Schleife, Loop-> Idle_handles == NULL &&
Loop-> anhängig_reqs_tail == null &&
Loop-> Endgame_handles == NULL &&
! Loop-> STOP_FLAG &&
(Loop-> Active_handles> 0 ||
!
! (Modus & uv_run_nowait));
/* setimmediate () etc*//
UV_Check_invoke (Schleife);
r = uv__loop_alive (Schleife);
if (modus & (uv_run_once | uv_run_nowait))
brechen;
}
Der Quellcode der Funktion uv_update_time lautet wie folgt: (https://github.com/joyent/libuv/blob/v0.10/src/win/timer.c)))
Die Codekopie lautet wie folgt:
void uv_update_time (uv_loop_t* Loop) {
/* Die aktuelle Systemzeit erhalten*/
DWORD Ticks = GetTickCount ();
/ * Die Annahme wird gemacht, dass large_integer.quadpart den gleichen Typ hat */
/* Loop-> Zeit, was zufällig ist. Gibt es eine Möglichkeit, dies zu behaupten? */
Large_integer* time = (large_integer*) & loop-> time;
/* Wenn der Timer eingewickelt ist, fügen Sie 1 zu seinem DWORD mit hoher Ordnung hinzu. */
/ * uv_poll muss sicherstellen, dass der Timer niemals mehr überlaufen kann als */
/* einmal zwischen zwei nachfolgenden uv_update_time -Aufrufen. */
if (ticks <Time-> low-part) {
Zeit-> HighPart += 1;
}
zeit-> lowPart = Ticks;
}
Die interne Implementierung dieser Funktion verwendet die Funktion GetTickCount () von Windows, um die aktuelle Zeit festzulegen. Einfach ausgedrückt, nachdem die SetTimeout-Funktion nach einer Reihe von Kämpfen aufgerufen wird, wird der fälliges internes Timer-> auf die aktuelle Zeitleitungszeit + Timeout eingestellt. Aktualisieren Sie in der Ereignisschleife zunächst die Zeit der aktuellen Schleife über uv_update_time und prüfen Sie dann, ob der Timer in uv_process_times abläuft. Wenn ja, betreten Sie die Welt von JavaScript. Nach dem Lesen des gesamten Artikels ähnelt die Ereignisschleife ungefähr diesem Prozess:
Globale Zeit aktualisieren
Überprüfen Sie den Timer. Wenn der Timer abläuft, führen Sie den Rückruf aus.
Überprüfen Sie die REQS -Warteschlange und führen Sie die Warteanforderung aus
Geben Sie die Wahlfunktion ein, um IO -Ereignisse zu sammeln. Wenn ein IO -Ereignis eintrifft, fügen Sie die entsprechende Verarbeitungsfunktion zur Ausführung in der nächsten Ereignisschleife zur Ausführung hinzu. In der Umfragefunktion wird eine Systemmethode aufgerufen, um IO -Ereignisse zu sammeln. Diese Methode blockiert den Prozess, bis ein IO -Ereignis eintrifft oder die festgelegte Zeitüberschreitungszeit erreicht ist. Wenn diese Methode aufgerufen wird, wird die Zeitüberschreitungszeit auf die Zeit festgelegt, zu der der neueste Timer abläuft. Dies bedeutet, dass IO -Ereignisse durch Blockieren gesammelt werden und die maximale Blockierungszeit das letzte Zeitpunkt des nächsten Timers ist.
Quellcode einer der Umfragefunktionen unter Windows:
Die Codekopie lautet wie folgt:
statische void uv_poll (uv_loop_t* loop, int block) {
DWORD -BYTES, Timeout;
Ulong_ptr key;
Überlappend* überlappend;
uv_req_t* req;
if (block) {
/* Nehmen Sie die Ablaufzeit des neuesten Timers*/
timeout = uv_get_poll_timeout (Loop);
} anders {
Timeout = 0;
}
GetQuEedCompletionStatus (Loop-> iocp,
& Bytes,
&Schlüssel,
& überlappt,
/* Höchstens blockiert, bis der nächste Timer abläuft*/
Time-out);
if (überlappt) {
/ * Paket war dequed */
req = uv_overlapped_to_req (überlappend);
/* IO -Ereignisse in die Warteschlange einfügen*/
uv_insert_pending_req (Schleife, req);
} else if (getLasterror ()! = wait_timeout) {
/ * Schwerwiegender Fehler */
uv_fatal_error (getLasterror (), "getQuEedCompletionStatus");
}
}
Nach den oben genannten Schritten blockiert die Umfragefunktion die meisten 1 ms und erholt sich dann nach der Wiederherstellung (wenn es im Zeitraum keine IO -Ereignisse gibt). Wenn Sie weiterhin die Ereignisschleife eingeben, aktualisiert UV_UPDATE_TIME die Zeit, und dann wird UV_PROCESS_TIMERERS feststellen, dass unser Timer abläuft und den Rückruf ausführt. Die vorläufige Analyse besteht also darin, dass entweder die UV_UPDATE_TIME ein Problem aufweist (die aktuelle Zeit wird nicht korrekt aktualisiert) oder die Umfragefunktion auf 1 ms wartet und sich dann erholt. Es gibt ein Problem, wenn dieses 1 ms wartet.
Als wir uns MSDN angesehen haben, entdeckten wir überraschend eine Beschreibung der Gettickcount -Funktion:
Die Auflösung der Gettickcount -Funktion ist auf die Auflösung des System -Timers beschränkt, der in der Regel im Bereich von 10 Millionen Sekunden bis 16 Millionen Sekunden liegt.
Gettickcounts Genauigkeit ist so rau! Angenommen, die Umfragefunktion blockiert die Zeit von 1 ms korrekt, aber beim nächsten Mal wird uv_update_time ausgeführt, die aktuelle Schleifenzeit wird nicht korrekt aktualisiert! Daher wurde unser Timer nicht als abgelaufen, sodass die Umfrage auf weitere 1 ms wartete und in die nächste Ereignisschleife eingegeben wurde. Bis GettickCount endgültig korrekt aktualisiert wird (die sogenannten 15.625 ms wird einmal aktualisiert), wird die aktuelle Zeit der Schleife aktualisiert, und unser Timer wird beurteilt, dass er in UV_PROCESS_TIMERS verfällt.
Bitten Sie Webkit um Hilfe
Dieser Quellcode von Node.js ist sehr hilflos: Er hat eine Zeitfunktion mit geringer Präzision verwendet und hat nichts getan. Aber wir dachten sofort, dass wir neben Node.js 'setTimeout zusätzlich zu Node-Webkit auch Chromiums SetTimeout haben. Schreiben Sie einen Testcode und verwenden Sie unseren Browser oder Node-Webkit aus: http://marks.lrednight.com/test.html#1 (# gefolgt von der Nummer gibt das zu gemessene Intervall an). Das Ergebnis ist wie folgt:
Nach HTML5 -Spezifikationen sollte das theoretische Ergebnis sein, dass die ersten 5 Ergebnisse 1 ms und die nächsten Ergebnisse 4 ms sind. Die im Testfall angezeigten Ergebnisse beginnen ab dem dritten Mal, was bedeutet, dass die Daten in der Tabelle theoretisch 1 ms für die ersten dreimal sein und die Ergebnisse danach 4 ms betragen. Das Ergebnis hat bestimmte Fehler, und gemäß den Vorschriften ist das kleinste theoretische Ergebnis, das wir erhalten können, 4 ms. Obwohl wir nicht zufrieden sind, ist es offensichtlich viel zufriedenstellender als das Ergebnis von Node.js. Starker Neugier -Trend lassen wir uns den Quellcode von Chromium ansehen, um zu sehen, wie er implementiert wird. (https://chromium.googlesource.com/chromium/src.git/+/38.0.2125.101/base/time/time_win.cc)
Erstens verwendet Chrom die Funktion TimeGetTime (), um die aktuelle Zeit der Schleife zu bestimmen. Durch die Betrachtung von MSDN können Sie feststellen, dass die Genauigkeit dieser Funktion durch das aktuelle Timer -Intervall des Systems beeinflusst wird. Auf unserer Testmaschine ist es theoretisch die oben genannten 1.001 ms. Standardmäßig ist das Timer -Intervall jedoch sein Maximalwert (15,625 ms auf der Testmaschine), es sei denn, die Anwendung verändert das globale Timer -Intervall.
Wenn Sie Nachrichten in der IT -Branche folgen, müssen Sie eine solche Neuigkeiten gesehen haben. Es scheint, dass unser Chrom das Timer -Intervall sehr klein gesetzt hat! Es scheint, dass wir uns keine Sorgen um das System -Timer -Intervall machen müssen? Sei nicht zu glücklich zu früh, eine solche Reparatur gab uns einen Schlag. Tatsächlich wurde dieses Problem in Chrome 38 behoben. Sollten wir die Behebung des vorherigen Knoten-Webkit verwenden? Dies ist offensichtlich nicht elegant genug und hindert uns daran, eine leistungsfähigere Version von Chrom zu verwenden.
Wenn wir den Chrom -Quellcode weiter betrachten, können wir feststellen, dass Chrom das globale Timerintervall des Systems, wenn es einen Timer gibt und das Zeitüberschreitungen des Timeouts <32 ms, mit einer Genauigkeit von weniger als 15,625 ms einen Timer erreicht. (Quellcode anzeigen) Wenn Sie den Timer starten, wird das, was als HighlesolutionTimerManager bezeichnet wird, aktiviert. In dieser Klasse wird die Funktion enableHigResolutionTimer basierend auf dem Stromtyp des aktuellen Geräts aufgerufen. Wenn das aktuelle Gerät den Akku verwendet, wird insbesondere EnableHigResolutionTimer (false) aufgerufen, und True wird bei der Verwendung von Strom übergeben. Die Implementierung der Funktion EnsableHigResolutionTimer ist wie folgt:
Die Codekopie lautet wie folgt:
void time :: enableHigResolutionTimer (bool enable) {
Base :: Autolock Lock (g_high_res_lock.get ());
if (g_high_res_timer_enabled == aktivieren)
zurückkehren;
g_high_res_timer_enabled = enable;
if (! g_high_res_timer_count)
zurückkehren;
// Da g_high_res_timer_count!
// wurde genannt, der TimeBeginPeriode mit g_high_res_timer_enable genannt hat
// mit einem Wert, der das Gegenteil von | aktivieren |. Mit diesen Informationen wir
// TimeendPeriod mit demselben Wert in TimeBeginPeriode und
// Daher den Periodeneffekt rückgängig.
if (aktivieren) {
TimeEldPeriod (kmintimerIntervallowresss);
TIMEBEGINPERIOD (KmintimerIntervalhighresms);
} anders {
TimeEldPeriod (KmintimerIntervalhighresms);
TIMEBEGINPERIOD (KmintimerIntervaLaLesms);
}
}
Wo, KmintimerIntervallowresms = 4 und KmintimerIntervalhighresms = 1. TimeBeginPerioD und TimeendPeriod sind Funktionen, die von Windows zur Änderung des System -Timer -Intervalls bereitgestellt werden. Das heißt, wenn Sie mit der Stromversorgung eine Verbindung herstellen, ist das kleinste Timer -Intervall, das wir erhalten können, 1 ms, während es bei der Verwendung der Batterie 4 ms ist. Da unser Schleifen nach der W3C -Spezifikation kontinuierlich SetTimeout aufruft, beträgt das Mindestintervall ebenfalls 4 ms. Ich fühle mich also erleichtert, dies hat wenig Einfluss auf uns.
Ein weiteres Präzisionsproblem
Zurück zum Anfang haben wir festgestellt, dass die Testergebnisse zeigen, dass das Intervall von SetTimeout bei 4 ms nicht stabil ist, sondern kontinuierlich schwankt. Die Testergebnisse http://marks.lrednight.com/test.html#48 zeigen auch, dass die Intervalle zwischen 48 ms und 49 ms schlagen. Der Grund dafür ist, dass in der Ereignisschleife von Chrom und Node.js die Genauigkeit des Windows -Funktionsaufrufs auf das IO -Ereignis vom Timer des aktuellen Systems beeinflusst wird. Die Implementierung der Spiellogik erfordert die RequestArimationFrame-Funktion (ständig aktualisiert die Leinwand), wodurch wir das Timer-Intervall auf mindestens KmintimerIntervallowressms festlegen können (da sie einen 16-ms-Timer benötigt, der die Erfordernis eines hochprezierenden Timers auslöst). Wenn die Testmaschine Strom verwendet, beträgt das System -Timer -Intervall 1 ms, sodass das Testergebnis einen Fehler von ± 1 ms aufweist. Wenn Ihr Computer das System -Timer -Intervall nicht geändert hat und den obigen Test Nr. 48 ausführt, kann Max 48+16 = 64 ms erreichen.
Mit der SetTimeout -Implementierung von Chromium können wir den Fehler von SetTimeout (FN, 1) auf etwa 4 ms steuern, während der Fehler von SetTimeout (FN, 48) den Fehler von SetTimeout (FN, 48) auf etwa 1 ms steuern kann. Wir haben also eine neue Blaupause im Kopf, wodurch unser Code so aussieht:
Die Codekopie lautet wie folgt:
/ * Holen Sie sich die maximale Intervalldekoration *//
var decoration = getMaxIntervaldeviation (bucketSize); // BucketSize = 48, Abweichung = 2;
Funktion gameloop () {
var nun = Datum.Now ();
if (vorherbucket + bucketsize <= jetzt) {
vorherbucket = jetzt;
Dologic ();
}
if (vorherbucket + bucketSize - date.now ()> dekoration) {
// warte 46 ms. Die tatsächliche Verzögerung beträgt weniger als 48 ms.
setTimeout (gameloop, bucketsize - Design);
} anders {
// beschäftigt zu warten. Verwenden Sie SetImmediate anstelle von process.nextTick, da erstere keine IO -Ereignisse blockieren.
SetImmediate (Gameloop);
}
}
Mit dem obigen Code warten wir eine Zeit mit einem Fehler, der weniger als Bucket_Size (Bucket_Size Definition) anstatt direkt einem Bucket_Size entspricht. Selbst wenn der maximale Fehler in der Verzögerung von 46 ms gemäß der obigen Theorie auftritt, beträgt das tatsächliche Intervall weniger als 48 ms. Der Rest der Zeit verwenden wir die geschäftige Wartenmethode, um sicherzustellen, dass unser Gameloop unter einem Intervall mit ausreichender Präzision ausgeführt wird.
Während wir das Problem in gewissem Maße mit Chrom gelöst haben, ist dies offensichtlich nicht elegant genug.
Erinnerst du dich an unsere erste Anfrage? Unser serverseitiger Code sollte in der Lage sein, mit einer Node.js-Umgebung ohne den Node-Webkit-Client direkt auf einem Computer auszuführen. Wenn Sie den obigen Code direkt ausführen, beträgt der Wert der Definition mindestens 16 ms, was bedeutet, dass wir in jedem 48 ms auf 16 ms warten müssen. Die Nutzungsrate der CPU stieg.
Unerwartete Überraschung
Es ist so nervig. Hat niemand einen so großen Fehler in Node.js bemerkt? Die Antwort macht uns wirklich überglücklich. Dieser Fehler wurde in der Version v.0.11.3 behoben. Sie können auch die geänderten Ergebnisse sehen, indem Sie den Master -Zweig des Libuv -Code direkt anzeigen. Der spezifische Ansatz besteht darin, der aktuellen Zeit der Schleife eine Zeitüberschreitung hinzuzufügen, nachdem die Wahlfunktion auf die Fertigstellung wartet. Auf diese Weise, selbst wenn Gettickcount nicht reagiert, fügten wir diese Wartezeit nach dem Warten der Umfrage hinzu. Der Timer kann also reibungslos ausfallen.
Mit anderen Worten, das Problem, das seit langem hart gearbeitet wurde, wurde in V.0.11.3 gelöst. Unsere Bemühungen waren jedoch nicht umsonst. Denn selbst wenn die GetTickCount -Funktion beseitigt wird, wird die Umfragefunktion selbst vom System -Timer beeinflusst. Eine Lösung besteht darin, das Node.js -Plugin zu schreiben, um die Intervalle von Systemtimern zu ändern.
Die ersten Einstellungen für unser Spiel diesmal sind jedoch nicht serverfrei. Nachdem der Client einen Raum erstellt hat, wird er zum Server. Der Servercode kann in der Node-Webkit-Umgebung ausgeführt werden, sodass die Priorität von Timer-Problemen auf Windows-Systemen nicht die höchste ist. Nach der oben angegebenen Lösung reichen die Ergebnisse aus, um uns zu befriedigen.
Ende
Nach der Lösung des Timer -Problems wird unsere Rahmen -Implementierung im Grunde nicht mehr behindert. Wir bieten WebSocket-Unterstützung (in reinen HTML5-Umgebungen) und passen das Kommunikationsprotokoll an, um eine höhere Leistungsstöcke zu unterstützen (in Node-Webkit-Umgebungen). Natürlich waren die Funktionen von Spaceroom zu Beginn relativ einfach, aber da die Nachfrage vorgeschlagen wurde und die Zeit zunahm, verbessern wir diesen Rahmen allmählich.
Als wir beispielsweise feststellten, dass wir in unserem Spiel konsistente Zufallszahlen generieren müssen, fügten wir diese Funktion dem Abstandsraum hinzu. Zu Beginn des Spiels verteilt der Spaceroom Zufallszahlensamen. Der Lebensraum des Kunden bietet eine Methode, mit der die Zufälligkeit von MD5 mithilfe von Zufallszahlensamen Zufallszahlen generiert wird.
Es scheint ziemlich erleichtert. Ich habe auch viel gelernt, um einen solchen Rahmen zu schreiben. Wenn Sie sich für den Abstandsraum interessieren, können Sie auch daran teilnehmen. Ich glaube, der Abstandsraum wird seine Fäuste und Füße an mehr Orten verwenden.