Skip to content
lizhengfu edited this page Jul 23, 2013 · 11 revisions

##游戏体验 在线地址

##部署游戏

lord of pomelo安装指南

##分析思路

游戏服务器的流程除了启动部分外,大部分事件和流程都是并发的,如果按照一个流程去描述这样一件事情,会很混乱,所以我会根据自己对代码的理解,分开不同用户模块,不同业务去分析Lordofpomelo的代码。

##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 操作。

API文档

添加实体对象更新 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();   //地图计时器,定时执行地图内的处理信息任务
};
  1. initMobZones 读取相对目录./map/mobzone.js 文件,初始化MobZone,通过读取game-server/config/data/character.json 文件来初始化。
  2. initNPCs 读取game-server/config/data/npc.json 生成NPC人物
  3. aiManager.start() 初始化AI行为,读取game-server/app/api/brain/目录下的ai行为文件,利用提供Pomelo-bt 行为树来控制ai的策略,通过aiManager 注册brain。当用户利用addEntity添加实例的时候,将ai行为添加到该实体。
  4. 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)的方式监测到对应的事件后,执行对应的回调事情。

未完,待续。

Clone this wiki locally