
Quero compartilhar os mecanismos que usamos no servidor para depuração visual da lógica do jogo e maneiras de alterar os estados de correspondência em tempo real.
Em artigos anteriores, eles relataram em detalhes (a lista imediatamente abaixo do corte) sobre como o ECS é organizado em nosso novo projeto em desenvolvimento e como escolher soluções prontas. Uma dessas soluções foi a
Entitas . Não nos serviu em primeiro lugar devido à falta de armazenamento da história dos estados, mas gostei muito porque no Unity você pode visualizar visual e graficamente todas as estatísticas sobre o uso de entidades, componentes, sistema de pool, desempenho de cada sistema, etc.
Isso nos inspirou a criar nossas próprias ferramentas no servidor do jogo para ver o que acontece na partida com os jogadores, como eles jogam, como o sistema como um todo se sairá. No cliente, também temos desenvolvimentos semelhantes para depuração visual do jogo, mas as ferramentas no cliente são um pouco mais simples em comparação com o que fizemos no servidor.

A lista prometida de todos os artigos publicados no projeto:
- " Como entramos em um jogo de tiro rápido e móvel: tecnologia e abordagens ."
- " Como e por que escrevemos nossa ECS ".
- " Como escrevemos o código de rede do shooter PvP móvel: sincronização do jogador no cliente ."
- “ Interação cliente-servidor em um novo dispositivo móvel de tiro em PvP e servidor de jogos: problemas e soluções .”
Agora, no final deste artigo. Para começar, escrevemos um pequeno servidor Web que expôs alguma API. O próprio servidor simplesmente abre a porta do soquete e escuta solicitações http nessa porta.
O processamento é uma maneira bastante padrãoprivate 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; }
Em seguida, registramos vários manipuladores especiais para cada solicitação.
Um deles é o manipulador de visualizar todas as partidas no jogo. Temos uma classe especial que controla o tempo de vida de todas as partidas.
Para um rápido desenvolvimento, eles simplesmente adicionaram um método, emitindo uma lista de correspondências em suas portas no 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(); }

Clicando no link da partida, vá para o menu de controle. Aqui fica muito mais interessante.
Cada correspondência no conjunto de depuração do servidor fornece dados completos sobre si. Incluindo o GameState
sobre o qual
escrevemos . Deixe-me lembrá-lo de que este é essencialmente o estado de toda a correspondência, incluindo dados estáticos e dinâmicos. Com esses dados, podemos exibir várias informações sobre a correspondência em html. Também podemos alterar diretamente esses dados, mas mais sobre isso posteriormente.
O primeiro link leva ao log de correspondência padrão:

Nele, exibimos os principais dados úteis sobre conexões, a quantidade transferida de dados, os principais ciclos de vida de caracteres e outros logs.
O segundo link do GameViewer leva a uma representação visual real da partida:

O gerador que cria o código ECS para empacotar os dados também cria código adicional para representar os dados em json. Isso permite que você leia com facilidade a estrutura de correspondência do json e a renderize usando a biblioteca three.js no WebGL para renderização.
A estrutura de dados se parece com isso { 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", } } }
E o ciclo de renderização de corpos dinâmicos (no nosso caso, players) é assim 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]; } } })();
Quase toda entidade que tem a lógica do movimento em nosso mundo físico tem um componente Transform. Para ver uma lista de todos os componentes, clique no link Editor de tabela do WorldState.

No menu suspenso acima, você pode selecionar vários tipos de componentes e ver seu status atual. Portanto, a figura acima mostra todas as transformações no jogo. O mais interessante: se você alterar os valores da transformação neste editor, o jogador ou outra entidade do jogo se teletransporta abruptamente para o ponto desejado (às vezes nos divertimos nos testes).
A alteração de dados em uma correspondência real ao editar uma tabela html ocorre porque puxamos um link especial para esta tabela, que contém o nome da tabela, o campo e os novos dados:
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); }
O servidor do jogo assina o URL desejado, exclusivo da tabela, graças ao código gerado:
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)); }
Outro recurso útil do editor visual nos permite monitorar a plenitude do buffer de entrada do player. Como segue os artigos anteriores do ciclo, isso é necessário para manter um jogo confortável para os clientes, para que o jogo não se mexa e o mundo não fique para trás.

O eixo vertical do gráfico é o número de entradas do player atualmente disponíveis no buffer de entrada no servidor. Idealmente, esse número não deve ser maior ou menor que 1. Mas, devido à instabilidade da rede e ao constante ajuste dos clientes a esses critérios, o gráfico geralmente oscila nas imediações dessa marca. Se o cronograma de entrada de um jogador cair drasticamente, entendemos que o cliente provavelmente perdeu a conexão. Se cair para um valor bastante alto e depois se recuperar abruptamente, isso significa que o cliente está enfrentando sérios problemas com a estabilidade da conexão.
*****
Os recursos descritos em nosso editor de partidas no servidor do jogo permitem depurar com eficiência os momentos da rede e do jogo e monitorar as partidas em tempo real. Porém, no prod, essas funções são desabilitadas, pois criam uma carga significativa no servidor e no coletor de lixo. Vale ressaltar que o sistema foi gravado em um tempo muito curto, graças ao gerador de código ECS já existente. Sim, o servidor não foi escrito de acordo com todas as regras dos modernos padrões da Web, mas nos ajuda muito no trabalho diário e na depuração do sistema. Ele também evolui gradualmente, adquirindo novas oportunidades. Quando houver um número suficiente deles, retornaremos a este tópico.