游戏体验
部署游戏
分析思路
游戏服务器的流程除了启动部分外,大部分事件和流程都是并发的,如果按照一个流程去描述这样一件事情,会很混乱,所以我会根据自己对代码的理解,分开不同用户模块,不同业务去分析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)的方式监测到对应的事件后,执行对应的回调事情。
未完,待续。