您的位置:首页 > 其它

游戏日志系统设计与实现

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,当数据过多时,会很慢
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: