Comment déboguer un ECS auto-écrit dans un navigateur sur un serveur de jeu



Je veux partager les mécanismes que nous utilisons sur le serveur pour le débogage visuel de la logique du jeu et les moyens de changer les états de correspondance en temps réel.

Dans des articles précédents, ils ont expliqué en détail (la liste immédiatement en dessous de la coupe) comment ECS est organisé dans notre nouveau projet en développement et comment choisir des solutions toutes faites. Une de ces solutions était Entitas . Cela ne nous convenait pas en premier lieu en raison du manque de stockage de l'historique des états, mais je l'ai beaucoup aimé parce que dans Unity, vous pouvez voir visuellement et graphiquement toutes les statistiques sur l'utilisation des entités, des composants, du système de pool, des performances de chaque système, etc.

Cela nous a inspiré pour créer nos propres outils sur le serveur de jeu pour regarder ce qui se passe dans le match avec les joueurs, comment ils jouent, comment le système dans son ensemble fonctionnera. Sur le client, nous avons également des développements similaires pour le débogage visuel du jeu, mais les outils du client sont légèrement plus simples par rapport à ce que nous avons fait sur le serveur.



La liste promise de tous les articles publiés sur le projet:

  1. " Comment nous avons évolué sur un jeu de tir mobile rapide: technologie et approches ."
  2. " Comment et pourquoi nous avons écrit notre ECS ."
  3. " Comme nous l'avons écrit le code réseau du tireur PvP mobile: synchronisation du joueur sur le client ."
  4. " Interaction client-serveur dans un nouveau tireur PvP mobile et un nouveau serveur de jeu: problèmes et solutions ."

Maintenant au bas de cet article. Pour commencer, nous avons écrit un petit serveur Web qui a exposé certaines API. Le serveur lui-même ouvre simplement le port socket et écoute les requêtes http sur ce port.

Le traitement est un moyen assez standard
private 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; } // receive POST body var body = string.Empty; if(reqMethod == RequestMethod.Post) { body = ReceiveBody(buf, bufLen, headers, bodyStart, socket); if(body == null) { return true; } } var path = main[1]; if(path == "/") { path = "/index.html"; } // try to serve by a file if(File.Exists(_docRoot + path)) { var content = File.ReadAllBytes(_docRoot + path); if (reqMethod == RequestMethod.Head) { content = null; } SendResponse(200, socket, content, GuessMime(path)); return true; } // try to serve by a handle foreach(var handler in _handlers) { if(handler.Match(reqMethod, path)) { if (handler.Async) { _jobs.Enqueue(() => { RunHandler(socket, path, body, handler); socket.Shutdown(SocketShutdown.Both); socket.Close(); }); return false; } else { RunHandler(socket, path, body, handler); return true; } } } // nothing found :-( var msg = "File not found " + path + "\ndoc root " + _docRoot + "\ncurrent dir " + Directory.GetCurrentDirectory(); SendResponse(404, socket, Encoding.UTF8.GetBytes(msg)); return true; } 


Ensuite, nous enregistrons plusieurs gestionnaires spéciaux pour chaque demande.
L'un d'eux est le gestionnaire de visualisation de tous les matchs du jeu. Nous avons une classe spéciale qui contrôle la durée de vie de tous les matchs.

Pour un développement rapide, ils y ont simplement ajouté une méthode, émettant une liste de correspondances sur leurs ports au format json
 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(); } 




En cliquant sur le lien avec le match, allez dans le menu de contrôle. Ici, cela devient beaucoup plus intéressant.

Chaque correspondance sur l'assemblage de débogage du serveur donne des données complètes sur lui-même. Y compris le GameState dont nous avons parlé . Permettez-moi de vous rappeler qu'il s'agit essentiellement de l'état de l'ensemble du match, y compris les données statiques et dynamiques. Ayant ces données, nous pouvons afficher diverses informations sur le match en html. Nous pouvons également modifier directement ces données, mais plus à ce sujet plus tard.

Le premier lien mène au journal de correspondance standard:



Dans ce document, nous affichons les principales données utiles sur les connexions, la quantité de données transférée, les principaux cycles de vie des personnages et d'autres journaux.

Le deuxième lien de GameViewer conduit à une véritable représentation visuelle du match:



Le générateur qui crée le code ECS pour que nous emballions les données crée également du code supplémentaire pour représenter les données dans json. Cela vous permet de lire assez facilement la structure de correspondance de json et de la rendre en utilisant la bibliothèque three.js dans WebGL pour le rendu.

La structure des données ressemble à ceci
 { 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", } } } 


Et le cycle de rendu des corps dynamiques (dans notre cas, les joueurs) est comme ça
 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]; } } })(); 


Presque toutes les entités qui ont la logique du mouvement dans notre monde physique ont une composante Transformer. Pour voir une liste de tous les composants, cliquez sur le lien WorldState Table Editor.



Dans le menu déroulant ci-dessus, vous pouvez sélectionner différents types de composants et voir leur état actuel. Ainsi, la figure ci-dessus montre toutes les transformations du jeu. La chose la plus intéressante: si vous modifiez les valeurs de la transformation dans cet éditeur, le joueur ou une autre entité de jeu se téléportera brusquement au point souhaité (parfois nous nous amusons sur les tests de jeu).

La modification des données dans une correspondance réelle lors de la modification d'une table html se produit car nous tirons un lien spécial vers cette table, qui contient le nom de la table, le champ et les nouvelles données:

 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); } 

Le serveur de jeu s'abonne à l'URL souhaitée, unique à la table, grâce au code généré:

 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)); } 

Une autre fonctionnalité utile de l'éditeur visuel nous permet de surveiller la plénitude du tampon d'entrée des joueurs. Comme il ressort des articles précédents du cycle, cela est nécessaire pour maintenir un jeu confortable pour les clients, afin que le jeu ne se contracte pas et que le monde ne traîne pas.



L'axe vertical du graphique est le nombre d'entrées de joueur actuellement disponibles dans le tampon d'entrée sur le serveur. Idéalement, ce nombre ne devrait pas être supérieur ou inférieur à 1. Mais en raison de l'instabilité du réseau et de l'ajustement constant des clients à ces critères, le graphique oscille généralement au voisinage immédiat de cette marque. Si le calendrier d'entrée d'un joueur baisse fortement, nous comprenons que le client a probablement perdu la connexion. S'il tombe à une valeur assez élevée, puis se rétablit brusquement, cela signifie que le client rencontre de sérieux problèmes avec la stabilité de la connexion.

*****

Les fonctionnalités décrites de notre éditeur de correspondance sur le serveur de jeu vous permettent de déboguer efficacement les moments du réseau et du jeu et de surveiller les correspondances en temps réel. Mais sur prod, ces fonctions sont désactivées, car elles créent une charge importante sur le serveur et le garbage collector. Il est à noter que le système a été écrit en très peu de temps, grâce au générateur de code ECS déjà existant. Oui, le serveur n'est pas écrit selon toutes les règles des standards Web modernes, mais il nous aide beaucoup dans le travail quotidien et le débogage du système. Il évolue également progressivement, acquérant de nouvelles opportunités. Quand il y en aura assez, nous reviendrons sur ce sujet.

Source: https://habr.com/ru/post/fr424267/


All Articles