需求分析
- 玩家进入遍布宝物的地图中,通过拾取宝物获取积分。
- 玩家积分排行
- 每个玩家的行动对其它玩家都是实时的
- 拾取宝物获得积分时会实时更新积分排行榜,更新对所有玩家实时可见。
- 宝物定时刷新
应用目录结构
文件 | 描述 |
---|---|
domain | domain表示数据和模型,比如玩家、宝物、移动等。 |
domain中的数据需要被序列化,因此需要定义序列化方法,比如toJSON。
domain中存在entity.js文件,entity是一个抽象的bean,意味着entity只是作为子bean的模板并不会被实例化,它会通过对象属性依赖注入到数值接口类中。
player作为entity的子类,通过在metadata配置中的parent继承了entity的prototype原型中的方法。
游戏服务器
网关服务器
连接服务器
创建连接服务器
连接服务器用于和客户端通讯,,要于客户端通信需建立一台前端服务器,用来维护与客户端的连接。处理客户端请求
场景服务器
- 场景服务器时游戏场景在服务端的抽象,根据游戏类型和内容不同其复杂度千差万别。
- 场景构成是一张开放的地图,地图中的存在玩家与定时刷新的宝物。
- 场景服务器要能够存储用户和宝物信息,可直接使用一个放在内存中的map存储场景中所有的实体。
- 将场景中中所有实体都抽象为一个Entity实体对象,放入map中。
- 为了能操作数据需暴露对外接口
接口类型
- 初始化接口:在init方法中设置场景信息并配置参数,同时启动场景中的时钟循环。
- 实体访问接口:比如addEntity、removeEntity等接口用于访问和修改场景中的实体
- 刷新场景中的宝物:当条件满足时外部事件会调用该接口刷新地图中的宝物。
使用一个无限循环的tick来驱动场景服务,在每个tick中更新场景中所有实体的状态信息。
安装依赖
创建项目
$ npm i -S pomelo
$ pomelo init ./treasure
$ cd treasure
$ npm-install.bat
安装游戏服依赖组件
$ cd game-server
$ npm i -S bearcat
$ npm i -S path
$ vim game-server/package.json
{
"name": "treasure",
"version": "0.0.1",
"private": false,
"dependencies": {
"bearcat": "^0.4.29",
"path": "^0.12.7",
"pomelo": "2.2.7"
}
}
服务器配置
服务器 | 名称 | 类型 |
---|---|---|
gate | 网关服务器 | 前端服务器 |
connector | 连接服务端 | 前端服务器 |
area | 地图服务器 | 后端服务器 |
$ vim game-server/config/adminServer.json
[
{
"type": "gate",
"token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
},
{
"type": "connector",
"token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
},
{
"type": "area",
"token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
}
]
$ vim game-server/config/servers.json
{
"development":{
"gate": [
{"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 3010, "frontend": true}
],
"connector": [
{"id": "connector-server-1", "host": "127.0.0.1", "port": 3150, "clientHost": "127.0.0.1", "clientPort": 3011, "frontend": true}
],
"area": [
{"id": "area-server-1", "host": "127.0.0.1", "port": 3250}
]
},
"production":{
"gate": [
{"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 3010, "frontend": true}
],
"connector": [
{"id": "connector-server-1", "host": "127.0.0.1", "port": 3150, "clientHost": "127.0.0.1", "clientPort": 3011, "frontend": true}
],
"area": [
{"id": "area-server-1", "host": "127.0.0.1", "port": 3250}
]
}
}
容器配置
$ vim game-server/context.json
{
"name": "treasure",
"scan": "app"
}
应用入口
$ vim game-server/app.js
const pomelo = require('pomelo');
const bearcat = require("bearcat");
const crc = require("crc");
//获取元数据配置文件的绝对路径
const abspath = require.resolve("./context.json");
//初始化IoC容器
bearcat.createApp([abspath]);
//启动IoC容器
bearcat.start(_=>{
console.log("bearcat ioc container started");
//创建应用
const app = pomelo.createApp();
//应用设置变量
app.set('name', 'treasure');
//应用配置全局服务器
app.configure('production|development', function(){
app.set('connectorConfig',
{
connector : pomelo.connectors.hybridconnector,
heartbeat : 10,
useDict : false,
useProtobuf : false
});
});
//路由负载均衡分配
const loadBalance = (session, serverType, channelName)=>{
const servers = app.getServersByType(serverType);
if(!servers || servers.length===0){
return false;
}
let index = 0;
let id = session.get(channelName);
if(!!id){
index = Math.abs(crc.crc32(id.toString())) % servers.length;
}
if(!servers[index]){
return false;
}
return servers[index].id;
};
//应用配置路由
app.route("area", (session, msg, app, callback)=>{
let serverId = loadBalance(session, "area", "cid");
if(!serverId){
callback(new Error("server not exists"));
return;
}
callback(null, serverId);
});
//开启应用
app.start();
});
process.on('uncaughtException', function (err) {
console.error(' Caught exception: ' + err.stack);
});
网关服务器
路由 | 描述 |
---|---|
gate.gateHandler.queryEntry | 获取连接服务器对外地址和端口 |
$ vim game-server/app/servers/gate/handler/gateHandler.js
const pomelo = require("pomelo");
const bearcat = require("bearcat");
const path = require("path");
const crc = require("crc");
let Handler = function(){
this.$id = path.basename(__filename, path.extname(__filename));
};
Handler.prototype.queryEntry = function(msg, session, next){
const app = pomelo.app;
const uid = msg.aid;
if(!uid){
next(null, {code:500});
return;
}
const servers = app.getServersByType("connector");
if(!servers || servers.length===0){
next(null, {code:500});
return;
}
const index = Math.abs(crc.crc32(uid.toString())) % servers.length;
const server = servers[index];
next(null, {code:200, data:{host:server.host, port:server.clientPort}});
};
module.exports = function(){
return bearcat.getBean(Handler);
};
连接器服务器
路由 | 描述 |
---|---|
connector.entryHandler.entry | 生成用户编号,设置用户对应的会话,设置连接与会话的对应关系。 |
$ vim game-server/app/servers/connector/handler/entryHandler.js
const bearcat = require("bearcat");
const pomelo = require("pomelo");
const path = require("path");
//ID自增产生器
let incId = 1;
let Handler = function(){
this.$id = path.basename(__filename, path.extname(__filename));
};
Handler.prototype.entry = function(msg, session, next){
const app = pomelo.app;
//获取参数
const userId = msg.aid;
const channelId = msg.cid;
//获取当前服务器编号中的数字
const serverId = app.get("serverId");
const svrId = serverId.split("-").pop();
//获取唯一用户编号
const uid = [svrId, channelId, userId, (++incId)].join("*");
//判断连接是否已绑定过 todo
//连接绑定用户编号
session.bind(uid);
//设置会话参数
session.set("cid", channelId);
session.pushAll();
//监听连接断开
session.on("closed", onClosed.bind(null, app));
//返回用户编号
next(null, {code:200, data:{uid}});
};
const onClosed = function(session, app){
if(session && session.uid){
app.rpc.area.playerRemote.kick(session, app.get("serverId"), session.uid, session.get("cid"), null);
}
};
module.exports = function(){
return bearcat.getBean(Handler);
};
地图服务器
Remote
playerRemote
$ vim game-server/app/servers/area/remote/playerRemote.js
const bearcat = require("bearcat");
const pomelo = require("pomelo");
const path = require("path");
let Remote = function(){
this.$id = path.basename(__filename, path.extname(__filename));
};
Remote.prototype.kick = function(serverId, uid, channelName, callback){
const app = pomelo.app;
const channelService = app.get("channelService");
const channel = channelService.getChannel(channelName, false);
channel.leave(uid, serverId);
channel.pushMessage("onKick", uid);
callback(uid);
};
module.exports = function(){
return bearcat.getBean(Remote);
};
Handler
playerHandler
$ vim game-server/app/servers/area/handler/playerHandler.js
const bearcat = require("bearcat");
const path = require("path");
const filename = path.basename(__filename, path.extname(__filename));
let Handler = function(app){
this.app = app;
this.areaService = null;
this.dataService = null;
};
/**玩家进入地图*/
Handler.prototype.enter = function(msg, session, next){
//获取参数
const playerId = msg.playerId || new Date().getTime();
const serverId = session.frontendId;
//console.log(session, session.frontendId);
//随机获取玩家角色
const role = this.dataService.init("role").getRandomData();
console.log(role);
const roleId = role.id;
//创建玩家
const player = bearcat.getBean("player", {playerId, roleId, serverId});
console.log(player);
//获取地图参数
const area = this.dataService.init("area").getRandomData();
console.log(area);
const areaId = area.id;
const width = area.width;
const height = area.height;
//获取地图服务
const areaService = this.areaService.init({areaId, width, height});
//console.log(areaService);
//地图添加随机玩家
let flag = areaService.addEntity(player);
console.log(flag, areaService);
if(!flag){
next(new Error("fail to add user into area"), {route:msg.route, code:200});
return;
}
//获取玩家与地图中所有实体信息
let data = {};
data.playerId = playerId;
data.area = this.areaService.getAreaInfo();
//返回地图数据和玩家数据
next(null, {code:200, data:data});
};
module.exports = function(app){
let bean = {};
bean.id = filename;
bean.func = Handler;
bean.args = [
{name:"app", value:app}
];
bean.props = [
{name:"areaService", ref:"areaService"},
{name:"dataService", ref:"dataService"},
];
return bearcat.getBean(bean);
};
数值处理
创建数值配置文件
$ vim game-server/app/data/area.json
[
["地图编号", "地图名称", "地图标识", "地图等级", "地图宽度", "地图高度", "数据地址"],
["id", "name", "identifier", "level", "width", "height", "dataurl"],
["1", "Oasis", "Oasis", 0, 2200, 1201, ""]
]
$ vim game-server/app/data/role.json
[
["角色编号", "角色名称", "角色标识", "角色等级", "初始血量", "初始魔法", "初始攻击值","初始防御值", "初始命中率", "初始闪避率","初始攻速", "初始移速","升级系数", "基础经验值"],
["id", "name", "identifier", "level", "healthPoint", "magicPoint", "attackValue", "defenceValue", "hitRate", "dodgeRate", "attackSpeed", "walkSpeed", "upgradeValue","baseExp"],
[201,"蜻蜓","Dragonfly","1",180,40,25,"8",90,15,"1",260,0.25,20],
[202,"鸟面人","Harpy","1",60,40,15,"8",90,10,"1",160,0.3,10],
[203,"灯泡龙","Bulb Dragon","3",15000,40,45,25,200,50,"1.8",360,0.28,2500],
[204,"蓝龙","BlueDragon","3",6000,40,40,28,90,0,0.6,180,0.27,500],
[205,"甲虫","Beetle","3",600,40,32,20,90,10,"1",220,0.25,55],
[206,"椰子怪","Coconut monster","1",300,40,22,13,90,10,"1",180,0.23,30],
[207,"石头怪","Rock","3",800,40,32,25,70,10,0.6,180,0.25,45],
[208,"独角仙","Unicorn Beetle","1",1600,40,30,18,90,10,"1",200,0.24,150],
[209,"食人花","Corpse flower","1",120,40,20,"8",90,"5","1",220,0.2,15],
[210,"天使","Angle","1",220,20,23,"9",90,13,"1.2",240,0.3,20],
[211,"炼金术士","Alchemist","1",180,60,18,12,95,10,"1.2",240,0.3,20]
]
$ vim game-server/app/data/treasure.json
[
["宝物编号","宝物名称","宝物标识","宝物描述","宝物类型","攻击值","防御值","卖出价格","宝物颜色","英雄等级","图片编号"],
["id","name","identifier","remark","kind","attackValue","defenceValue","price","color","heroLevel","imgId"],
["1","星火剑","Red tasselled pear","攻击力","Weapon",33,0,400,"white","4",301304],
["2","雷云剑","Double dagger","攻击力","Weapon",52,0,1800,"white",12,301504],
["3","极限法剑","Bronze dagger","攻击力","Weapon",71,0,3200,"white",20,301804],
["4","吴越剑","Wuyue sword","攻击力","Weapon",90,0,4600,"blue",28,301904],
["5","龙泉剑","Longquan sword","攻击力","Weapon",109,0,6000,"blue",36,304204],
["6","龙渊","Ebony trident","攻击力","Weapon",128,0,7400,"blue",44,304304],
["7","金蛇信","Golden snake sword","攻击力","Weapon",147,0,8800,"blue",52,304404],
["8","寒雪枪","Bronze axe","攻击力","Weapon",166,0,10200,"blue",60,304501],
["9","丰城剑","Spike knife","攻击力","Weapon",185,0,11600,"blue",68,304504],
[10,"悲欢剑","Bamboo double sword","攻击力","Weapon",204,0,13000,"blue",76,304584]
]
创建数值服务
$ vim game-server/app/service/dataService.js
const path = require("path");
const filename = path.basename(__filename, path.extname(__filename));
let DataService = function(){
this.name = "";
this.data = {};
};
DataService.prototype.init = function(name){
this.name = name;
if(this.data[this.name]===undefined){
this.load();
}
return this;
};
DataService.prototype.load = function(){
const file = path.join(path.resolve(__dirname, ".."), "data", this.name);
const json = require(file);
//console.log(json);
//获取字段
let fields = {};
json[1].forEach(function(value, index){
fields[value] = index;
});
//去掉数据中第一行与第二行
json.splice(0, 2);
//console.log(json);
//将数据转化为对象
let rows = {}, ids = [];
json.forEach(function(item){
let obj = {};
for(let key in fields){
let index = fields[key];
obj[key] = item[index];
}
let id = obj.id;
rows[id] = obj;
ids.push(id);
});
this.data[this.name] = {rows,ids};
};
DataService.prototype.findById = function(id){
const rows = this.data[this.name].rows;
return rows[id];
};
DataService.prototype.getRandomData = function(){
const ids = this.data[this.name].ids;
const rows = this.data[this.name].rows;
const length = ids.length;
const index = Math.floor(Math.random() * length);
const id = ids[index];
return rows[id];
};
DataService.prototype.getData = function(){
return this.data[this.name].rows;
};
DataService.prototype.getIds = function(){
return this.data[this.name].ids;
};
module.exports = {id:filename, func:DataService, scope:"prototype"};
常量服务
$ vim game-server/app/service/codeService.js
const path = require("path");
const filename = path.basename(__filename, path.extname(__filename));
let CodeService = function(){
this.EntityType = {PLAYER:"player", TREASURE:"treasure"};
};
module.exports = {id:filename, func:CodeService};
地图服务
$ vim game-server/app/service/areaService.js
const pomelo = require("pomelo");
const path = require("path");
const filename = path.basename(__filename, path.extname(__filename));
let AreaService = function(){
this.areaId = 0;
this.width = 0;
this.height = 0;
this.distance = 50;
this.entities = {};//实体列表
this.players = {};//玩家列表
this.reduced = {};//已删除的实体
this.channel = null;//频道对象
this.codeService = null;
};
AreaService.prototype.init = function(opts){
this.areaId = opts.areaId || 1;
this.width = opts.width || 0;
this.height = opts.height || 0;
return this;
};
/*地图增加实体*/
AreaService.prototype.addEntity = function(entity){
console.log(entity);
const self = this;
if(!entity || !entity.entityId){
return false;
}
this.entities[entity.entityId] = entity;
if(!entity.x && !entity.y){
const pos = this.setRandomPosition();
entity.x = pos.x;
entity.y = pos.y;
}
if(entity.entityType === this.codeService.EntityType.PLAYER){
this.players[entity.playerId] = entity.entityId;
//将用户和前端服务器对应关系存储到频道
if(entity.playerId && entity.serverId){
self.getChannel().add(entity.playerId, entity.serverId);
//玩家注册拾取事件
entity.on("pick", function(args){
//获取当前玩家
const player = self.entities[args.entityId];
//获取拾取目标
const target = self.entities[args.targetId];
if(target){
//玩家增加积分
//player.addScore(target.score);
//移除拾取目标
//self.removeEntity(args.targetId);
//推送拾取成功消息
//self.getChannel().pushMessage({route:"onPick", entityId:args.entityId, targetId:args.targetId, score:target.score});
}
});
}
}
return true;
};
/**地图移除实体*/
AreaService.prototype.removeEntity = function(entityId){
//判断实体是否存在
const entity = this.entities[entityId];
if(!entity){
return true;
}
//删除玩家实体
if(entity.entityType === this.codeService.EntityType.PLAYER){
//用户踢下线
this.getChannel().leave(entity.playerId, entity.serverId);
//忽略实体动作 todo
//从玩家列表中删除
delete this.players[entity.playerId];
}
//从实体集合中删除
delete this.entities[entityId];
//写入已删除对象
this.reduced.push(entityId);
return true;
};
AreaService.prototype.getChannel = function(){
if(!this.channel){
const app = pomelo.app;
const channelName = "area_"+this.areaId;
this.channel = app.get("channelService").getChannel(channelName, true);
}
return this.channel;
};
/*设置随机地图坐标*/
AreaService.prototype.setRandomPosition = function(){
const random = (min, max)=>Math.round(Math.random()*(max - min)) + min;
const x = random(this.distance, this.width - this.distance);
const y = random(this.distance, this.height - this.distance);
return {x, y};
};
/**获取地图与所有实体信息*/
AreaService.prototype.getAreaInfo = function(){
const areaId = this.areaId;
const width = this.width;
const height = this.height;
const entities = this.getEntities();
return {areaId, width, height, entities};
};
/**获取地图中所有实体信息*/
AreaService.prototype.getEntities = function(){
let result = {};
for(let entityId in this.entities){
result[entityId] = this.entities[entityId].toJson();
}
return result;
};
module.exports = {
id:filename,
func:AreaService,
props:[
{name:"codeService", ref:"codeService"}
]
};
实体处理
创建基础实体抽象父类
$ vim game-server/app/domain/entity.js
//加载事件模块中事件触发器
const EventEmitter = require("events").EventEmitter;
const path = require("path");
const util = require("util");
//获取当前文件名称
const filename = path.basename(__filename, path.extname(__filename));
//实体编号 自增唯一
let incId = 1;
/**实体构造函数*/
let Entity = function(opts){
EventEmitter .call(this);
//实体编号
this.entityId = incId++;
//实体类型
this.entityType = opts.entityType || "";
//前端服务器ID
this.serverId = opts.serverId || "";
//实体坐标
this.x = opts.x || 0;//X坐标值
this.y = opts.y || 0;//Y坐标值
};
/**Entity实体类使用原型链继承自EventEmitter事件触发器*/
util.inherits(Entity, EventEmitter );
//获取实体坐标
Entity.prototype.getPosition = function(){
const x = this.x;
const y = this.y;
return {x, y};
};
//设置实体坐标
Entity.prototype.setPosition = function(x, y){
this.x = x;
this.y = y;
};
//实体数据结构
Entity.prototype._toJson = function(){
let json = {};
json.entityId = this.id;
json.entityType = this.entityType;
json.x = this.x;
json.y = this.y;
json.serverId = this.serverId;
return json;
};
//抽象实体类
module.exports = {id:filename, func:Entity, abstract:true};
创建玩家实体
$ vim game-server/app/domain/player.js
const bearcat = require("bearcat");
const path = require("path");
//获取当前文件名称
const filename = path.basename(__filename, path.extname(__filename));
/**玩家构造函数*/
let Player = function(opts){
//实体公共属性
this.opts = opts;
this.opts["entityType"] = filename;
//玩家专用属性
this.playerId = opts.playerId || 0;//编号
this.roleId = opts.roleId || 0;//角色
this.score = opts.score || 0;//积分
};
/**玩家初始化*/
Player.prototype.init = function(){
const entity = bearcat.getFunction("entity");
entity.call(this, this.opts);
};
/**玩家增减积分*/
Player.prototype.addScore = function(score = 0){
this.score += score;
};
/**玩家数据结构*/
Player.prototype.toJson = function(){
let json = this._toJson();
json["playerId"] = this.playerId;
json["roleId"] = this.roleId;
json["score"] = this.score;
return json;
};
module.exports = {id:filename, func:Player, scope:"prototype", parent:"entity", init:"init", args:[{name:"opts", type:"Object"}]};
创建目标实体
$ vim game-server/app/domain/target.js
const path = require("path");
const bearcat = require("bearcat");
const filename = path.basename(__filename, path.extname(__filename));
const parentClass = "entity";
let Target = function(opts){
//父类实体属性
this.opts = opts;
this.entityType = filename;
//专有属性
this.score = opts.score || 0;
};
Target.prototype.init = function(){
const ParentClass = bearcat.getFunction(parentClass);
ParentClass.call(this, this.opts);
};
Target.prototype.toJson = function(){
//获取父类方法
let json = this.toJson();
//增加子类属性
json.score = this.score;
return json;
};
module.exports = {
id:filename,
func:Target,
args:[{name:"opts", type:"Object"}],
scope:"prototype",
init:"init",
parent:parentClass
};