我们如何在游戏服务器的浏览器中调试自写的ECS



我想分享我们在服务器上用于视觉调试游戏逻辑的机制以及实时更改比赛状态的方法。

在之前的文章中,他们详细介绍了ECS在我们正在开发的新项目中的排列方式(如何选择现成的解决方案)(紧接在清单下方)。 一种这样的解决方案是Entitas 。 由于缺少状态历史记录存储,它并不适合我们,但我非常喜欢它,因为在Unity中,您可以直观地以图形方式查看有关实体,组件,池系统,每个系统的性能等使用情况的所有统计信息。

这激发了我们在游戏服务器上创建自己的工具,以观察与玩家进行比赛时发生的情况,他们的比赛方式以及整个系统的性能。 在客户端上,我们也进行了类似的可视化游戏调试开发,但是与我们在服务器上进行的操作相比,客户端中的工具稍微简单一些。



该项目所有已发表文章的承诺清单:

  1. 我们如何在移动快节奏射手上摇摆:技术和方法 。”
  2. 我们如何以及为什么编写了ECS 。”
  3. 当我们编写移动PvP射击游戏的网络代码时:客户端上的播放器同步 。”
  4. 新的移动PvP射击游戏服务器设备中的客户端与服务器交互:问题和解决方案 。”

现在到本文的底部。 首先,我们编写了一个小型Web服务器,其中公开了一些API。 服务器本身只是打开套接字端口,并在该端口上侦听http请求。

处理是相当标准的方式
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; } 


然后,我们为每个请求注册几个特殊的处理程序。
其中之一是查看游戏中所有比赛的处理程序。 我们有一个特殊的类来控制所有比赛的生存时间。

为了快速开发,他们只是向其中添加了一个方法,以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(); } 




单击具有匹配项的链接,然后转到控制菜单。 在这里,它变得更加有趣。

服务器的Debug-assembly上的每个匹配都给出有关其自身的完整数据。 包括我们撰写的GameState。 让我提醒您,这实际上是整个匹配的状态,包括静态和动态数据。 有了这些数据,我们可以在html中显示有关匹配的各种信息。 我们还可以直接更改此数据,但稍后会进行更多更改。

第一个链接指向标准匹配日志:



在其中,我们显示有关连接的主要有用数据,数据的传输量,字符的主要生命周期和其他日志。

GameViewer的第二个链接可导致比赛的真实视觉呈现:



为我们打包数据创建ECS代码的生成器还创建了额外的代码来表示json中的数据。 这使您可以轻松地从json中读取匹配结构,并使用WebGL中的three.js库进行渲染。

数据结构看起来像这样
 { 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", } } } 


动态物体(在我们的例子中是玩家)的渲染周期是这样的
 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]; } } })(); 


在我们的物理世界中,几乎所有具有运动逻辑的实体都具有一个“变换”组件。 要查看所有组件的列表,请单击链接WorldState Table Editor。



在上方的下拉菜单中,您可以选择各种类型的组件并查看其当前状态。 因此,上图显示了游戏中的所有变换。 最有趣的事情是:如果您在此编辑器中更改了变换的值,则玩家或其他游戏实体将迅速传送到所需的位置(有时我们在游戏测试中会很开心)。

在编辑html表时,会发生真正匹配中的数据更改,因为我们拉到该表的特殊链接,其中包含表名称,字段和新数据:

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

由于生成的代码,游戏服务器订阅了所需的URL,该URL对表是唯一的:

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

视觉编辑器的另一个有用功能是允许我们监视玩家输入缓冲区的充满度。 如本周期前几篇文章所述,这对于为客户维持舒适的游戏是必要的,这样游戏就不会抽搐,世界也不会落后。



图的垂直轴是服务器上输入缓冲区中当前可用的玩家条目数。 理想情况下,该数字不应大于或小于1。但是,由于网络的不稳定性以及客户不断根据这些标准进行调整,因此该图通常会在该标记的附近振荡。 如果玩家的入场时间表急剧下降,我们了解到客户很可能失去了联系。 如果下降到相当大的值,然后突然恢复,则意味着客户端在连接稳定性方面遇到了严重的问题。

*****

游戏服务器上比赛编辑器的上述功能使您可以有效地调试网络和比赛时刻,并实时监控比赛。 但是在生产时,这些功能被禁用,因为它们在服务器和垃圾收集器上造成了很大的负担。 值得注意的是,由于已有ECS代码生成器,因此该系统是在很短的时间内编写的。 是的,服务器并不是按照现代Web标准的所有规则编写的,但是它可以在系统的日常工作和调试中为我们提供很多帮助。 他也逐渐发展,获得了新的机会。 当它们足够多时,我们将返回此主题。

Source: https://habr.com/ru/post/zh-CN424267/


All Articles