Lordofpomelo游戏分析

游戏体验

在线地址

部署游戏

lord of pomelo安装指南

分析思路

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

Lordofpomelo 服务器介绍

各类服务器介绍

服务器图

Lordofpomelo启动流程

Lordofpomelo 启动流程

用户登录模块

用户流程

点击登录->输入账号和密码->输入正确

流程图

登录流程图

1.客户端输入账号密码,发送到register服务器上。

web-server/public/js/ui/clientManager.js


 
 
  1. $.post(httpHost + 'login', { username: username, password: pwd}, function(data) {
  2. if (data.code === 501) {
  3. alert( 'Username or password is invalid!');
  4. loading = false;
  5. return;
  6. }
  7. if (data.code !== 200) {
  8. alert( 'Username is not exists!');
  9. loading = false;
  10. return;
  11. }
  12. authEntry(data.uid, data.token, function() { //发送token到gate服务器
  13. loading = false;
  14. });
  15. localStorage.setItem( 'username', username);
  16. });
  17. function authEntry(uid, token, callback) {
  18. queryEntry(uid, function(host, port) { //gate服务器
  19. entry(host, port, token, callback);
  20. });
  21. }

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


 
 
  1. Handler.prototype.queryEntry = function(msg, session, next) {
  2. var uid = msg.uid;
  3. if(!uid) {
  4. next( null, { code: Code.FAIL});
  5. return;
  6. }
  7. var connectors = this.app.getServersByType( 'connector');
  8. if(!connectors || connectors.length === 0) {
  9. next( null, { code: Code.GATE.NO_SERVER_AVAILABLE});
  10. return;
  11. }
  12. var res = dispatcher.dispatch(uid, connectors);
  13. next( null, { code: Code.OK, host: res.host, port: res.clientPort});
  14. // next(null, {code: Code.OK, host: res.pubHost, port: res.clientPort});
  15. };

5.根据获取的host和ip发送token到指定的connector服务器

客户端

web-server/public/js/ui/clientManager.js


 
 
  1. pomelo.request( 'gate.gateHandler.queryEntry', { uid: uid}, function( data) {
  2. pomelo.disconnect();
  3. if( data.code === 2001) {
  4. alert( 'Servers error!');
  5. return;
  6. }
  7. callback( data.host, data.port);
  8. });

6.执行connector.entryHandler.entry

7.将token发送到auth服务器,进行验证,验证没问题,生成session

8.服务器最后返回玩家信息给客户端

服务器端

game-server/app/servers/connector/handler/entryHandler.js


 
 
  1. /**
  2. * New client entry game server. Check token and bind user info into session.
  3. *
  4. * @param {Object} msg request message
  5. * @param {Object} session current session object
  6. * @param {Function} next next stemp callback
  7. * @return {Void}
  8. */
  9. pro.entry = function(msg, session, next) {
  10. var token = msg.token, self = this; //验证token信息,生成session
  11. if(!token) {
  12. next( new Error( 'invalid entry request: empty token'), {code: Code.FAIL});
  13. return;
  14. }
  15. var uid, players, player;
  16. async.waterfall([
  17. function(cb) {
  18. // auth token
  19. self.app.rpc.auth.authRemote.auth(session, token, cb); //通过
  20. }, function(code, user, cb) {
  21. // query player info by user id
  22. if(code !== Code.OK) {
  23. next( null, {code: code});
  24. return;
  25. }
  26. if(!user) {
  27. next( null, {code: Code.ENTRY.FA_USER_NOT_EXIST});
  28. return;
  29. }
  30. uid = user.id;
  31. userDao.getPlayersByUid(user.id, cb); //从数据库读取用户信息
  32. }, function(res, cb) {
  33. // generate session and register chat status
  34. players = res;
  35. self.app.get( 'sessionService').kick(uid, cb); //kick掉其他终端登录的用户
  36. }, function(cb) {
  37. session.bind(uid, cb);
  38. }, function(cb) {
  39. if(!players || players.length === 0) {
  40. next( null, {code: Code.OK});
  41. return;
  42. }
  43. player = players[ 0];
  44. session.set( 'serverId', self.app.get( 'areaIdMap')[player.areaId]); //根据数据库的记录获取玩家在哪个地图服务器,将地图服务器写在session
  45. session.set( 'playername', player.name); //用户名
  46. session.set( 'playerId', player.id); //用户ID
  47. session.on( 'closed', onUserLeave.bind( null, self.app));
  48. session.pushAll(cb);
  49. }, function(cb) {
  50. self.app.rpc.chat.chatRemote.add(session, player.userId, player.name,
  51. channelUtil.getGlobalChannelName(), cb); //添加玩家到聊天室
  52. }
  53. ], function(err) {
  54. if(err) {
  55. next(err, {code: Code.FAIL});
  56. return;
  57. }
  58. next( null, {code: Code.OK, player: players ? players[ 0] : null}); //返回用户信息
  59. });
  60. };

9.客户端收到玩家信息后,进行消息监听 loginMsgHandler监听登录和玩家在线情况,gameMsgHandler游戏逻辑信息监听,如移动行为等。

web-server/public/js/ui/clientManager.js


 
 
  1. function entry(host, port, token, callback) {
  2. // init handler
  3. loginMsgHandler.init();
  4. gameMsgHandler.init();
  5. }

10.加载地图信息,加载地图怪物,人物信息。

客户端 web-server/public/js/ui/clientManager.js


 
 
  1. function afterLogin(data) {
  2. var userData = data.user;
  3. var playerData = data.player;
  4. var areaId = playerData.areaId;
  5. var areas = { 1: {map: {id: 'jiangnanyewai.png', width: 3200, height: 2400}, id: 1}}; //读取trim地图信息
  6. if (!!userData) {
  7. pomelo.uid = userData.id;
  8. }
  9. pomelo.playerId = playerData.id;
  10. pomelo.areaId = areaId;
  11. pomelo.player = playerData;
  12. loadResource({jsonLoad: true}, function() {
  13. //enterScene();
  14. gamePrelude();
  15. });
  16. }
  17. function loadResource(opt, callback) {
  18. switchManager.selectView( "loadingPanel");
  19. var loader = new ResourceLoader(opt);
  20. var $percent = $( '#id_loadPercent').html( 0);
  21. var $bar = $( '#id_loadRate').css( 'width', 0);
  22. loader.on( 'loading', function(data) {
  23. var n = parseInt(data.loaded * 100 / data.total, 10);
  24. $bar.css( 'width', n + '%'); //加载地图进度
  25. $percent.html(n);
  26. });
  27. loader.on( 'complete', function() { //完成
  28. if (callback) {
  29. setTimeout( function(){
  30. callback();
  31. }, 500);
  32. }
  33. });
  34. loader.loadAreaResource();
  35. }

web-server/public/js/utils/resourceLoader.js


 
 
  1. pro.loadAreaResource = function() {
  2. var self = this;
  3. pomelo.request( 'area.resourceHandler.loadAreaResource', {}, function(data) {
  4. self.setTotalCount( 1 + 1 + (data.players.length + data.mobs.length) * 16 + data.npcs.length + data.items.length + data.equipments.length);
  5. self.loadJsonResource( function(){
  6. self.setLoadedCount( self.loadedCount + 1);
  7. self.loadMap(data.mapName);
  8. self.loadCharacter(data.players);
  9. self.loadCharacter(data.mobs);
  10. self.loadNpc(data.npcs);
  11. self.loadItem(data.items);
  12. self.loadEquipment(data.equipments);
  13. initObjectPools(data.mobs, EntityType.MOB);
  14. initObjectPools(data.players, EntityType.PLAYER);
  15. });
  16. });
  17. };

服务器

11.读取game-server/config/data目录下的配置信息,返回客户端


 
 
  1. handler.loadResource = function(msg, session, next) {
  2. var data = {};
  3. if (msg.version.fightskill !== version.fightskill) {
  4. data.fightskill = dataApi.fightskill.all(); //技能
  5. }
  6. if (msg.version.equipment !== version.equipment) {
  7. data.equipment = dataApi.equipment.all(); //装备
  8. }
  9. if (msg.version.item !== version.item) { //物品
  10. data.item = dataApi.item.all();
  11. }
  12. if (msg.version.character !== version.character) { //人物
  13. data.character = dataApi.character.all();
  14. }
  15. if (msg.version.npc !== version.npc) { //npc
  16. data.npc = dataApi.npc.all();
  17. }
  18. if (msg.version.animation !== version.animation) { //动物
  19. data.animation = _getAnimationJson();
  20. }
  21. if (msg.version.effect !== version.effect) {
  22. data.effect = require( '../../../../config/effect.json');
  23. }
  24. next( null, {
  25. data: data,
  26. version: version
  27. });
  28. };

12.加载地图数据完毕后,执行enterScene进入场景

web-server/public/js/ui/clientManager.js


 
 
  1. function enterScene(){
  2. pomelo.request( "area.playerHandler.enterScene", null, function(data){
  3. app.init(data);
  4. });
  5. }

13.服务器area.playerHandler.enterScene


 
 
  1. /**
  2. * Player enter scene, and response the related information such as
  3. * playerInfo, areaInfo and mapData to client.
  4. *
  5. * @param {Object} msg
  6. * @param {Object} session
  7. * @param {Function} next
  8. * @api public
  9. */
  10. handler.enterScene = function(msg, session, next) {
  11. var area = session.area;
  12. var playerId = session.get( 'playerId');
  13. var areaId = session.get( 'areaId');
  14. var teamId = session.get( 'teamId') || consts.TEAM.TEAM_ID_NONE;
  15. var isCaptain = session.get( 'isCaptain');
  16. var isInTeamInstance = session.get( 'isInTeamInstance');
  17. var instanceId = session.get( 'instanceId');
  18. utils.myPrint( "1 ~ EnterScene: areaId = ", areaId);
  19. utils.myPrint( "1 ~ EnterScene: playerId = ", playerId);
  20. utils.myPrint( "1 ~ EnterScene: teamId = ", teamId);
  21. userDao.getPlayerAllInfo(playerId, function(err, player) { //读取用户所有信息
  22. if (err || !player) {
  23. logger.error( 'Get user for userDao failed! ' + err.stack);
  24. next( new Error( 'fail to get user from dao'), {
  25. route: msg.route,
  26. code: consts.MESSAGE.ERR
  27. });
  28. return;
  29. }
  30. player.serverId = session.frontendId;
  31. player.teamId = teamId;
  32. player.isCaptain = isCaptain;
  33. player.isInTeamInstance = isInTeamInstance;
  34. player.instanceId = instanceId;
  35. areaId = player.areaId;
  36. utils.myPrint( "2 ~ GetPlayerAllInfo: player.instanceId = ", player.instanceId);
  37. pomelo.app.rpc.chat.chatRemote.add(session, session.uid,
  38. player.name, channelUtil.getAreaChannelName(areaId), null);
  39. var map = area.map; //加入到 该地图的频道
  40. //Reset the player's position if current pos is unreachable
  41. if(!map.isReachable(player.x, player.y)){
  42. var pos = map.getBornPoint(); //玩家的出生位置
  43. player.x = pos.x;
  44. player.y = pos.y;
  45. }
  46. var data = {
  47. entities: area.getAreaInfo({x: player.x, y: player.y}, player.range),
  48. curPlayer: player.getInfo(),
  49. map: {
  50. name : map.name,
  51. width: map.width,
  52. height: map.height,
  53. tileW : map.tileW,
  54. tileH : map.tileH,
  55. weightMap: map.collisions
  56. }
  57. };
  58. // utils.myPrint("1.5 ~ GetPlayerAllInfo data = ", JSON.stringify(data));
  59. next( null, data); //发送data到客户端
  60. utils.myPrint( "2 ~ GetPlayerAllInfo player.teamId = ", player.teamId);
  61. utils.myPrint( "2 ~ GetPlayerAllInfo player.isCaptain = ", player.isCaptain);
  62. if (!area.addEntity(player)) { 将玩家的最新信息添加到area
  63. logger.error( "Add player to area faild! areaId : " + player.areaId);
  64. next( new Error( 'fail to add user into area'), {
  65. route: msg.route,
  66. code: consts.MESSAGE.ERR
  67. });
  68. return;
  69. }
  70. if (player.teamId > consts.TEAM.TEAM_ID_NONE) {
  71. // send player's new info to the manager server(team manager)
  72. var memberInfo = player.toJSON4TeamMember();
  73. memberInfo.backendServerId = pomelo.app.getServerId();
  74. pomelo.app.rpc.manager.teamRemote.updateMemberInfo(session, memberInfo, //更新队伍信息
  75. function(err, ret) {
  76. });
  77. }
  78. });
  79. };

14.客户端收到服务器的信息后,执行app.init

web-server/public/js/app.js


 
 
  1. /**
  2. * Init client ara
  3. * @param data {Object} The data for init area
  4. */
  5. function init( data) {
  6. var map = data.map;
  7. pomelo.player = data.curPlayer;
  8. switchManager.selectView( 'gamePanel');
  9. if(inited){
  10. configData( data);
  11. area = new Area( data, map);
  12. } else{
  13. initColorBox();
  14. configData( data);
  15. area = new Area( data, map);
  16. area.run();
  17. chat.init();
  18. inited = true;
  19. }
  20. ui.init();
  21. }

数据持久化模块

Lord采用Pomelo-sync从内存同步数据到数据库,该模块的作用是创建一个sql行为处理队列,每隔一段时间轮询一次,执行队列里的sql 操作。

API文档

添加实体对象更新 game-server/app/domain/area.js


 
 
  1. Instance.prototype.addEntity = function(e) {
  2. ...
  3. eventManager.addEvent(e);
  4. ...
  5. }

game-server/app/domain/event/eventManager.js


 
 
  1. /**
  2. * Listen event for entity
  3. */
  4. exp.addEvent = function(entity){
  5. ...
  6. addSaveEvent(entity);
  7. ...
  8. };
  9. /**
  10. * Add save event for player
  11. * @param {Object} player The player to add save event for.
  12. */
  13. function addSaveEvent(player) { //通过同步工具,回写相关信息到数据库
  14. var app = pomelo.app;
  15. player.on( 'save', function() {
  16. app.get( 'sync').exec( 'playerSync.updatePlayer', player.id, player.strip());
  17. });
  18. player.bag.on( 'save', function() {
  19. app.get( 'sync').exec( 'bagSync.updateBag', player.bag.id, player.bag);
  20. });
  21. player.equipments.on( 'save', function() {
  22. app.get( 'sync').exec( 'equipmentsSync.updateEquipments', player.equipments.id, player.equipments);
  23. });
  24. }

Pomelo-sync的模块提供了exec方法,当函数收到save事件后,执行exec,将操作行为放到数据库队列里面,每隔一段时间执行。

如何发送save事件呢?

game-server/app/domain/persistent.js


 
 
  1. /**
  2. * Persistent object, it is saved in database
  3. *
  4. * @param {Object} opts
  5. * @api public
  6. */
  7. var Persistent = function(opts) {
  8. this.id = opts.id;
  9. this.type = opts.type;
  10. EventEmitter.call( this);
  11. };
  12. util.inherits(Persistent, EventEmitter);
  13. module.exports = Persistent;
  14. // Emit the event 'save'
  15. Persistent.prototype.save = function() {
  16. this.emit( 'save');
  17. };

这个是可持久化对象的基类,所有的子类都可以调用基类的方法,如equipments装备,executeTask任务,fightskill,通过执行基类的方法,向EventEmitter发送事件,监听的事件得到相应后,写入同步数据库缓冲队列,每隔一段时间回写到服务器。

场景管理模块

简介

Lordofpomelo中每个场景对应一个独立的场景服务器,所有的业务逻辑都在场景服务器内部进行。

主流程

初始化场景管理模块game-server/app/domain/area/area.js


 
 
  1. /**
  2. * Init areas
  3. * @param {Object} opts
  4. * @api public
  5. */
  6. var Instance = function(opts){
  7. this.areaId = opts.id;
  8. this.type = opts.type;
  9. this.map = opts.map;
  10. //The map from player to entity
  11. this.players = {}; //玩家
  12. this.users = {};
  13. this.entities = {}; //实体
  14. this.zones = {}; //地区
  15. this.items = {}; //物品
  16. this.channel = null;
  17. this.playerNum = 0;
  18. this.emptyTime = Date.now();
  19. //Init AOI
  20. this.aoi = aoiManager.getService(opts);
  21. this.aiManager = ai.createManager({area: this}); //怪物ai 工厂方法
  22. this.patrolManager = patrol.createManager({area: this}); //patrol 巡逻工厂方法
  23. this.actionManager = new ActionManager(); //action 动作工厂方法
  24. this.timer = new Timer({
  25. area : this,
  26. interval : 100
  27. });
  28. this.start();
  29. };

启动场景管理服务 game-server/app/domain/area/area.js


 
 
  1. /**
  2. * @api public
  3. */
  4. Instance.prototype.start = function() {
  5. aoiEventManager.addEvent( this, this.aoi.aoi); //aoi监听事件
  6. //Init mob zones
  7. this.initMobZones( this.map.getMobZones()); //初始化怪物空间
  8. this.initNPCs( this); //初始化NPC
  9. this.aiManager.start(); //AI管理服务启动
  10. this.timer.run(); //地图计时器,定时执行地图内的处理信息任务
  11. };
  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


 
 
  1. var Timer = function(opts){
  2. this.area = opts.area;
  3. this.interval = opts.interval|| 100;
  4. };
  5. Timer.prototype.run = function () {
  6. this.interval = setInterval( this.tick.bind( this), this.interval); //定时执行 tick
  7. };
  8. Timer.prototype.tick = function() {
  9. var area = this.area;
  10. //Update mob zones
  11. for( var key in area.zones){
  12. area.zones[key].update(); //遍历 所有zones的更新
  13. }
  14. //Update all the items
  15. for( var id in area.items) { //检查人物状态值
  16. var item = area.entities[id];
  17. item.update();
  18. if(item.died) { //如果角色死亡,向客户端发送消息
  19. area.channel.pushMessage( 'onRemoveEntities', { entities: [id]});
  20. area.removeEntity(id);
  21. }
  22. }
  23. //run all the action
  24. area.actionManager.update(); //动作更新
  25. area.aiManager.update(); //ai 更新,检查ai反应动作
  26. area.patrolManager.update(); //patrol巡逻动作更新
  27. };
  • 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 初始化动作队列


 
 
  1. /**
  2. * Action Manager, which is used to contrll all action
  3. */
  4. var ActionManager = function(opts){
  5. opts = opts||{};
  6. this.limit = opts.limit|| 10000;
  7. //The map used to abort or cancel action, it's a two level map, first leven key is type, second leven is id
  8. this.actionMap = {};
  9. //The action queue, default size is 10000, all action in the action queue will excute in the FIFO order
  10. this.actionQueue = new Queue( this.limit);
  11. };

添加动作到动作队列


 
 
  1. /**
  2. * Add action
  3. * @param {Object} action The action to add, the order will be preserved
  4. */
  5. ActionManager.prototype.addAction = function(action){
  6. if(action.singleton) {
  7. this.abortAction(action.type, action.id);
  8. }
  9. this.actionMap[action.type] = this.actionMap[action.type]||{};
  10. this.actionMap[action.type][action.id] = action;
  11. return this.actionQueue.push(action);
  12. };

遍历动作数组里的所有执行动作,并执行该动作的update 方法


 
 
  1. /**
  2. * Update all action
  3. * @api public
  4. */
  5. ActionManager.prototype.update = function(){
  6. var length = this.actionQueue.length;
  7. for( var i = 0; i < length; i++){
  8. var action = this.actionQueue.pop();
  9. if(action.aborted){
  10. continue;
  11. }
  12. action.update();
  13. if(!action.finished){
  14. this.actionQueue.push(action);
  15. } else{
  16. delete this.actionMap[action.type][action.id];
  17. }
  18. }
  19. };

Example:当客户端发送一个玩家移动行为的时候,服务器将创建一个Move对象


 
 
  1. var action = new Move({
  2. entity: player,
  3. path: path,
  4. speed: speed
  5. });

当执行area.actionManager.update()时,将执行动作队列里的Move.update的方法;

game-server/app/domain/action/move.js


 
 
  1. /**
  2. * Update the move action, it will calculate and set the entity's new position, and update AOI module
  3. */
  4. Move.prototype.update = function(){
  5. this.tickNumber++;
  6. var time = Date.now()- this.time;
  7. ....
  8. };

总得来说,为了避免太多的动作行为,导致服务器多次响应,所以采用一个队列,隔一段短时间,处理一次。

AI管理模块

/game-server/app/ai/service/aiManager.js

为角色添加AI行为和行为准则


 
 
  1. /**
  2. * Add a character into ai manager.
  3. * Add a brain to the character if the type is mob.
  4. * Start the tick if it has not started yet.
  5. */
  6. pro.addCharacters = function(cs) {
  7. ...
  8. brain = this.brainService.getBrain( 'player', Blackboard.create({
  9. manager: this,
  10. area: this.area,
  11. curCharacter: c
  12. }));
  13. this.players[c.entityId] = brain;
  14. }
  15. ....
  16. };

读取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

持续攻击行为


 
 
  1. var loopAttack = new Loop({
  2. blackboard: blackboard,
  3. child: attack,
  4. loopCond: checkTarget
  5. });

行为树的Loop循环节点,循环判断checkTarget检查对象是否存在,如果存在,则一直攻击

如果有目标,则开始执行持续攻击


 
 
  1. var attackIfHaveTarget = new If({
  2. blackboard: blackboard,
  3. cond: haveTarget,
  4. action: loopAttack
  5. });

使用了行为树中的条件节点,当haveTarget的作用是检查角色里面target有没锁定对象符合条件,则展开loopAttack持续攻击


 
 
  1. //find nearby target action
  2. //var findTarget = new FindNearbyPlayer({blackboard: blackboard});
  3. //patrol action
  4. var patrol = new Patrol({blackboard: blackboard});
  5. //composite them together
  6. this.action = new Select({
  7. blackboard: blackboard
  8. });
  9. this.action.addChild(attackIfHaveTarget);
  10. //this.action.addChild(findTarget);
  11. this.action.addChild(patrol);

怪物的行为策略为,Select 顺序节点,优先选择攻击附近对象,其次是巡逻,通过行为树的组合,组合成了AI。

遍历所有怪物 game-server/app/service/aiManager.js


 
 
  1. /**
  2. * Update all the managed characters.
  3. * Stop the tick if there is no ai mobs.
  4. */
  5. pro.update = function() {
  6. if(! this.started || this.closed) {
  7. return;
  8. }
  9. var id;
  10. for(id in this.players) {
  11. if(typeof this.players[id].update === 'function') {
  12. this.players[id].update();
  13. }
  14. }
  15. for(id in this.mobs) {
  16. if(typeof this.mobs[id].update === 'function') {
  17. this.mobs[id].update();
  18. }
  19. }
  20. };

遍历this.mobs的怪物对象,执行对象的update方法,执行doAction


 
 
  1. pro.update = function() {
  2. return this.action.doAction();
  3. };

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服务的对象,产生变化的时候,会激活回调事件


 
 
  1. aoiEventManager.addEvent( this, this.aoi.aoi); //aoi监听事件
  2. //Add event for aoi
  3. exp.addEvent = function(area, aoi){
  4. aoi. on( 'add', function( params){
  5. params.area = area;
  6. switch( params.type){
  7. case EntityType.PLAYER:
  8. onPlayerAdd( params);
  9. break;
  10. case EntityType.MOB:
  11. onMobAdd( params);
  12. break;
  13. }
  14. });
  15. aoi. on( 'remove', function( params){
  16. params.area = area;
  17. switch( params.type){
  18. case EntityType.PLAYER:
  19. onPlayerRemove( params);
  20. break;
  21. case EntityType.MOB:
  22. break;
  23. }
  24. });
  25. aoi. on( 'update', function( params){
  26. params.area = area;
  27. switch( params.type){
  28. case EntityType.PLAYER:
  29. onObjectUpdate( params);
  30. break;
  31. case EntityType.MOB:
  32. onObjectUpdate( params);
  33. break;
  34. }
  35. });
  36. aoi. on( 'updateWatcher', function( params) {
  37. params.area = area;
  38. switch( params.type) {
  39. case EntityType.PLAYER:
  40. onPlayerUpdate( params);
  41. break;
  42. }
  43. });
  44. };

根据AOI不同的事件回调,向客户端发出不同的回调事件。如添加实物,附近玩家等信息。

点击实物模块

用户流程

点击实物->怪物->攻击行为

点击实物->NPC->聊天

点击实物->玩家->组队或战斗

程序流程

1.客户端绑定鼠标点击实体事件

web-server/public/js/componentAdder.js


 
 
  1. /**
  2. * Mouse click handlerFunction
  3. */
  4. var launchAi = function (event, node) {
  5. var id = node.id;
  6. if (event.type === 'mouseClicked') {
  7. clientManager.launchAi({ id: id});
  8. }
  9. };

绑定鼠标点击实体事件到launchAi函数

2.检查鼠标点击实物的事件,属于哪种类型

web-server/js/ui/clientManager.js


 
 
  1. function launchAi(args) {
  2. var areaId = pomelo.areaId;
  3. var playerId = pomelo.playerId;
  4. var targetId = args.id;
  5. if (pomelo.player.entityId === targetId) {
  6. return;
  7. }
  8. var skillId = pomelo.player.curSkill;
  9. var area = app.getCurArea();
  10. var entity = area.getEntity(targetId);
  11. if (entity. type === EntityType.PLAYER || entity. type === EntityType.MOB) { //被攻击的对象类型判断
  12. if (entity.died) {
  13. return;
  14. }
  15. if (entity. type === EntityType.PLAYER) { //如果是玩家,弹出选项,组队或者交易等
  16. var curPlayer = app.getCurPlayer();
  17. pomelo.emit( 'onPlayerDialog', {targetId: targetId, targetPlayerId: entity.id,
  18. targetTeamId: entity.teamId, targetIsCaptain: entity.isCaptain,
  19. myTeamId: curPlayer.teamId, myIsCaptain: curPlayer.isCaptain});
  20. } else if (entity. type === EntityType.MOB) {
  21. pomelo.notify( 'area.fightHandler.attack',{targetId: targetId}); //通知服务器处理攻击事件,不要求回调
  22. }
  23. } else if (entity. type === EntityType.NPC) { //如果是NPC是对话模式
  24. pomelo.notify( 'area.playerHandler.npcTalk',{areaId :areaId, playerId: playerId, targetId: targetId});
  25. } else if (entity. type === EntityType.ITEM || entity. type === EntityType.EQUIPMENT) { //检查一下捡东西相关的
  26. var curPlayer = app.getCurPlayer();
  27. var bag = curPlayer.bag;
  28. if (bag.isFull()) {
  29. curPlayer.getSprite().hintOfBag();
  30. return;
  31. }
  32. pomelo.notify( 'area.playerHandler.pickItem',{areaId :areaId, playerId: playerId, targetId: targetId}); //捡东西
  33. }
  34. }

3.不同类型的点击行为对应不同的服务器响应函数

  • 怪物: area.fightHandler.attack
  • NPC: area.playerHandler.npcTalk
  • 捡东西: area.playerHandler.pickItem

点击玩家后出现对话框 对话框选项,可以根据需求


 
 
  1. /**
  2. * Execute player action
  3. */
  4. function exec(type, params) {
  5. switch (type) {
  6. case btns.ATTACK_PLAYER: {
  7. attackPlayer( params); //攻击玩家
  8. }
  9. break;
  10. case btns.APPLY_JOIN_TEAM: {
  11. applyJoinTeam( params); //加入队伍
  12. }
  13. break;
  14. case btns.INVITE_JOIN_TEAM: {
  15. inviteJoinTeam( params); //邀请加入队伍
  16. }
  17. break;
  18. }
  19. }
  • 攻击玩家: area.fightHandler.attack
  • 加入队伍: area.teamHandler.applyJoinTeam
  • 邀请队伍: area.teamHandler.inviteJoinTeam

4.执行服务端程序

area.fightHandler.attack


 
 
  1. /**
  2. * Action of attack.
  3. * Handle the request from client, and response result to client
  4. * if error, the code is consts.MESSAGE.ERR. Or the code is consts.MESSAGE.RES
  5. *
  6. * @param {Object} msg
  7. * @param {Object} session
  8. * @api public
  9. */
  10. handler.attack = function(msg, session, next) {
  11. var player = session.area.getPlayer(session.get( 'playerId'));
  12. var target = session.area.getEntity(msg.targetId);
  13. if(!target || !player || (player.target === target.entityId) || (player.entityId === target.entityId) || target.died){
  14. next();
  15. return;
  16. } //数据校验
  17. session.area.timer.abortAction( 'move', player.entityId); //停止移动
  18. player.target = target.entityId; //锁定攻击目标
  19. next();
  20. };

area.playerHandler.npcTalk


 
 
  1. handler.npcTalk = function(msg, session, next) {
  2. var player = session.area.getPlayer(session.get( 'playerId'));
  3. player.target = msg.targetId;
  4. next();
  5. };

area.playerHandler.pickItem


 
 
  1. /**
  2. * Player pick up item.
  3. * Handle the request from client, and set player's target
  4. *
  5. * @param {Object} msg
  6. * @param {Object} session
  7. * @param {Function} next
  8. * @api public
  9. */
  10. handler.pickItem = function(msg, session, next) {
  11. var area = session.area;
  12. var player = area.getPlayer(session.get( 'playerId'));
  13. var target = area.getEntity(msg.targetId);
  14. if(!player || !target || (target.type !== consts.EntityType.ITEM && target.type !== consts.EntityType.EQUIPMENT)){
  15. next( null, {
  16. route: msg.route,
  17. code: consts.MESSAGE.ERR
  18. });
  19. return;
  20. }
  21. player.target = target.entityId;
  22. next();
  23. };

上述三个处理函数都有一个共同点,只设置了player.target就返回给客户端了,这当中包含了什么玄机?请回忆起场景处理模块。

图

1.客户端发送请求到服务器

2.服务器修改play.target 为taget添加对象

3.场景通过tick对area.aiManager.update()进行更新,根据ai行为树,检测到target存在对象,判断对象是否能被攻击,是否能谈话,是否能捡起来,分配到不同的处理函数,处理函数执行完毕后,服务器端通过pushMessage等方式,向客户端发出广播信息,客户端通过pomelo.on(event,func)的方式监测到对应的事件后,执行对应的回调事情。

未完,待续。

我的git地址:https://github.com/youyudehexie/lordofpomelo/wiki

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值