游戏日志系统设计与实现
2017-12-05 13:15
531 查看
作用
游戏临近上线,需要做一个日志系统,记录玩家的行为,用途如下:监控玩家状态变化,如账号登记,角色创建,上线下线,充值等;
分析玩家行为,如金币钻石消耗在什么系统上了,主要参与了哪些活动和玩法;
帮助分析bug,记录玩家的行为和数据变化,可以回溯bug产生的过程;
方便客服,查询和处理玩家的反馈。
结构设计
首先,用一台公共的服务器左右日志的db服务器,所有游戏中产生的日志,都往这个db中写;然后,查询系统需要一个后端,与前端交互,来处理查询逻辑,反馈数据;
最后,需要一个前端,提交查询条件,展示查询结果。
实现
日志数据库
在网易无论手游还是端游,基本上都是用mongo,出来之后游戏数据库也就用了mongo,在我看来主要基于两个优点:游戏需求多变,mongo直接写json,省去需要建表改表的麻烦;
对于游戏数据库,无须完成逻辑,只要存数据就好,用不上复杂的SQL语句;
查询系统后端
系统后端直接用了nodejs,主要是基于经验和前段吧,因为之前用nodejs和python写过http服务器,可选的就是这两个了,加上前段用的网页,前后端统一就都用js了。// dao.js var MongoClient = require('mongodb').MongoClient; var StrUtils = require("./StrUtils"); var TIMEOUT = 3000;// 毫秒 function getConnStr(host, port, rpSetName, dbname) { return StrUtils.format("mongodb://{0}:{1}/{2}?connectTimeoutMS={3}&replicaSet={4}", host, port, dbname, TIMEOUT, rpSetName); }; function findDocuments(conditions, db, col, startIndex, rows, callback) { db.slaveOk = true; var collection = db.collection(col); var cursor = collection.find(conditions).sort({tm: -1}).skip(startIndex).limit(rows); cursor.toArray(function(err, docs) { if (err) { console.error(err); callback([], 0); return; } cursor.count(false, function(err, count) { if (err) { console.error(err); callback([], 0); return; } callback(docs, count); }); }); } function findRecord(host, port, rpSetName, dbname, colname, conditions, startIndex, rows, callback) { var conn = getConnStr(host, port, rpSetName, dbname, colname); MongoClient.connect(conn, function(err, db) { if (err) { console.error(err); callback([], 0); } else { findDocuments(conditions, db, colname, startIndex, rows, function(docs, cnt) { callback(docs, cnt); db.close(); }); } }); } exports.findRecord = findRecord;
前端
考虑到这个工具会时常更新,多方会用到,用客户端的话,用网页比较合适,更新之后刷新就可以了。关于日志的显示的表格,用了一个 jQeruy 的插件 jqGrid ,关于使用可以参考下我之前写过的博客。
这里有点技巧就是列名需要动态的获取,否则就要在客户端写很多Grid模板了,主要代码如下:
// log.js function createGrid(colNames, colModel, url) { var reader = { root: "rows",// 包含实际数据的数组 page: "page",// 当前页 total: "total",// 总页数 records: "records",// 查询出的记录数 repeatitems: true,// 每行的数据是可以重复的 cell: "cell",// 当前行所有单元格的数据数组 id: "id",// 行id userdata: "userdata"// 额外参数 }; var options = { // 请求 url: url, autoencode: true, datatype: "json", mtype: "GET", // 表格显示 caption: "查询结果", colNames: colNames, colModel: colModel, // 页数 rowNum: DEFAULT_ROW, rowList: [30, 50], pager: '#pager', page: 1, // 排序 sortable: false, sortname: 'accout', sortorder: "desc", // 尺寸 height: 'auto', width: 'auto', shrinkToFit: true, autowidth: true, // 附加功能 viewrecords: true, rownumbers: true, multiselect: false, cellEdit: false, hidegrid: false, // 数据解析 jsonReader: reader, loadComplete: function (jsonData) { if (jsonData.error) { alert(jsonData.error); return; } } }; var grid = $("#grid"); grid.jqGrid(options); } function getColModel(colNames, colWidth) { var colModel = []; for (var i = 0; i < colNames.length; i++) { var name = colNames[i]; var width = colWidth[i] || 10; colModel.push({name: name, sortable: false, width: width}); } return colModel; } function getUrlArgs(getCol, isExport) { var acc = $("input#account").val(); var tp = $("#sel_op").val(); var args = { usr: getCookie(COOKIE_KEY), getCol: getCol, acc: acc, tp: tp }; if (getCol) { return args; } else { args.sid = $("#sel_server").val(); args.channel = $("#sel_channel").val(); args.pkg = $("#sel_pkg").val(); args.fdate = $("#date_picker_from").datepicker('getDate').getTime(); args.tdate = $("#date_picker_to").datepicker('getDate').getTime(); args.name = $("input#name").val(); args.id = $("input#id").val(); if (isExport) { args.page = 1; args.rows = 0; } return args; } } function onClickQuery() { $.jgrid.gridUnload("#grid"); var url = getURL(HOST, PORT, "/roleinfo", getUrlArgs(true, false)); $.get(url, function(jsonData){ if (jsonData.error) { alert(jsonData.error); return; } var colNames = jsonData.colNames; var colWidth = jsonData.colWidth; if (colNames && colNames.length) { var colModel = getColModel(colNames, colWidth); var url = getURL(HOST, PORT, "/roleinfo", getUrlArgs(false, false)); createGrid(colNames, colModel, url); } else { alert("未知操作类型"); } }, "json"); }
另外,前端还有个导出csv的小功能,代码如下:
// export // 参考:http://jsfiddle.net/pxfunc/aa2t3ntt/1/ function JSONToCSVConvertor(arrData, title) { var CSV = ''; // 表头 CSV += title + '\r\n\n'; // 列名 var thList = []; var colNames = ""; for (var colName in arrData[0]) { colNames += colName + ','; thList.push(colName); } colNames = colNames.slice(0, -1); CSV += colNames + '\r\n'; // 数据 for (var i = 0; i < arrData.length; i++) { var data = arrData[i]; var line = ""; for (var j = 0; j < thList.length; j++) { var key = thList[j]; line += '"' + data[key] + '",'; } line.slice(0, line.length - 1); CSV += line + '\r\n'; } if (CSV === '') { alert("Invalid data"); return; } // 创建一个标签并自动点击下载,然后删除 var fileName = title.replace(/ /g,"_"); var uri = 'data:text/csv;charset=utf-8,\ufeff' + encodeURIComponent(CSV); var link = document.createElement("a"); link.href = uri; link.style = "visibility:hidden"; link.download = fileName + ".csv"; document.body.appendChild(link); link.click(); document.body.removeChild(link); }
webserver
同样也用nodejs简单地实现了一个,省去配置Apache或Nginx的麻烦,代码如下:// webserver.js var http = require('http'); var url = require('url'); var fs = require('fs'); var path = require('path'); var PORT = 9950; var mime = { "css": "text/css", "gif": "image/gif", "html": "text/html", "ico": "image/x-icon", "jpeg": "image/jpeg", "jpg": "image/jpeg", "js": "text/javascript", "json": "application/json", "pdf": "application/pdf", "png": "image/png", "svg": "image/svg+xml", "swf": "application/x-shockwave-flash", "tiff": "image/tiff", "txt": "text/plain", "wav": "audio/x-wav", "wma": "audio/x-ms-wma", "wmv": "video/x-ms-wmv", "xml": "text/xml", "woff": "application/x-woff", "woff2": "application/x-woff2", "tff": "application/x-font-truetype", "otf": "application/x-font-opentype", "eot": "application/vnd.ms-fontobject" }; var server = http.createServer(function(request, response) { var pathname = url.parse(request.url).pathname || "/index.html"; var realPath = path.join(".", pathname); var ext = path.extname(realPath); if (!ext) { pathname = "/index.html"; realPath = path.join(".", pathname); ext = path.extname(realPath); } ext = ext ? ext.slice(1) : 'unknown'; fs.exists(realPath, function(exists) { if (exists) { fs.readFile(realPath, "binary", function(err, file) { if (err) { response.writeHead(500, {'Content-Type': 'text/plain'}); response.end(err); } else { var contentType = mime[ext] || "text/plain"; response.writeHead(200, {'Content-Type': contentType}); response.write(file, "binary"); response.end(); } }); } else { response.writeHead(404, {'Content-Type': 'text/plain'}); response.write("This request URL " + pathname + " was not found on this server."); response.end(); } }); }); server.listen(PORT);
最后上截图:
注意:
js跨域问题,处理起来要小心
mongo分页查询skip,当数据过多时,会很慢
相关文章推荐
- 一步步实现j2me游戏引擎(一),系统日志
- Java日志系统框架的设计与实现
- .NET 日志系统设计思路及实现代码
- Linux下一个简单的日志系统的设计及其C代码实现
- 系统操作日志设计-代码实现
- 系统操作日志设计-代码实现
- 游戏任务成就体系的实现(附三):成就系统基于Mysql+Cache的数据库访问设计实现
- 观点:关于游戏系统的规划、设计与实现。
- 游戏任务成就体系的实现(附七):成就系统基于Redis的数据库访问设计实现
- 【游戏设计模式】之二 论撤消重做、回放系统的优雅实现:命令模式
- 系统操作日志设计-代码实现
- 设计日志三:3D游戏内的GUI系统
- Linux下一个简单的日志系统的设计及其C代码实现
- 【7. 日志分析模块】云跳板机服务系统设计及实现
- 一种日志结构文件系统的设计与实现(一)
- 系统操作日志设计-代码实现(转载)
- cocos Creator[框架封装之二] 日志系统 设计 与 实现
- 系统操作日志设计(二)-代码实现
- Linux下一个简单的日志系统的设计及其C代码实现
- 系统操作日志设计(二)-代码实现