Lordofpomelo遊戲分析

遊戲體驗

在線地址

部署遊戲

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)的方式監測到對應的事件後,執行對應的回調事情。

未完,待續。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章