##游戏体验 在线地址
##部署游戏
##分析思路
游戏服务器的流程除了启动部分外,大部分事件和流程都是并发的,如果按照一个流程去描述这样一件事情,会很混乱,所以我会根据自己对代码的理解,分开不同用户模块,不同业务去分析Lordofpomelo的代码。
##Lordofpomelo 服务器介绍 各类服务器介绍
##Lordofpomelo启动流程
##用户登录模块
###用户流程
点击登录->输入账号和密码->输入正确
###流程图
1.客户端输入账号密码,发送到register服务器上。
web-server/public/js/ui/clientManager.js
$.post(httpHost + 'login', {username: username, password: pwd}, function(data) {
if (data.code === 501) {
alert('Username or password is invalid!');
loading = false;
return;
}
if (data.code !== 200) {
alert('Username is not exists!');
loading = false;
return;
}
authEntry(data.uid, data.token, function() { //发送token到gate服务器
loading = false;
});
localStorage.setItem('username', username);
});
function authEntry(uid, token, callback) {
queryEntry(uid, function(host, port) { //gate服务器
entry(host, port, token, callback);
});
}
2.Register服务器返回token
3.客户端连接到pomelo的GATE服务器上
pomelo.init({host: config.GATE_HOST, port: config.GATE_PORT, log: true}, function() {})
4.发送uid到pomelo的Gate服务器上,执行服务器函数gate.gateHandler.queryEntry,该函数分配其中一台connector为用户服务,返回改connector服务器对应的IP和端口,客户端收到返回信息后,断开与gate服务器连接,并获得connector服务器的IP和端口。
game-server/app/servers/gate/handler/gateHandler.js
Handler.prototype.queryEntry = function(msg, session, next) {
var uid = msg.uid;
if(!uid) {
next(null, {code: Code.FAIL});
return;
}
var connectors = this.app.getServersByType('connector');
if(!connectors || connectors.length === 0) {
next(null, {code: Code.GATE.NO_SERVER_AVAILABLE});
return;
}
var res = dispatcher.dispatch(uid, connectors);
next(null, {code: Code.OK, host: res.host, port: res.clientPort});
// next(null, {code: Code.OK, host: res.pubHost, port: res.clientPort});
};
5.根据获取的host和ip发送token到指定的connector服务器
客户端
web-server/public/js/ui/clientManager.js
pomelo.request('gate.gateHandler.queryEntry', { uid: uid}, function(data) {
pomelo.disconnect();
if(data.code === 2001) {
alert('Servers error!');
return;
}
callback(data.host, data.port);
});
6.执行connector.entryHandler.entry
7.将token发送到auth服务器,进行验证,验证没问题,生成session
8.服务器最后返回玩家信息给客户端
服务器端
game-server/app/servers/connector/handler/entryHandler.js
/**
* New client entry game server. Check token and bind user info into session.
*
* @param {Object} msg request message
* @param {Object} session current session object
* @param {Function} next next stemp callback
* @return {Void}
*/
pro.entry = function(msg, session, next) {
var token = msg.token, self = this; //验证token信息,生成session
if(!token) {
next(new Error('invalid entry request: empty token'), {code: Code.FAIL});
return;
}
var uid, players, player;
async.waterfall([
function(cb) {
// auth token
self.app.rpc.auth.authRemote.auth(session, token, cb); //通过
}, function(code, user, cb) {
// query player info by user id
if(code !== Code.OK) {
next(null, {code: code});
return;
}
if(!user) {
next(null, {code: Code.ENTRY.FA_USER_NOT_EXIST});
return;
}
uid = user.id;
userDao.getPlayersByUid(user.id, cb); //从数据库读取用户信息
}, function(res, cb) {
// generate session and register chat status
players = res;
self.app.get('sessionService').kick(uid, cb); //kick掉其他终端登录的用户
}, function(cb) {
session.bind(uid, cb);
}, function(cb) {
if(!players || players.length === 0) {
next(null, {code: Code.OK});
return;
}
player = players[0];
session.set('serverId', self.app.get('areaIdMap')[player.areaId]); //根据数据库的记录获取玩家在哪个地图服务器,将地图服务器写在session
session.set('playername', player.name); //用户名
session.set('playerId', player.id); //用户ID
session.on('closed', onUserLeave.bind(null, self.app));
session.pushAll(cb);
}, function(cb) {
self.app.rpc.chat.chatRemote.add(session, player.userId, player.name,
channelUtil.getGlobalChannelName(), cb); //添加玩家到聊天室
}
], function(err) {
if(err) {
next(err, {code: Code.FAIL});
return;
}
next(null, {code: Code.OK, player: players ? players[0] : null}); //返回用户信息
});
};
9.客户端收到玩家信息后,进行消息监听 loginMsgHandler监听登录和玩家在线情况,gameMsgHandler游戏逻辑信息监听,如移动行为等。
web-server/public/js/ui/clientManager.js
function entry(host, port, token, callback) {
// init handler
loginMsgHandler.init();
gameMsgHandler.init();
}
10.加载地图信息,加载地图怪物,人物信息。
客户端 web-server/public/js/ui/clientManager.js
function afterLogin(data) {
var userData = data.user;
var playerData = data.player;
var areaId = playerData.areaId;
var areas = {1: {map: {id: 'jiangnanyewai.png', width: 3200, height: 2400}, id: 1}}; //读取trim地图信息
if (!!userData) {
pomelo.uid = userData.id;
}
pomelo.playerId = playerData.id;
pomelo.areaId = areaId;
pomelo.player = playerData;
loadResource({jsonLoad: true}, function() {
//enterScene();
gamePrelude();
});
}
function loadResource(opt, callback) {
switchManager.selectView("loadingPanel");
var loader = new ResourceLoader(opt);
var $percent = $('#id_loadPercent').html(0);
var $bar = $('#id_loadRate').css('width', 0);
loader.on('loading', function(data) {
var n = parseInt(data.loaded * 100 / data.total, 10);
$bar.css('width', n + '%'); //加载地图进度
$percent.html(n);
});
loader.on('complete', function() { //完成
if (callback) {
setTimeout(function(){
callback();
}, 500);
}
});
loader.loadAreaResource();
}
web-server/public/js/utils/resourceLoader.js
pro.loadAreaResource = function() {
var self = this;
pomelo.request('area.resourceHandler.loadAreaResource', {},function(data) {
self.setTotalCount(1 + 1 + (data.players.length + data.mobs.length) * 16 + data.npcs.length + data.items.length + data.equipments.length);
self.loadJsonResource(function(){
self.setLoadedCount(self.loadedCount + 1);
self.loadMap(data.mapName);
self.loadCharacter(data.players);
self.loadCharacter(data.mobs);
self.loadNpc(data.npcs);
self.loadItem(data.items);
self.loadEquipment(data.equipments);
initObjectPools(data.mobs, EntityType.MOB);
initObjectPools(data.players, EntityType.PLAYER);
});
});
};
服务器
11.读取game-server/config/data目录下的配置信息,返回客户端
handler.loadResource = function(msg, session, next) {
var data = {};
if (msg.version.fightskill !== version.fightskill) {
data.fightskill = dataApi.fightskill.all(); //技能
}
if (msg.version.equipment !== version.equipment) {
data.equipment = dataApi.equipment.all(); //装备
}
if (msg.version.item !== version.item) { //物品
data.item = dataApi.item.all();
}
if (msg.version.character !== version.character) { //人物
data.character = dataApi.character.all();
}
if (msg.version.npc !== version.npc) { //npc
data.npc = dataApi.npc.all();
}
if (msg.version.animation !== version.animation) { //动物
data.animation = _getAnimationJson();
}
if (msg.version.effect !== version.effect) {
data.effect = require('../../../../config/effect.json');
}
next(null, {
data: data,
version: version
});
};
12.加载地图数据完毕后,执行enterScene进入场景
web-server/public/js/ui/clientManager.js
function enterScene(){
pomelo.request("area.playerHandler.enterScene", null, function(data){
app.init(data);
});
}
13.服务器area.playerHandler.enterScene
/**
* Player enter scene, and response the related information such as
* playerInfo, areaInfo and mapData to client.
*
* @param {Object} msg
* @param {Object} session
* @param {Function} next
* @api public
*/
handler.enterScene = function(msg, session, next) {
var area = session.area;
var playerId = session.get('playerId');
var areaId = session.get('areaId');
var teamId = session.get('teamId') || consts.TEAM.TEAM_ID_NONE;
var isCaptain = session.get('isCaptain');
var isInTeamInstance = session.get('isInTeamInstance');
var instanceId = session.get('instanceId');
utils.myPrint("1 ~ EnterScene: areaId = ", areaId);
utils.myPrint("1 ~ EnterScene: playerId = ", playerId);
utils.myPrint("1 ~ EnterScene: teamId = ", teamId);
userDao.getPlayerAllInfo(playerId, function(err, player) { //读取用户所有信息
if (err || !player) {
logger.error('Get user for userDao failed! ' + err.stack);
next(new Error('fail to get user from dao'), {
route: msg.route,
code: consts.MESSAGE.ERR
});
return;
}
player.serverId = session.frontendId;
player.teamId = teamId;
player.isCaptain = isCaptain;
player.isInTeamInstance = isInTeamInstance;
player.instanceId = instanceId;
areaId = player.areaId;
utils.myPrint("2 ~ GetPlayerAllInfo: player.instanceId = ", player.instanceId);
pomelo.app.rpc.chat.chatRemote.add(session, session.uid,
player.name, channelUtil.getAreaChannelName(areaId), null);
var map = area.map; //加入到 该地图的频道
//Reset the player's position if current pos is unreachable
if(!map.isReachable(player.x, player.y)){
var pos = map.getBornPoint(); //玩家的出生位置
player.x = pos.x;
player.y = pos.y;
}
var data = {
entities: area.getAreaInfo({x: player.x, y: player.y}, player.range),
curPlayer: player.getInfo(),
map: {
name : map.name,
width: map.width,
height: map.height,
tileW : map.tileW,
tileH : map.tileH,
weightMap: map.collisions
}
};
// utils.myPrint("1.5 ~ GetPlayerAllInfo data = ", JSON.stringify(data));
next(null, data); //发送data到客户端
utils.myPrint("2 ~ GetPlayerAllInfo player.teamId = ", player.teamId);
utils.myPrint("2 ~ GetPlayerAllInfo player.isCaptain = ", player.isCaptain);
if (!area.addEntity(player)) { 将玩家的最新信息添加到area
logger.error("Add player to area faild! areaId : " + player.areaId);
next(new Error('fail to add user into area'), {
route: msg.route,
code: consts.MESSAGE.ERR
});
return;
}
if (player.teamId > consts.TEAM.TEAM_ID_NONE) {
// send player's new info to the manager server(team manager)
var memberInfo = player.toJSON4TeamMember();
memberInfo.backendServerId = pomelo.app.getServerId();
pomelo.app.rpc.manager.teamRemote.updateMemberInfo(session, memberInfo, //更新队伍信息
function(err, ret) {
});
}
});
};
14.客户端收到服务器的信息后,执行app.init
web-server/public/js/app.js
/**
* Init client ara
* @param data {Object} The data for init area
*/
function init(data) {
var map = data.map;
pomelo.player = data.curPlayer;
switchManager.selectView('gamePanel');
if(inited){
configData(data);
area = new Area(data, map);
}else{
initColorBox();
configData(data);
area = new Area(data, map);
area.run();
chat.init();
inited = true;
}
ui.init();
}
##数据持久化模块
Lord采用Pomelo-sync从内存同步数据到数据库,该模块的作用是创建一个sql行为处理队列,每隔一段时间轮询一次,执行队列里的sql 操作。
添加实体对象更新 game-server/app/domain/area.js
Instance.prototype.addEntity = function(e) {
...
eventManager.addEvent(e);
...
}
game-server/app/domain/event/eventManager.js
/**
* Listen event for entity
*/
exp.addEvent = function(entity){
...
addSaveEvent(entity);
...
};
/**
* Add save event for player
* @param {Object} player The player to add save event for.
*/
function addSaveEvent(player) { //通过同步工具,回写相关信息到数据库
var app = pomelo.app;
player.on('save', function() {
app.get('sync').exec('playerSync.updatePlayer', player.id, player.strip());
});
player.bag.on('save', function() {
app.get('sync').exec('bagSync.updateBag', player.bag.id, player.bag);
});
player.equipments.on('save', function() {
app.get('sync').exec('equipmentsSync.updateEquipments', player.equipments.id, player.equipments);
});
}
Pomelo-sync的模块提供了exec方法,当函数收到save事件后,执行exec,将操作行为放到数据库队列里面,每隔一段时间执行。
如何发送save事件呢?
game-server/app/domain/persistent.js
/**
* Persistent object, it is saved in database
*
* @param {Object} opts
* @api public
*/
var Persistent = function(opts) {
this.id = opts.id;
this.type = opts.type;
EventEmitter.call(this);
};
util.inherits(Persistent, EventEmitter);
module.exports = Persistent;
// Emit the event 'save'
Persistent.prototype.save = function() {
this.emit('save');
};
这个是可持久化对象的基类,所有的子类都可以调用基类的方法,如equipments装备,executeTask任务,fightskill,通过执行基类的方法,向EventEmitter发送事件,监听的事件得到相应后,写入同步数据库缓冲队列,每隔一段时间回写到服务器。
##场景管理模块
###简介 Lordofpomelo中每个场景对应一个独立的场景服务器,所有的业务逻辑都在场景服务器内部进行。
###主流程
初始化场景管理模块game-server/app/domain/area/area.js
/**
* Init areas
* @param {Object} opts
* @api public
*/
var Instance = function(opts){
this.areaId = opts.id;
this.type = opts.type;
this.map = opts.map;
//The map from player to entity
this.players = {}; //玩家
this.users = {};
this.entities = {}; //实体
this.zones = {}; //地区
this.items = {}; //物品
this.channel = null;
this.playerNum = 0;
this.emptyTime = Date.now();
//Init AOI
this.aoi = aoiManager.getService(opts);
this.aiManager = ai.createManager({area:this}); //怪物ai 工厂方法
this.patrolManager = patrol.createManager({area:this}); //patrol 巡逻工厂方法
this.actionManager = new ActionManager(); //action 动作工厂方法
this.timer = new Timer({
area : this,
interval : 100
});
this.start();
};
启动场景管理服务 game-server/app/domain/area/area.js
/**
* @api public
*/
Instance.prototype.start = function() {
aoiEventManager.addEvent(this, this.aoi.aoi); //aoi监听事件
//Init mob zones
this.initMobZones(this.map.getMobZones()); //初始化怪物空间
this.initNPCs(this); //初始化NPC
this.aiManager.start(); //AI管理服务启动
this.timer.run(); //地图计时器,定时执行地图内的处理信息任务
};
- initMobZones 读取相对目录./map/mobzone.js 文件,初始化MobZone,通过读取game-server/config/data/character.json 文件来初始化。
- initNPCs 读取game-server/config/data/npc.json 生成NPC人物
- aiManager.start() 初始化AI行为,读取game-server/app/api/brain/目录下的ai行为文件,利用提供Pomelo-bt 行为树来控制ai的策略,通过aiManager 注册brain。当用户利用addEntity添加实例的时候,将ai行为添加到该实体。
- timer.run() 执行地图的tick,轮询地图内的变量,当变量有变化的时候,通知客户端。
game-server/app/domain/area/timer.js
var Timer = function(opts){
this.area = opts.area;
this.interval = opts.interval||100;
};
Timer.prototype.run = function () {
this.interval = setInterval(this.tick.bind(this), this.interval); //定时执行 tick
};
Timer.prototype.tick = function() {
var area = this.area;
//Update mob zones
for(var key in area.zones){
area.zones[key].update(); //遍历 所有zones的更新
}
//Update all the items
for(var id in area.items) { //检查人物状态值
var item = area.entities[id];
item.update();
if(item.died) { //如果角色死亡,向客户端发送消息
area.channel.pushMessage('onRemoveEntities', {entities: [id]});
area.removeEntity(id);
}
}
//run all the action
area.actionManager.update(); //动作更新
area.aiManager.update(); //ai 更新,检查ai反应动作
area.patrolManager.update(); //patrol巡逻动作更新
};
- area.zones[key].update() 定时刷怪
- item.update(); 检查用户生命时间,若到0,则玩家状态变成死亡。
- area.actionManager.update() 将日常攻击,移动的动作寄存在一个一个队列里面,定时将队列里面的动作执行和清空
- area.aiManager.update() ai根据行为树,作出反应,让怪物可以主动攻击玩家
- area.patrolManager.update() 怪物巡逻
###动作缓冲机制
在Area地图Tick下 area.actionManager.update() 读取action数组,执行action行为。
game-server/app/domain/action/actionManager.js 初始化动作队列
/**
* Action Manager, which is used to contrll all action
*/
var ActionManager = function(opts){
opts = opts||{};
this.limit = opts.limit||10000;
//The map used to abort or cancel action, it's a two level map, first leven key is type, second leven is id
this.actionMap = {};
//The action queue, default size is 10000, all action in the action queue will excute in the FIFO order
this.actionQueue = new Queue(this.limit);
};
添加动作到动作队列
/**
* Add action
* @param {Object} action The action to add, the order will be preserved
*/
ActionManager.prototype.addAction = function(action){
if(action.singleton) {
this.abortAction(action.type, action.id);
}
this.actionMap[action.type] = this.actionMap[action.type]||{};
this.actionMap[action.type][action.id] = action;
return this.actionQueue.push(action);
};
遍历动作数组里的所有执行动作,并执行该动作的update 方法
/**
* Update all action
* @api public
*/
ActionManager.prototype.update = function(){
var length = this.actionQueue.length;
for(var i = 0; i < length; i++){
var action = this.actionQueue.pop();
if(action.aborted){
continue;
}
action.update();
if(!action.finished){
this.actionQueue.push(action);
}else{
delete this.actionMap[action.type][action.id];
}
}
};
Example:当客户端发送一个玩家移动行为的时候,服务器将创建一个Move对象
var action = new Move({
entity: player,
path: path,
speed: speed
});
当执行area.actionManager.update()时,将执行动作队列里的Move.update的方法;
game-server/app/domain/action/move.js
/**
* Update the move action, it will calculate and set the entity's new position, and update AOI module
*/
Move.prototype.update = function(){
this.tickNumber++;
var time = Date.now()-this.time;
....
};
总得来说,为了避免太多的动作行为,导致服务器多次响应,所以采用一个队列,隔一段短时间,处理一次。
###AI管理模块
/game-server/app/ai/service/aiManager.js
为角色添加AI行为和行为准则
/**
* Add a character into ai manager.
* Add a brain to the character if the type is mob.
* Start the tick if it has not started yet.
*/
pro.addCharacters = function(cs) {
...
brain = this.brainService.getBrain('player', Blackboard.create({
manager: this,
area: this.area,
curCharacter: c
}));
this.players[c.entityId] = brain;
}
....
};
读取game-server/app/ai/brain目录下的所有行为模式。lord目录下,有player.js和tiger.js ,将动作行为,添加到this.mobs[]下
以怪物来做案例 game-server/app/ai/brain/tiger.js
var bt = require('pomelo-bt'); //初始化了 ai的行为树
行为树原理 http://www.cnblogs.com/cnas3/archive/2011/08/14/2138445.html
pomelo-bt API https://github.com/NetEase/pomelo-bt
持续攻击行为
var loopAttack = new Loop({
blackboard: blackboard,
child: attack,
loopCond: checkTarget
});
行为树的Loop循环节点,循环判断checkTarget检查对象是否存在,如果存在,则一直攻击
如果有目标,则开始执行持续攻击
var attackIfHaveTarget = new If({
blackboard: blackboard,
cond: haveTarget,
action: loopAttack
});
使用了行为树中的条件节点,当haveTarget的作用是检查角色里面target有没锁定对象符合条件,则展开loopAttack持续攻击
//find nearby target action
//var findTarget = new FindNearbyPlayer({blackboard: blackboard});
//patrol action
var patrol = new Patrol({blackboard: blackboard});
//composite them together
this.action = new Select({
blackboard: blackboard
});
this.action.addChild(attackIfHaveTarget);
//this.action.addChild(findTarget);
this.action.addChild(patrol);
怪物的行为策略为,Select 顺序节点,优先选择攻击附近对象,其次是巡逻,通过行为树的组合,组合成了AI。
遍历所有怪物 game-server/app/service/aiManager.js
/**
* Update all the managed characters.
* Stop the tick if there is no ai mobs.
*/
pro.update = function() {
if(!this.started || this.closed) {
return;
}
var id;
for(id in this.players) {
if(typeof this.players[id].update === 'function') {
this.players[id].update();
}
}
for(id in this.mobs) {
if(typeof this.mobs[id].update === 'function') {
this.mobs[id].update();
}
}
};
遍历this.mobs的怪物对象,执行对象的update方法,执行doAction
pro.update = function() {
return this.action.doAction();
};
doAction 遍历行为树,根据行为树的设定,执行响应的 action。
###AOI灯塔模块
Lord采用的是思路,空间切割监视的灯塔设计,将场景分为等大的格子,在对象进入或退出格子时,维护每个灯塔上的对象列表。
####pomelo-aoi文档 https://github.com/NetEase/pomelo-aoi/blob/master/README.md
实际使用的时候很简单
- 当一个人第一次登入到地图的时候,我们就调用aoi.addObject(obj, pos) 添加对象到aoi上,通知附近观察者,aoi.addWatcher(watcher, oldPos, newPos, oldRange, newRange);
- 当一个人移动的时候,那么我们就调用aoi.updateObject(obj, oldPos, newPos);更新个人位置,通知其他观察者updateWatcher(watcher, oldPos, newPos, oldRange, newRange);
- Watcher 相当于人物的视野
- Object 相当于在塔的对象
当aoi服务的对象,产生变化的时候,会激活回调事件
aoiEventManager.addEvent(this, this.aoi.aoi); //aoi监听事件
//Add event for aoi
exp.addEvent = function(area, aoi){
aoi.on('add', function(params){
params.area = area;
switch(params.type){
case EntityType.PLAYER:
onPlayerAdd(params);
break;
case EntityType.MOB:
onMobAdd(params);
break;
}
});
aoi.on('remove', function(params){
params.area = area;
switch(params.type){
case EntityType.PLAYER:
onPlayerRemove(params);
break;
case EntityType.MOB:
break;
}
});
aoi.on('update', function(params){
params.area = area;
switch(params.type){
case EntityType.PLAYER:
onObjectUpdate(params);
break;
case EntityType.MOB:
onObjectUpdate(params);
break;
}
});
aoi.on('updateWatcher', function(params) {
params.area = area;
switch(params.type) {
case EntityType.PLAYER:
onPlayerUpdate(params);
break;
}
});
};
根据AOI不同的事件回调,向客户端发出不同的回调事件。如添加实物,附近玩家等信息。
##点击实物模块
###用户流程
点击实物->怪物->攻击行为
点击实物->NPC->聊天
点击实物->玩家->组队或战斗
###程序流程
1.客户端绑定鼠标点击实体事件
web-server/public/js/componentAdder.js
/**
* Mouse click handlerFunction
*/
var launchAi = function (event, node) {
var id = node.id;
if (event.type === 'mouseClicked') {
clientManager.launchAi({id: id});
}
};
绑定鼠标点击实体事件到launchAi函数
2.检查鼠标点击实物的事件,属于哪种类型
web-server/js/ui/clientManager.js
function launchAi(args) {
var areaId = pomelo.areaId;
var playerId = pomelo.playerId;
var targetId = args.id;
if (pomelo.player.entityId === targetId) {
return;
}
var skillId = pomelo.player.curSkill;
var area = app.getCurArea();
var entity = area.getEntity(targetId);
if (entity.type === EntityType.PLAYER || entity.type === EntityType.MOB) { //被攻击的对象类型判断
if (entity.died) {
return;
}
if (entity.type === EntityType.PLAYER) { //如果是玩家,弹出选项,组队或者交易等
var curPlayer = app.getCurPlayer();
pomelo.emit('onPlayerDialog', {targetId: targetId, targetPlayerId: entity.id,
targetTeamId: entity.teamId, targetIsCaptain: entity.isCaptain,
myTeamId: curPlayer.teamId, myIsCaptain: curPlayer.isCaptain});
} else if (entity.type === EntityType.MOB) {
pomelo.notify('area.fightHandler.attack',{targetId: targetId}); //通知服务器处理攻击事件,不要求回调
}
} else if (entity.type === EntityType.NPC) { //如果是NPC是对话模式
pomelo.notify('area.playerHandler.npcTalk',{areaId :areaId, playerId: playerId, targetId: targetId});
} else if (entity.type === EntityType.ITEM || entity.type === EntityType.EQUIPMENT) { //检查一下捡东西相关的
var curPlayer = app.getCurPlayer();
var bag = curPlayer.bag;
if (bag.isFull()) {
curPlayer.getSprite().hintOfBag();
return;
}
pomelo.notify('area.playerHandler.pickItem',{areaId :areaId, playerId: playerId, targetId: targetId}); //捡东西
}
}
3.不同类型的点击行为对应不同的服务器响应函数
- 怪物: area.fightHandler.attack
- NPC: area.playerHandler.npcTalk
- 捡东西: area.playerHandler.pickItem
点击玩家后出现对话框 对话框选项,可以根据需求
/**
* Execute player action
*/
function exec(type, params) {
switch (type) {
case btns.ATTACK_PLAYER: {
attackPlayer(params); //攻击玩家
}
break;
case btns.APPLY_JOIN_TEAM: {
applyJoinTeam(params); //加入队伍
}
break;
case btns.INVITE_JOIN_TEAM: {
inviteJoinTeam(params); //邀请加入队伍
}
break;
}
}
- 攻击玩家: area.fightHandler.attack
- 加入队伍: area.teamHandler.applyJoinTeam
- 邀请队伍: area.teamHandler.inviteJoinTeam
4.执行服务端程序
area.fightHandler.attack
/**
* Action of attack.
* Handle the request from client, and response result to client
* if error, the code is consts.MESSAGE.ERR. Or the code is consts.MESSAGE.RES
*
* @param {Object} msg
* @param {Object} session
* @api public
*/
handler.attack = function(msg, session, next) {
var player = session.area.getPlayer(session.get('playerId'));
var target = session.area.getEntity(msg.targetId);
if(!target || !player || (player.target === target.entityId) || (player.entityId === target.entityId) || target.died){
next();
return;
} //数据校验
session.area.timer.abortAction('move', player.entityId); //停止移动
player.target = target.entityId; //锁定攻击目标
next();
};
area.playerHandler.npcTalk
handler.npcTalk = function(msg, session, next) {
var player = session.area.getPlayer(session.get('playerId'));
player.target = msg.targetId;
next();
};
area.playerHandler.pickItem
/**
* Player pick up item.
* Handle the request from client, and set player's target
*
* @param {Object} msg
* @param {Object} session
* @param {Function} next
* @api public
*/
handler.pickItem = function(msg, session, next) {
var area = session.area;
var player = area.getPlayer(session.get('playerId'));
var target = area.getEntity(msg.targetId);
if(!player || !target || (target.type !== consts.EntityType.ITEM && target.type !== consts.EntityType.EQUIPMENT)){
next(null, {
route: msg.route,
code: consts.MESSAGE.ERR
});
return;
}
player.target = target.entityId;
next();
};
上述三个处理函数都有一个共同点,只设置了player.target就返回给客户端了,这当中包含了什么玄机?请回忆起场景处理模块。
1.客户端发送请求到服务器
2.服务器修改play.target 为taget添加对象
3.场景通过tick对area.aiManager.update()进行更新,根据ai行为树,检测到target存在对象,判断对象是否能被攻击,是否能谈话,是否能捡起来,分配到不同的处理函数,处理函数执行完毕后,服务器端通过pushMessage等方式,向客户端发出广播信息,客户端通过pomelo.on(event,func)的方式监测到对应的事件后,执行对应的回调事情。