
Ich möchte die Mechanismen, die wir auf dem Server zum visuellen Debuggen der Spielelogik verwenden, und Möglichkeiten zum Ändern von Übereinstimmungsstatus in Echtzeit teilen.
In früheren Artikeln wurde ausführlich beschrieben (die Liste unmittelbar unter dem Schnitt), wie ECS in unserem neuen Entwicklungsprojekt angeordnet ist und wie vorgefertigte Lösungen ausgewählt werden können. Eine solche Lösung war
Entitas . Es hat uns in erster Linie nicht gepasst, weil die Geschichte der Zustände nicht gespeichert wurde, aber es hat mir sehr gut gefallen, weil Sie in Unity alle Statistiken über die Verwendung von Entitäten, Komponenten, das Poolsystem, die Leistung jedes Systems usw. visuell und grafisch anzeigen können.
Dies hat uns dazu inspiriert, unsere eigenen Tools auf dem Spielserver zu erstellen, um zu beobachten, was im Spiel mit den Spielern passiert, wie sie spielen und wie das System insgesamt funktioniert. Auf dem Client haben wir ähnliche Entwicklungen für das visuelle Debuggen des Spiels, aber die Tools im Client sind etwas einfacher als auf dem Server.

Die versprochene Liste aller veröffentlichten Artikel zum Projekt:
- " Wie wir uns auf einem mobilen Fast-Paced-Shooter bewegten: Technologie und Ansätze ."
- " Wie und warum haben wir unser ECS geschrieben? "
- " Wie wir den Netzwerkcode des mobilen PvP-Shooters geschrieben haben: Player-Synchronisation auf dem Client ."
- " Client-Server-Interaktion in einem neuen mobilen PvP-Shooter und Spieleserver: Probleme und Lösungen ."
Nun zum Ende dieses Artikels. Zu Beginn haben wir einen kleinen Webserver geschrieben, auf dem einige APIs verfügbar gemacht wurden. Der Server selbst öffnet einfach den Socket-Port und wartet auf http-Anforderungen an diesem Port.
Die Verarbeitung ist ein ziemlich normaler Wegprivate bool HandleHttp(Socket socket) { var buf = new byte[8192]; var bufLen = 0; var recvBuf = new byte[8192]; var bodyStart = -1; while(bodyStart == -1) { var recvLen = socket.Receive(recvBuf); if(recvLen == 0) { return true; } Buffer.BlockCopy(recvBuf, 0, buf, bufLen, recvLen); bufLen += recvLen; bodyStart = FindBodyStart(buf, bufLen); } var headers = Encoding.UTF8.GetString(buf, 0, bodyStart - 2).Replace("\r", "").Split('\n'); var main = headers[0].Split(' '); var reqMethod = ParseRequestMethod(main[0]); if (reqMethod == RequestMethod.Invalid) { SendResponse(400, socket); return true; }
Dann registrieren wir für jede Anfrage mehrere spezielle Handler.
Einer von ihnen ist der Handler, der alle Spiele im Spiel anzeigt. Wir haben eine spezielle Klasse, die die Lebensdauer aller Spiele kontrolliert.
Für eine schnelle Entwicklung haben sie einfach eine Methode hinzugefügt und eine Liste der Übereinstimmungen an ihren Ports im JSON-Format ausgegeben public string ListMatches(string method, string path) { var sb = new StringBuilder(); sb.Append("[\n"); foreach (var match in _matches.Values) { sb.Append("{id:\"" + match.GameId + "\"" + ", www:" + match.Tool.Port + "},\n" ); } sb.Append("]"); return sb.ToString(); }

Klicken Sie auf den Link mit dem Match und gehen Sie zum Kontrollmenü. Hier wird es viel interessanter.
Jede Übereinstimmung auf der Debug-Assembly des Servers gibt vollständige Daten über sich selbst aus. Einschließlich des GameState, über den wir
geschrieben haben . Ich möchte Sie daran erinnern, dass dies im Wesentlichen der Status des gesamten Spiels ist, einschließlich statischer und dynamischer Daten. Mit diesen Daten können wir verschiedene Informationen über die Übereinstimmung in HTML anzeigen. Wir können diese Daten auch direkt ändern, aber dazu später mehr.
Der erste Link führt zum Standard-Übereinstimmungsprotokoll:

Darin zeigen wir die wichtigsten nützlichen Daten zu Verbindungen, die übertragene Datenmenge, die Hauptlebenszyklen von Zeichen und andere Protokolle an.
Der zweite Link von GameViewer führt zu einer realen visuellen Darstellung des Spiels:

Der Generator, der den ECS-Code zum Packen der Daten erstellt, erstellt auch zusätzlichen Code zur Darstellung der Daten in json. Auf diese Weise können Sie die Übereinstimmungsstruktur ganz einfach aus json lesen und zum Rendern mit der Bibliothek three.js in WebGL rendern.
Die Datenstruktur sieht ungefähr so aus { enums: { "HostilityLayer": { 1: "PlayerTeam1", 2: "PlayerTeam2", 3: "NeutralShootable", } }, components: { Transform: { name: 'Transform', fields: { Angle: {type: "float"}, Position: {type: "Vector2"}, }, }, TransformExact: { name: 'TransformExact', fields: { Angle: {type: "float"}, Position: {type: "Vector2"}, } } }, tables: { Transform: { name: 'Transform', component: 'Transform', }, TransformExact: { name: 'TransformExact', component: 'TransformExact', hint: "Copy of Transform for these entities that need full precision when sent over network", } } }
Und der Renderzyklus dynamischer Körper (in unserem Fall Spieler) ist so var rulebook = {}; var worldstate = {}; var physics = {}; var update_dynamic_physics; var camera, scene, renderer; var controls; function init3D () { camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000); camera.up.set(0,0,1); scene = new THREE.Scene(); scene.add( new THREE.AmbientLight( 0x404040 ) ); var light = new THREE.DirectionalLight( 0xFFFFFF, 1 ); light.position.set(-11, -23, 45); scene.add( light ); renderer = new THREE.WebGLRenderer(); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild( renderer.domElement ); controls = new THREE.OrbitControls( camera, renderer.domElement ); var cam = localStorage.getObject('gv_camera'); if (cam) { camera.matrix.fromArray(cam.matrix); camera.matrix.decompose(camera.position, camera.quaternion, camera.scale); controls.target.set(cam.target.x, cam.target.y, cam.target.z); } else { camera.position.x = 40; camera.position.y = 40; camera.position.z = 50; controls.target.set(0, 0, 0); } window.addEventListener( 'resize', onWindowResize, false ); } init3D(); function handle_recv_dynamic (r) { eval('physics = ' + r + ';'); update_dynamic_physics(); sleep(10) .then(() => ajax("GET", "/physics/dynamic/")) .then(handle_recv_dynamic); } (function init_dynamic_physics () { var colour = 0x4B5440; var material = new THREE.MeshLambertMaterial({color: colour, flatShading: true}); var meshes = {}; update_dynamic_physics = function () { var i, p, mesh; var to_del = {}; for (i in meshes) to_del[i] = true; for (i in physics) { p = physics[i]; mesh = meshes[p.id]; if (!mesh) { mesh = create_shapes(worldstate, 'Dynamic', p, material, layers.dynamic_collider); meshes[p.id] = mesh; } mesh.position.x = p.pos[0]; mesh.position.y = p.pos[1]; delete to_del[p.id]; } for (i in to_del) { mesh = meshes[i]; scene.remove(mesh); delete meshes[i]; } } })();
Fast jede Entität, die die Logik der Bewegung in unserer physischen Welt hat, hat eine Transformationskomponente. Klicken Sie auf den Link WorldState Table Editor, um eine Liste aller Komponenten anzuzeigen.

Im Dropdown-Menü oben können Sie verschiedene Arten von Komponenten auswählen und deren aktuellen Status anzeigen. Die obige Abbildung zeigt also alle Transformationen im Spiel. Das Interessanteste: Wenn Sie die Werte der Transformation in diesem Editor ändern, teleportiert sich der Spieler oder eine andere Spieleinheit scharf zum gewünschten Punkt (manchmal haben wir Spaß an Spieletests).
Das Ändern von Daten in einer echten Übereinstimmung beim Bearbeiten einer HTML-Tabelle erfolgt, weil wir einen speziellen Link zu dieser Tabelle ziehen, der den Tabellennamen, das Feld und neue Daten enthält:
function handle_edit (id, table_name, field_name, value) { var data = table_name + "\n" + field_name + "\n" + id + "\n" + value; ajax("POST", tableset_name + "/edit/", data); }
Der Spieleserver abonniert dank des generierten Codes die gewünschte URL, die für die Tabelle eindeutig ist:
public static void RegisterEditorHandlers(Action<string, Func<string, string, string>> addHandler, string path, Func<TableSet> ts) { addHandler(path + "/data/", (p, b) => EditorPackJson(ts())); addHandler(path + "/edit/", (p, b) => EditorUpdate(ts(), b)); addHandler(path + "/ins/", (p, b) => EditorInsert(ts(), b)); addHandler(path + "/del/", (p, b) => EditorDelete(ts(), b)); addHandler(path + "/create/", (p, b) => EditorCreateEntity(ts(), b)); }
Eine weitere nützliche Funktion des visuellen Editors ermöglicht es uns, die Fülle des Eingabepuffers des Players zu überwachen. Wie aus den vorherigen Artikeln des Zyklus hervorgeht, ist dies notwendig, um ein komfortables Spiel für die Kunden aufrechtzuerhalten, damit das Spiel nicht zuckt und die Welt nicht zurückbleibt.

Die vertikale Achse des Diagramms ist die Anzahl der Spielereinträge, die derzeit im Eingabepuffer auf dem Server verfügbar sind. Idealerweise sollte diese Zahl nicht größer oder kleiner als 1 sein. Aufgrund der Instabilität des Netzwerks und der ständigen Anpassung der Clients an diese Kriterien schwingt der Graph normalerweise in unmittelbarer Nähe dieser Marke. Wenn der Eintrittsplan für einen Spieler stark sinkt, haben wir verstanden, dass der Client höchstwahrscheinlich die Verbindung verloren hat. Wenn es auf einen ziemlich großen Wert fällt und sich dann abrupt erholt, bedeutet dies, dass der Client ernsthafte Probleme mit der Stabilität der Verbindung hat.
*****
Mit den beschriebenen Funktionen unseres Match-Editors auf dem Spielserver können Sie Netzwerk- und Spielmomente effektiv debuggen und Spiele in Echtzeit überwachen. Auf prod sind diese Funktionen jedoch deaktiviert, da sie den Server und den Garbage Collector erheblich belasten. Es ist erwähnenswert, dass das System dank des bereits vorhandenen ECS-Codegenerators in sehr kurzer Zeit geschrieben wurde. Ja, der Server ist nicht nach allen Regeln moderner Webstandards geschrieben, aber er hilft uns sehr bei der täglichen Arbeit und beim Debuggen des Systems. Er entwickelt sich auch allmählich weiter und erhält neue Möglichkeiten. Wenn es genug davon gibt, werden wir auf dieses Thema zurückkommen.