
Quiero compartir los mecanismos que utilizamos en el servidor para la depuraci贸n visual de la l贸gica del juego y las formas de cambiar los estados de los partidos en tiempo real.
En art铆culos anteriores informaron en detalle (la lista inmediatamente debajo del corte) sobre c贸mo se organiza ECS en nuestro nuevo proyecto en desarrollo y c贸mo elegir soluciones listas para usar. Una de esas soluciones fue
Entitas . En primer lugar, no nos conven铆a debido a la falta de almacenamiento del historial del estado, pero me gust贸 mucho porque en Unity puedes ver visual y gr谩ficamente todas las estad铆sticas sobre el uso de entidades, componentes, el sistema de agrupaci贸n, el rendimiento de cada sistema, etc.
Esto nos inspir贸 a crear nuestras propias herramientas en el servidor del juego para ver lo que sucede en el partido con los jugadores, c贸mo juegan, c贸mo funcionar谩 el sistema en su conjunto. En el cliente, tambi茅n tenemos desarrollos similares para la depuraci贸n visual del juego, pero las herramientas en el cliente son un poco m谩s simples en comparaci贸n con lo que hicimos en el servidor.

La lista prometida de todos los art铆culos publicados sobre el proyecto:
- " C贸mo nos lanzamos en un tirador m贸vil de ritmo r谩pido: tecnolog铆a y enfoques ".
- " C贸mo y por qu茅 escribimos nuestro ECS ".
- " Como escribimos el c贸digo de red del tirador PvP m贸vil: sincronizaci贸n del jugador en el cliente ".
- " Interacci贸n cliente-servidor en un nuevo dispositivo m贸vil PvP shooter y servidor de juegos: problemas y soluciones ".
Ahora al final de este art铆culo. Para comenzar, escribimos un peque帽o servidor web que expuso algunas API. El servidor mismo simplemente abre el puerto del socket y escucha las solicitudes http en ese puerto.
El procesamiento es una forma bastante est谩ndarprivate 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; }
Luego registramos varios manejadores especiales para cada solicitud.
Uno de ellos es el manejador de ver todos los partidos en el juego. Tenemos una clase especial que controla el tiempo de vida de todos los partidos.
Para un desarrollo r谩pido, simplemente le agregaron un m茅todo, emitiendo una lista de coincidencias en sus puertos en formato 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(); }

Al hacer clic en el enlace con el partido, vaya al men煤 de control. Aqu铆 se vuelve mucho m谩s interesante.
Cada coincidencia en el ensamblaje de depuraci贸n del servidor proporciona datos completos sobre s铆 mismo. Incluyendo el GameState sobre el que
escribimos . Perm铆tame recordarle que este es esencialmente el estado de toda la coincidencia, incluidos los datos est谩ticos y din谩micos. Con estos datos, podemos mostrar diversa informaci贸n sobre la coincidencia en html. Tambi茅n podemos cambiar directamente estos datos, pero m谩s sobre eso m谩s adelante.
El primer enlace conduce al registro de coincidencias est谩ndar:

En 茅l, mostramos los principales datos 煤tiles sobre las conexiones, la cantidad de datos transferidos, los principales ciclos de vida de los caracteres y otros registros.
El segundo enlace de GameViewer conduce a una representaci贸n visual real del partido:

El generador que crea el c贸digo ECS para que empaquemos los datos tambi茅n crea un c贸digo adicional para representar los datos en json. Esto le permite leer con bastante facilidad la estructura de coincidencia de json y representarla utilizando la biblioteca three.js en WebGL para la representaci贸n.
La estructura de datos se parece a esto { 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", } } }
Y el ciclo de renderizado de cuerpos din谩micos (en nuestro caso, jugadores) es as铆 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]; } } })();
Casi todas las entidades que tienen la l贸gica del movimiento en nuestro mundo f铆sico tienen un componente Transformar. Para ver una lista de todos los componentes, haga clic en el enlace WorldState Table Editor.

En el men煤 desplegable anterior, puede seleccionar varios tipos de componentes y ver su estado actual. Entonces, la figura de arriba muestra todas las transformaciones en el juego. Lo m谩s interesante: si cambia los valores de la transformaci贸n en este editor, el jugador u otra entidad del juego se teletransportar谩 bruscamente al punto deseado (a veces nos divertimos en las pruebas de juego).
El cambio de datos en una coincidencia real al editar una tabla html se produce porque extraemos un enlace especial a esta tabla, que contiene el nombre de la tabla, el campo y los datos nuevos:
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); }
El servidor del juego se suscribe a la URL deseada, exclusiva de la tabla, gracias al c贸digo generado:
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)); }
Otra caracter铆stica 煤til del editor visual nos permite monitorear la plenitud del b煤fer de entrada de los jugadores. Como se desprende de los art铆culos anteriores del ciclo, esto es necesario para mantener un juego c贸modo para los clientes, para que el juego no se contraiga y el mundo no se quede atr谩s.

El eje vertical del gr谩fico es el n煤mero de entradas de jugadores actualmente disponibles en el b煤fer de entrada en el servidor. Idealmente, este n煤mero no deber铆a ser mayor o menor que 1. Pero debido a la inestabilidad de la red y al ajuste constante de los clientes a estos criterios, el gr谩fico generalmente oscila en la vecindad inmediata de esta marca. Si el horario de entrada para un jugador se reduce dr谩sticamente, entendemos que el cliente probablemente perdi贸 la conexi贸n. Si cae a un valor bastante grande y luego se recupera abruptamente, esto significa que el cliente est谩 experimentando serios problemas con la estabilidad de la conexi贸n.
*****
Las caracter铆sticas descritas de nuestro editor de partidos en el servidor del juego te permiten depurar de manera efectiva los momentos de la red y del juego y monitorear los partidos en tiempo real. Pero en prod, estas funciones est谩n deshabilitadas, ya que crean una carga significativa en el servidor y el recolector de basura. Vale la pena se帽alar que el sistema fue escrito en muy poco tiempo, gracias al generador de c贸digo ECS ya existente. S铆, el servidor no est谩 escrito de acuerdo con todas las reglas de los est谩ndares web modernos, pero nos ayuda mucho en el trabajo diario y la depuraci贸n del sistema. Tambi茅n evoluciona gradualmente, adquiriendo nuevas oportunidades. Cuando haya suficientes, volveremos a este tema.