#Tutorial 2 -- Treasures ##描述?Treasures?游戲是從?LordOfPomelo?中抽取出來,去掉了大量的游戲邏輯,用以更好的展示?Pomelo?框架的用法以及運作機制。
Treasures 很簡單,輸入一個用戶名后,會隨機得到一個游戲角色,進入游戲場景。在游戲場景中地上會散落一些寶物,每個寶物都有分數,玩家操作游戲人物去撿起地上的寶物,然后就能得到相應的分數。
##安裝和運行 安裝?pomelo
npm install -g pomelo
獲取源碼
git clone https://github.com/NetEase/treasures.git
安裝?npm
?依賴包(先進入項目目錄)
sh npm-install.sh
啟動?web-server
?(先進入web-server
目錄)
node app.js
啟動?game-server
?(先進入game-server
目錄)
pomelo start
在瀏覽器中訪問?http://localhost:3001?進入游戲
Pomelo自帶的demo只Treasures運行時報錯解決方案:
Pomelo自帶demo之Treasures,下載源碼后進入web-server目錄,先輸入命令 npm install -d 安裝第三方模塊,之后運行會報錯,錯誤提示是:TypeError: mime.lookup is not a function,web-server\node_modules\connect\lib\middleware\static.js:144。原因是mime模塊從2.x版本開始把lookup方法改為了getType,而作者編寫時的mime模塊是1.x版本。如果把lookup改為getType,雖能解決眼前這個錯誤,但接下來還會有其它地方報錯,因為2.x版本不僅僅修改了這一個方法。更簡單的解決辦法是直接使用mime的1.x版本。方法有2種:
1、在web-server目錄下運行命令:npm install mime;
2、打開web-server目錄的package.json,在dependencies字段中增加一行:"mime": "^1",意思是安裝mime的1.x版本。之后運行命令:npm install -d;
第1種方法雖簡單,但如果把源碼clone到別處,問題會重復出現,所以推薦使用第2種方法,在package.json明確好版本,不管clone到哪,只要npm install -d就好了。
?
##架構 Treasures 分為 web-Server 和 game-Server 兩部分。
-
web-server
?是用 Express 建立的最一個基礎的 http 服務,用來支撐瀏覽器頁面的訪問。 -
game-server
?是 WebSocket 服務器,用來運行整個游戲的邏輯。
首先,通過配置文件,來看?game-server
?的具體架構?game-server/config/server.json
{"development": {"connector": [{"id": "connector-server-1", "host": "127.0.0.1", "port": 3150, "clientPort": 3010, "frontend": true},{"id": "connector-server-2", "host": "127.0.0.1", "port": 3151, "clientPort": 3011, "frontend": true}],"area": [{"id": "area-server-1", "host": "127.0.0.1", "port": 3250, "areaId": 1}],"gate": [{"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 3014, "frontend": true}]}
}
可以看出,服務端是由以下幾個部分構成:
- 2 個?
connector
?服務器,主要用于接受和發送消息。 - 1 個?
gate
?服務器,主要用于負載均衡,將來自客戶端的連接分散到兩個?connector
?服務器上。 - 1 個?
area
?服務器,主要用于驅動游戲場景,和游戲邏輯
##源碼分析 通過游戲流程來分析代碼。
1. 連接服務器 客戶端?web-server/public/js/main.js
?中?entry
?方法中
pomelo.request('gate.gateHandler.queryEntry', {uid: name}, function(data) {//...
});
服務端?game-server/app/servers/gate/handler/gateHandler.js
?中
Handler.prototype.queryEntry = function(msg, session, next) {// ...// 返回要連接的 connector 服務器的 host 和 portnext(null, {code: Code.OK, host: res.host, port: res.wsPort});
};
這樣客戶端就能連接到分配的?connector
?服務器上。
2. 進入游戲 在與?connector
?服務器建立連接之后,開始進入游戲
pomelo.request('connector.entryHandler.entry', {name: name}, function(data) {// ...
});
在客戶端第一次向?connector
?服務器發送請求時,服務器會將?session
?信息進行初始化和綁定
// session 與 playerId 綁定
session.bind(playerId);
// 設置玩家 areaId
session.set('areaId', 1);
進入游戲場景,客戶端向服務端發起進入場景請求:
pomelo.request("area.playerHandler.enterScene", {name: name, playerId: data.playerId}, function(data) {// ...
});
客戶端向服務端發送請求后,先到達?connector
?服務器,然后?connector
?服務器根據?game-server/app/util/routeUtil.js
?中轉發規則,將請求路由到相應的?area
?服務器(本例子中只有一個area
服務器),area
?服務器中的?playerHandler
?再處理相應的請求。這樣玩家就加入到游戲場景中了。
在一個玩家加入到游戲場景之后,其他玩家必須能即時的看到這個玩家的加入,所以服務端必須將消息廣播到在此游戲場景中的所有玩家。 建立?channel
,所有加入此游戲場景的玩家都會加入到這個?channel
?中
// 獲取 channel,如果沒有就創建一個
channel = pomelo.app.get('channelService').getChannel('area_' + id, true);
// 將玩家加入 channel
channel.add(e.id, e.serverId);
當?area
?中有玩家加入,或其他狀態發生改變時,這些信息都會被推送到在這個?channel
?中的每個玩家。比如有玩家加入時:
channel.pushMessage({route: 'addEntities', entities: added});
這些消息都是通過?connector
?服務器發送到客戶端。而?area
?中的消息是通過?session.frontendId
?來決定是由哪個?connector
?服務器發出去。
客戶端接受消息:
// 當有新玩家加入時,服務端會廣播消息給所有玩家。客戶端通過這個路由綁定,來獲取消息
pomelo.on('addEntities', function(data) {// ...
});
3. Area 服務器?area
?服務器是一個由?tick
?驅動的游戲場景。每個?tick
?都會對場景中的?entity
?的狀態進行更新,如果狀態有發生改變,這些改變會被推送到客戶端。
function tick() {//run all the actionarea.actionManager().update();// update entitiesarea.entityUpdate();// update rankarea.rankUpdate();
}
比如玩家發起一個?move
?動作:
客戶端
// 向服務端發送 move 請求通知
pomelo.notify('area.playerHandler.move', {targetPos: {x: entity.x, y: entity.y}, target: targetId});
服務端?playerHandler
?接受請求:
handler.move = function(msg, session, next) {// ...// 產生一個 move actionvar action = new Move({entity: player,endPos: endPos,});
});
然后這個?action
?會在每個?tick
?中更新。
###4. 客戶端發送和接受消息 客戶端和服務端的通訊有以下幾種方式:
- Request - Response 方式
// 向 connector 發送請求,參數 {name: name}
pomelo.request('connector.entryHandler.entry', {name: name}, function(data) {// 回調函數得到請求返回結果// do something
});
- Notify (向服務端發送通知)
// 向服務端發送 move 請求通知
pomelo.notify('area.playerHandler.move', {targetPos: {x: entity.x, y: entity.y}, target: targetId});
- Push (服務端主動發送消息到客戶端)
// 當有新玩家加入時,服務端會廣播消息給所有玩家。客戶端通過這個路由綁定,來獲取消息
pomelo.on('addEntities', function(data) {// ...
});
###5. 離開游戲 就是在玩家離開游戲時,connector
?服務器會先收到斷開的消息,這時,它需要在?area
?服務器中將用戶剔除,并廣播消息給其他在線玩家。 因為服務器之間的進程都是獨立的,所以這就涉及到一個 RPC 調用,好在 Pomelo 框架對 RPC 做了很好的封裝,做法如下:?area
?服務器想要提供一系列的 Remote 接口供其他服務器進程調用,只需要在?servers/area
?目錄下,創建一個?remote
?目錄,在這個目錄下的文件暴露出的接口,都可以作為 RPC 調用接口。 比如,玩家離開:
// connector 中對 session 綁定事件,當 session 關閉時,觸發事件
session.on('closed', onUserLeave.bind(null, self.app));var onUserLeave = function (app, session, reason) {if (session && session.uid) {// rpc 調用app.rpc.area.playerRemote.playerLeave(session, {playerId: session.get('playerId'), areaId: session.get('areaId')}, null);}
};
對應的?area/remote/playerRemote.js
?中?playerLeave
?方法
exports.playerLeave = function(args, cb) {// 發出通知area.getChannel().pushMessage({route: 'onUserLeave', code: consts.MESSAGE.RES, playerId: playerId});// ...
};
這樣就輕易的完成了一個跨進程的調用?
?