需求分析
- 玩家進(jìn)入遍布寶物的地圖中忆矛,通過拾取寶物獲取積分皇帮。
- 玩家積分排行
- 每個玩家的行動對其它玩家都是實時的
- 拾取寶物獲得積分時會實時更新積分排行榜亚兄,更新對所有玩家實時可見礼旅。
- 寶物定時刷新
應(yīng)用目錄結(jié)構(gòu)
文件 | 描述 |
---|---|
domain | domain表示數(shù)據(jù)和模型,比如玩家、寶物、移動等抄课。 |
domain中的數(shù)據(jù)需要被序列化攒盈,因此需要定義序列化方法,比如toJSON。
domain中存在entity.js文件姿鸿,entity是一個抽象的bean笋熬,意味著entity只是作為子bean的模板并不會被實例化,它會通過對象屬性依賴注入到數(shù)值接口類中。
player作為entity的子類,通過在metadata配置中的parent繼承了entity的prototype原型中的方法。
游戲服務(wù)器
網(wǎng)關(guān)服務(wù)器
連接服務(wù)器
創(chuàng)建連接服務(wù)器
連接服務(wù)器用于和客戶端通訊,,要于客戶端通信需建立一臺前端服務(wù)器,用來維護(hù)與客戶端的連接捶枢。處理客戶端請求
場景服務(wù)器
- 場景服務(wù)器時游戲場景在服務(wù)端的抽象蒜鸡,根據(jù)游戲類型和內(nèi)容不同其復(fù)雜度千差萬別畜眨。
- 場景構(gòu)成是一張開放的地圖昼牛,地圖中的存在玩家與定時刷新的寶物。
- 場景服務(wù)器要能夠存儲用戶和寶物信息辜伟,可直接使用一個放在內(nèi)存中的map存儲場景中所有的實體。
- 將場景中中所有實體都抽象為一個Entity實體對象贪婉,放入map中。
- 為了能操作數(shù)據(jù)需暴露對外接口
接口類型
- 初始化接口:在init方法中設(shè)置場景信息并配置參數(shù)卢肃,同時啟動場景中的時鐘循環(huán)尤蒿。
- 實體訪問接口:比如addEntity、removeEntity等接口用于訪問和修改場景中的實體
- 刷新場景中的寶物:當(dāng)條件滿足時外部事件會調(diào)用該接口刷新地圖中的寶物逊脯。
使用一個無限循環(huán)的tick來驅(qū)動場景服務(wù)优质,在每個tick中更新場景中所有實體的狀態(tài)信息。
安裝依賴
創(chuàng)建項目
$ 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"
}
}
服務(wù)器配置
服務(wù)器 | 名稱 | 類型 |
---|---|---|
gate | 網(wǎng)關(guān)服務(wù)器 | 前端服務(wù)器 |
connector | 連接服務(wù)端 | 前端服務(wù)器 |
area | 地圖服務(wù)器 | 后端服務(wù)器 |
$ 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"
}
應(yīng)用入口
$ vim game-server/app.js
const pomelo = require('pomelo');
const bearcat = require("bearcat");
const crc = require("crc");
//獲取元數(shù)據(jù)配置文件的絕對路徑
const abspath = require.resolve("./context.json");
//初始化IoC容器
bearcat.createApp([abspath]);
//啟動IoC容器
bearcat.start(_=>{
console.log("bearcat ioc container started");
//創(chuàng)建應(yīng)用
const app = pomelo.createApp();
//應(yīng)用設(shè)置變量
app.set('name', 'treasure');
//應(yīng)用配置全局服務(wù)器
app.configure('production|development', function(){
app.set('connectorConfig',
{
connector : pomelo.connectors.hybridconnector,
heartbeat : 10,
useDict : false,
useProtobuf : false
});
});
//路由負(fù)載均衡分配
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;
};
//應(yīng)用配置路由
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);
});
//開啟應(yīng)用
app.start();
});
process.on('uncaughtException', function (err) {
console.error(' Caught exception: ' + err.stack);
});
網(wǎng)關(guān)服務(wù)器
路由 | 描述 |
---|---|
gate.gateHandler.queryEntry | 獲取連接服務(wù)器對外地址和端口 |
$ 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);
};
連接器服務(wù)器
路由 | 描述 |
---|---|
connector.entryHandler.entry | 生成用戶編號军洼,設(shè)置用戶對應(yīng)的會話巩螃,設(shè)置連接與會話的對應(yīng)關(guān)系。 |
$ vim game-server/app/servers/connector/handler/entryHandler.js
const bearcat = require("bearcat");
const pomelo = require("pomelo");
const path = require("path");
//ID自增產(chǎn)生器
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;
//獲取參數(shù)
const userId = msg.aid;
const channelId = msg.cid;
//獲取當(dāng)前服務(wù)器編號中的數(shù)字
const serverId = app.get("serverId");
const svrId = serverId.split("-").pop();
//獲取唯一用戶編號
const uid = [svrId, channelId, userId, (++incId)].join("*");
//判斷連接是否已綁定過 todo
//連接綁定用戶編號
session.bind(uid);
//設(shè)置會話參數(shù)
session.set("cid", channelId);
session.pushAll();
//監(jiān)聽連接斷開
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);
};
地圖服務(wù)器
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;
};
/**玩家進(jìn)入地圖*/
Handler.prototype.enter = function(msg, session, next){
//獲取參數(shù)
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;
//創(chuàng)建玩家
const player = bearcat.getBean("player", {playerId, roleId, serverId});
console.log(player);
//獲取地圖參數(shù)
const area = this.dataService.init("area").getRandomData();
console.log(area);
const areaId = area.id;
const width = area.width;
const height = area.height;
//獲取地圖服務(wù)
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();
//返回地圖數(shù)據(jù)和玩家數(shù)據(jù)
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);
};
數(shù)值處理
創(chuàng)建數(shù)值配置文件
$ vim game-server/app/data/area.json
[
["地圖編號", "地圖名稱", "地圖標(biāo)識", "地圖等級", "地圖寬度", "地圖高度", "數(shù)據(jù)地址"],
["id", "name", "identifier", "level", "width", "height", "dataurl"],
["1", "Oasis", "Oasis", 0, 2200, 1201, ""]
]
$ vim game-server/app/data/role.json
[
["角色編號", "角色名稱", "角色標(biāo)識", "角色等級", "初始血量", "初始魔法", "初始攻擊值","初始防御值", "初始命中率", "初始閃避率","初始攻速", "初始移速","升級系數(shù)", "基礎(chǔ)經(jīng)驗值"],
["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,"藍(lán)龍","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,"煉金術(shù)士","Alchemist","1",180,60,18,12,95,10,"1.2",240,0.3,20]
]
$ vim game-server/app/data/treasure.json
[
["寶物編號","寶物名稱","寶物標(biāo)識","寶物描述","寶物類型","攻擊值","防御值","賣出價格","寶物顏色","英雄等級","圖片編號"],
["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]
]
創(chuàng)建數(shù)值服務(wù)
$ 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;
});
//去掉數(shù)據(jù)中第一行與第二行
json.splice(0, 2);
//console.log(json);
//將數(shù)據(jù)轉(zhuǎn)化為對象
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"};
常量服務(wù)
$ 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};
地圖服務(wù)
$ 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;
//將用戶和前端服務(wù)器對應(yīng)關(guān)系存儲到頻道
if(entity.playerId && entity.serverId){
self.getChannel().add(entity.playerId, entity.serverId);
//玩家注冊拾取事件
entity.on("pick", function(args){
//獲取當(dāng)前玩家
const player = self.entities[args.entityId];
//獲取拾取目標(biāo)
const target = self.entities[args.targetId];
if(target){
//玩家增加積分
//player.addScore(target.score);
//移除拾取目標(biāo)
//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;
};
/*設(shè)置隨機地圖坐標(biāo)*/
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"}
]
};
實體處理
創(chuàng)建基礎(chǔ)實體抽象父類
$ vim game-server/app/domain/entity.js
//加載事件模塊中事件觸發(fā)器
const EventEmitter = require("events").EventEmitter;
const path = require("path");
const util = require("util");
//獲取當(dāng)前文件名稱
const filename = path.basename(__filename, path.extname(__filename));
//實體編號 自增唯一
let incId = 1;
/**實體構(gòu)造函數(shù)*/
let Entity = function(opts){
EventEmitter .call(this);
//實體編號
this.entityId = incId++;
//實體類型
this.entityType = opts.entityType || "";
//前端服務(wù)器ID
this.serverId = opts.serverId || "";
//實體坐標(biāo)
this.x = opts.x || 0;//X坐標(biāo)值
this.y = opts.y || 0;//Y坐標(biāo)值
};
/**Entity實體類使用原型鏈繼承自EventEmitter事件觸發(fā)器*/
util.inherits(Entity, EventEmitter );
//獲取實體坐標(biāo)
Entity.prototype.getPosition = function(){
const x = this.x;
const y = this.y;
return {x, y};
};
//設(shè)置實體坐標(biāo)
Entity.prototype.setPosition = function(x, y){
this.x = x;
this.y = y;
};
//實體數(shù)據(jù)結(jié)構(gòu)
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};
創(chuàng)建玩家實體
$ vim game-server/app/domain/player.js
const bearcat = require("bearcat");
const path = require("path");
//獲取當(dāng)前文件名稱
const filename = path.basename(__filename, path.extname(__filename));
/**玩家構(gòu)造函數(shù)*/
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;
};
/**玩家數(shù)據(jù)結(jié)構(gòu)*/
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"}]};
創(chuàng)建目標(biāo)實體
$ 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
};