在我們將加入地圖的指令處理完畢後,我們就能夠收到來自其他玩家的「加入」資訊,也就能夠呈現其他玩家在地圖上並且做出反應。不過原本 RPG Maker 所支援的只有產生單機玩家的功能。因此我們需要擴充原本的 Player 物件,加入能夠任意產生和伺服器連動的修改版 Player 物件。
實際上開發時其實不太會像這系列文章從前後端來回切換開發,不過如果是在嘗試製作 Prototype 的話這樣的方式是很適合用來驗證想法是否可行。如果是正式開發的話,還是會建議將完整的指令、行為都規劃好之後,各別開發會比較適合。
打開 RPG Maker 專案中我們自己加入的的 js/plugins/SimpleRPG_Map.js
來擴充看起來不太適合,因為這是跟地圖相關的。因此我們需要再另外擴充一個 js/plugins/SimpleRPG_RemotePlayer.js
來實現這個功能。
// js/plugins/SimpleRPG_Player.js
//=============================================================================
// SimpleRPG_RemotePlayer.js
//=============================================================================
/*:
* @plugindesc RemotePlayer
* @author Aotokitsuruya
*/
var SimpleRPG = SimpleRPG || {};
(function() {
// ES5 的物件繼承(class RemotePlayer extends Player)
function Player() {
this.initialize.apply(this, arguments);
};
Player.prototype = Object.create(Game_Player.prototype);
Player.prototype.constructor = Player;
Player.prototype.initMembers = function() {
Game_Player.prototype.initMembers.call(this);
this._destinationX = null;
this._destinationY = null;
};
// 針對原有 Player 物件的修改
Player.prototype.isDestinationValid = function() {
return this._destinationX !== null;
};
// 擴充自訂的 moveTo 行為
Player.prototype.moveTo = function(x, y) {
this._destinationX = x;
this._destinationY = y;
};
// 將原本控制玩家的機制改寫成我們自己的版本
Player.prototype.moveByInput = function() {
if (!this.isMoving() && this.canMove()) {
var direction = 0;
if (this.isDestinationValid()){
var x = this._destinationX;
var y = this._destinationY;
direction = this.findDirectionTo(x, y);
}
if (direction > 0) {
this.executeMove(direction);
}
}
};
Player.prototype.update = function(sceneActive) {
var wasMoving = this.isMoving();
this.updateDashing();
if (sceneActive) {
this.moveByInput();
}
Game_Character.prototype.update.call(this);
if (!this.isMoving()) {
this.updateNonmoving(wasMoving);
}
};
SimpleRPG.RemotePlayer = Player;
})();
上面這段程式碼我們做了一些處理,首先是繼承 RPG Maker 原本定義的 Game_Player
物件進行擴充,不論是本地還是遠端的玩家都應該是一種玩家類型,擁有的行為勢必會是相同的。原本的 Game_Player
物件因為是針對單機玩家設計的,因此我們擴充了自訂的 moveTo
方法來讓我們可以設定移動的路徑,並且修改原本控制移動的機制讓這些角色能根據角色身上的目的地座標移動,而不是依靠原本的輸入系統。
除此之外我們還擴充了一個命名空間(Namespace)用在不同的 Plugins 中共享 SimpleRPG
相關的物件,如此一來才能將不同功能互相串聯起來。最後也別忘記要去啟用插件,我們才能實際上使用到這個機制。
在實現我們需要的機制時會產生非常多的檔案,這是 RPG Maker MV 在擴充性設計上的問題,因此如果選擇要基於 RPG Maker MV 來開發的話,會推薦使用 Webpacker 之類的工具輔助。
接下來我們要在地圖上生成玩家,因此我們要繼續修改 js/plugins/SimpleRPG_Map.js
將原本的地圖功能擴充出自訂的生成玩家機制。
// js/plugins/SimpleRPG_Map.js
//=============================================================================
// SimpleRPG_Map.js
//=============================================================================
/*:
* @plugindesc Map Handler
* @author Aotokitsuruya
*/
var SimpleRPG = SimpleRPG || {};
(function() {
// 連線處理
var currentMap = null;
var socket = new WebSocket('ws://localhost:9292');
socket.onmessage = function(event) {
var data = JSON.parse(event.data);
let player;
if (!currentMap) {
return;
}
switch (data['command']) {
case 'join':
player = currentMap.addPlayer.apply(currentMap, data['parameters'])
player.setImage('Actor2', 1);
player.locate(8, 6);
break;
case 'move':
let id = data['parameters'].shift();
player = currentMap.findPlayer(id)
if (player) {
player.moveTo.apply(player, data['parameters'])
}
break;
}
}
// 擴充原有地圖機制
var _Scene_Map_initialize = Scene_Map.prototype.initialize;
Scene_Map.prototype.initialize = function() {
_Scene_Map_initialize.call(this);
this._remotePlayers = {};
this._remotePlayerSprites = {};
};
// 地圖載入後的處理
var _Scene_Map_onMapLoaded = Scene_Map.prototype.onMapLoaded;
Scene_Map.prototype.onMapLoaded = function() {
_Scene_Map_onMapLoaded.call(this);
// 設定當下地圖跟登記玩家
currentMap = this;
socket.send(JSON.stringify({ command: 'join', parameters: [] }));
// 隱藏原本的玩家角色(因為不會使用)
$gamePlayer.setOpacity(0);
}
// 玩家相關處理
Scene_Map.prototype.remotePlayers = function() {
return Object.values(this._remotePlayers);
};
Scene_Map.prototype.findPlayer = function(id) {
return this._remotePlayers[id];
};
Scene_Map.prototype.addPlayer = function(id) {
if (this._remotePlayers[id]) {
return this._remotePlayers[id];
}
this._remotePlayers[id] = new SimpleRPG.RemotePlayer();
this._remotePlayerSprites[id] = new Sprite_Character(this._remotePlayers[id]);
this._spriteset._tilemap.addChild(this._remotePlayerSprites[id]);
return this._remotePlayers[id];
};
// 更新遠端玩家
var _Scene_Map_updateMain = Scene_Map.prototype.updateMain;
Scene_Map.prototype.updateMain = function() {
_Scene_Map_updateMain.call(this);
this.updateRemotePlayers(this.isActive());
};
Scene_Map.prototype.updateRemotePlayers = function(active) {
this.remotePlayers().forEach(function(player) {
player.update(active);
});
};
// 處理玩家操作
Scene_Map.prototype.processMapTouch = function() {
if (TouchInput.isTriggered() || this._touchCount > 0) {
if (TouchInput.isPressed()) {
if (this._touchCount === 0 || this._touchCount >= 15) {
var x = $gameMap.canvasToMapX(TouchInput.x);
var y = $gameMap.canvasToMapY(TouchInput.y);
// $gameTemp.setDestination(x, y);
socket.send(JSON.stringify({ command: 'move', parameters: [x, y] }));
}
this._touchCount++;
} else {
this._touchCount = 0;
}
}
};
}());
基本上除了最後段落的玩家輸入處理,我們增加了不少東西。首先是原本的 WebSocket 連線部分增加了針對指令操作的檢查,根據伺服器不同的行為作出不同的處理。剩下的則是將原本的 Scene_Map
物件擴充,增加了玩家以及玩家外觀的資訊,並且可以透過 Key-Value 對應的方式找到對應的玩家並且進行設定座標或者外觀。
當我們實際打開遊戲測試時,會發現新加入的玩家是看不到前面加入的玩家。而且當前面的玩家移動時,也不會有任何反應,因此我們還需要再修改伺服器將玩家初次加入時的指令增加一個「玩家列表」將當下這張地圖存在的玩家顯示出來。
不過,在這之前我們在地圖上做了一些額外的動作,也就是 WebSocket 的處理。以前面我們從 Unlight 客戶端的了解,這類連線應該是由專門的物件管理,並且對應到 Controller 來觸發相應的動作,雖然我們在伺服器有實作做這些行為,但是客戶端其實還沒有完善這方面的設計。
因此下一篇會先進行重構,透過這樣的方式我們在之後也才能更好的擴充遊戲與增加相對應的機制。
我的個人部落格是弦而時習之平常會把自己發現的一些新技巧紀錄在上面,也歡迎大家來逛逛。