遊戲體驗
部署遊戲
分析思路
遊戲服務器的流程除了啓動部分外,大部分事件和流程都是併發的,如果按照一個流程去描述這樣一件事情,會很混亂,所以我會根據自己對代碼的理解,分開不同用戶模塊,不同業務去分析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)的方式監測到對應的事件後,執行對應的回調事情。
未完,待續。