How we are debugging in a browser samopisny ECS on the game server
I want to share the mechanisms that we use on the server to visually debug game logic and how to change the states of a match in real time.
In previous articles, they described in detail (the list immediately under the cut) how ECS is arranged in our new project in development and how they chose ready-made solutions. One such solution was Entitas . He did not suit us primarily due to the lack of storage of the history of states, but I liked the fact that in Unity you can visually and visually see all the statistics on the use of entities, components, the system of pools, the performance of each system, etc.
This inspired us to create our own tools on the game server, to see what happens in the match with the players, how they play, how the system performs as a whole. On the client, we also have similar practices for visual debugging of the game, but the tools in the client are slightly simpler compared to what we did on the server.
The promised list of all published articles on the project:
- " As we swung at the mobile fast paced shooter: technologies and approaches ."
- " How and why we wrote our ECS ."
- " As we wrote the network code of mobile PvP shooter: player synchronization on the client ."
- " Client-server interaction in the new mobile PvP-shooter and game server device: problems and solutions ."
Now to the point of this article. To begin with, we wrote a small web server that gave out some API. The server itself simply opens the socket port and listens to http requests on this port.
Processing is pretty standard way
privateboolHandleHttp(Socket socket)
{
var buf = newbyte[8192];
var bufLen = 0;
var recvBuf = newbyte[8192];
var bodyStart = -1;
while(bodyStart == -1)
{
var recvLen = socket.Receive(recvBuf);
if(recvLen == 0)
{
returntrue;
}
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);
returntrue;
}
// receive POST bodyvar body = string.Empty;
if(reqMethod == RequestMethod.Post)
{
body = ReceiveBody(buf, bufLen, headers, bodyStart, socket);
if(body == null)
{
returntrue;
}
}
var path = main[1];
if(path == "/")
{
path = "/index.html";
}
// try to serve by a fileif(File.Exists(_docRoot + path))
{
var content = File.ReadAllBytes(_docRoot + path);
if (reqMethod == RequestMethod.Head)
{
content = null;
}
SendResponse(200, socket, content, GuessMime(path));
returntrue;
}
// try to serve by a handleforeach(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();
});
returnfalse;
}
else
{
RunHandler(socket, path, body, handler);
returntrue;
}
}
}
// nothing found :-(var msg = "File not found " + path
+ "\ndoc root " + _docRoot
+ "\ncurrent dir " + Directory.GetCurrentDirectory();
SendResponse(404, socket, Encoding.UTF8.GetBytes(msg));
returntrue;
}
Then we register several special handlers for each request.
One of them is a handler for viewing all matches in the game. We have a special class that controls the lifetime of all matches.
For a quick development, we simply added a method to it, issuing a list of matches on its ports in json format.
publicstringListMatches(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();
}
Clicking on the link with the match, go to the control menu. Here it becomes much more interesting.
Each match on the Debug-assembly of the server gives out full data about itself. Including the GameState we wrote about . Let me remind you that this is essentially the state of the whole match, including static and dynamic data. Having this data, we can display various information about the match in html. We can also directly change this data, but this will be a little later.
The first link leads to the standard match log:
In it, we display the main useful data on connections, transferred data volume, main life cycles of characters and other logs.
The second link GameViewer leads to a real visual representation of the match:
The generator, which creates us the ECS code for data packing, also creates additional code for representing the data in json. This makes it quite easy to read the match structure from json and render it to render using the three.js library in WebGL.
The data structure looks like this.
{
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",
}
}
}
And the cycle of rendering dynamic bodies (in our case, players) is
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];
}
}
})();
Almost every entity that possesses the logic of movement in our physical world has a component Transform. To see a list of all components, follow the link WorldState Table Editor.
In the dropdown menu at the top, you can select different types of components and see their current status. Thus, the figure above shows all the transformations in the game. The most interesting thing: if you change the values of the transformation in this editor, then the player or another game entity will teleport sharply to the desired point (sometimes we have fun on playtests).
Changing data in a real match when editing an html table occurs because we pull a special link to this table, which contains the table name, the field and the new data:
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);
}
On the part of the game server, a subscription to the necessary URL, unique to the table, takes place thanks to the generated code:
publicstaticvoidRegisterEditorHandlers(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));
}
Another useful feature of the visual editor allows us to monitor the fullness of the player input buffer. As follows from the previous articles of the cycle, it is necessary to maintain a comfortable game for customers so that the game does not twitch and the world does not lag.
The vertical axis of the graph is the number of player inputs currently available in the server's input buffer. Ideally, this number should not be more or less than 1. But due to the instability of the network and the constant adjustment of customers to these criteria, the schedule usually oscillates in the immediate vicinity of this mark. If the input schedule for the player goes down sharply, we understand that the client most likely lost the connection. If he falls down to a rather large value and then recovers abruptly, this means that the client is experiencing serious problems with the stability of the connection.
*****
The features of our match editor on the game server allow you to effectively debug network and game moments and monitor matches in real time. But on the sale, these functions are disabled, as they create a significant load on the server and the garbage collector. It should be noted that the system was written in a very short time, thanks to the already existing ECS code generator. Yes, the server is not written according to all the rules of modern web-standards, but it helps us a lot in everyday work and debugging the system. He also gradually evolves, acquiring new opportunities. When there are enough of them - we will return to this topic.