Pomelo Treasures

需求分析

  • 玩家进入遍布宝物的地图中,通过拾取宝物获取积分。
  • 玩家积分排行
  • 每个玩家的行动对其它玩家都是实时的
  • 拾取宝物获得积分时会实时更新积分排行榜,更新对所有玩家实时可见。
  • 宝物定时刷新

应用目录结构

文件描述
domaindomain表示数据和模型,比如玩家、宝物、移动等。

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
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值