當我們已經能夠管理所有線上的玩家後,需要再進一步做的處理就是將其他玩家都顯示出來。因此我們要先增一個叫做 join
的指令表示有一個玩家進入了地圖。
為了要能夠實現加入指令,我們需要多處理好幾個項目,第一個就是我們要在伺服器上對每個玩家作識別。最簡單的方式就是建立一個 Model(資料物件)來對這些玩家建檔。
# app/models/player.rb
# frozen_string_literal: true
class Player
attr_accessor :id
def initialize(attributes = {})
attributes.each do |name, value|
instance_variable_set(:"@#{name}", value)
end
end
end
我們可以簡單定義一個只有紀錄玩家編號的 Model 來做紀錄,就現階段而言這個 Model 已經是非常足夠的。
為了方便使用,我們讓物件可以透過
Player.new(id: 1)
的方式建立。
基本上每個玩家在連線建立的階段後就能被識別出來,因此我們需要去修改 app/network/connection.rb
將玩家記錄在上面。
在前面閱讀 Unlight 原始碼有提到,玩家會透過 SRP 計算出來的 Session Key 去做
negotiate
這個動作,其實就是在確認玩家是否存在以及是否需要被登記到伺服器上。
# app/network/connection.rb
# frozen_string_literal: true
require 'json'
class Connection
class << self
# ...
def players
pool.map(&:player)
end
end
attr_reader :player
# ...
def open(_event)
next_id = Connection.players.max_by(&:id)&.id.to_i + 1
@player = Player.new(id: next_id)
Connection.pool.add(self)
end
end
我們在這邊做了一些簡單的處理,第一個是將原本的 Connection.pool
擴充出一個 Connection.players
方便我們在需要的情況下針對玩家抓取。而物件本身新增了 Read-only 的 player
屬性用來尋找玩家。
在原本開啟連線的地方則是在加入連線池之前,先透過玩家列表找出最大的 ID 再對他 +1 當作新玩家的編號,這樣至少能在一定程度上避免跟其他玩家重複。
當完成這些動作之後,我們需要在玩家成功加入的時候對所有人廣播「加入成功」來進行通知,因此需要到 app/controllers/map_controller.rb
來定義廣播的動作。
# app/controllers/map_controller.rb
class MapController
def initialize(conn)
@conn = conn
end
def join
@conn.broadcast({
command: 'join',
parameters: [@conn.player.id]
})
end
def move(x, y)
@conn.write({ command: 'move', parameters: [x, y]})
end
end
目前的 Controller 看起來雖然能夠正常運作,不過對未來擴充新的 Controller 或者增加新的指令都有點複雜,因此我們先將一些共通的行為封裝到 BaseController
用繼承的方式來拓展。
# app/controllers/base_controller.rb
# frozen_string_literal: true
class BaseController
def initialize(conn)
@conn = conn
end
def response(command, *parameters)
@conn.write(
command: command,
parameters: parameters
)
end
def broadcast(command, *parameters)
@conn.broadcast(
command: command,
parameters: parameters
)
end
def current_player
@conn.player
end
end
基本上 Controller 的初始化只會固定接收 conn
參數,因此可以在 BasController 一起定義,而 #broadcast
和 #response
是最常見的兩個行為,因此也將這兩個行為封裝成比較方便呼叫的形式放到 BasController,最後則是玩家在遊戲中是最常互動的物件,因此也將他封裝成 #current_player
方便取用。
如此一來,我們的 MapController 就能用非常簡潔的方式實現。
# frozen_string_literal: true
class MapController < BaseController
def join
broadcast(:join, current_player.id)
end
def move(x, y)
broadcast(:move, current_player.id, x, y)
end
end
在做重構的同時也把 current_player.id
插入到移動指令裡面,並且改成使用廣播的方式。如此一來只要有一個玩家移動,就會廣播「我是玩家 1 我移動到 (x, y) 座標」這樣的訊息出去,那麼就能讓所有在線上的玩家都看到這個變化。
在 Day 21 的 Auto-Reloading 章節缺少了
Thread.new { @fsevent.run }
的行為讓 Auto-Reloading 失效,已經補上在該篇文章中。
我的個人部落格是弦而時習之平常會把自己發現的一些新技巧紀錄在上面,也歡迎大家來逛逛。