iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 25
1

到目前為止我們已經能讓遊戲的操作跟伺服器連結起來,可以透過伺服器去管理一些行為的操作。不過要讓其他玩家能夠出現在地圖上,我們就必須調整現階段的程式碼讓伺服器能夠管理「線上玩家」的資訊。

連線池

WebSocket 的連線是由我們使用的套件所管理,但是我們需要一個已經抽象成方便我們處理的 Connection 物件列表,因此我們要來實作「連線池」這個物件來幫助我們管理。

# app/network/connection_pool.rb

# frozen_string_literal: true

class ConnectionPool
  include Enumerable

  def initialize
    @mutex = Mutex.new
    @connections = []
  end

  def each(&block)
    @connections.each(&block)
  end

  def add(conn)
    @mutex.synchronize do
      @connections.push(conn)
    end
  end

  def remove(conn)
    @mutex.synchronize do
      @connections.delete(conn)
    end
  end
end

這是一個很簡單的物件封裝,實際上只是利用一個陣列將物件儲存起來。不過因為我們無法確定使用者會不會在某個時間點同時加入,因此為了避免這樣的情況我們利用 Mutex 的機制確保一次只會有一個人可以同時操作我們的陣列。

因為我們使用的 Puma 是多執行緒的所以使用者是有很大的機率同時連上並且開始登記,因此需要將操作進行一個上鎖的動作確保一次只能有一位玩家登記到連線列表中。

這個物件其實也能設計成 Singleton 的形式,不過在處理資料庫的連線或者其他地方都是可以使用的為了保有未來的擴充性就先以普通物件的方式設計。

接下來要稍微改動 Connection 透過靜態方法的方式讓他增加一個連線池,並且將玩家登記進去。

# app/network/connection.rb

class Connection
  class << self
    def pool
      @pool ||= ConnectionPool.new
    end
  end

  def open(_event)
    Connection.pool.add(self)
  end

  def close(_event)
    Connection.pool.remove(self)
  end
  
  # ...
end

在這邊我們對連線增加了 #open#close 兩個行為來對連線池增加或者刪除連線,如此一來只要調整 Protocol::WebSocket 針對開啟跟關閉連線時間呼叫,就能夠自動做到調整連線池跟線上玩家一致的狀態。

這邊其實有一個需要評估的地方是 @pool || ConnectionPool.new 這個處理是否恰當,在 Ruby 中 Singleton 物件是會自動使用 Mutex 確保不會重複性的初始化,那麼在這裡的 @pool 是否也需要加入相關的設計來避免重複初始化呢?

接下來再對 app/network/protocol/websocket.rb 做調整,將 open 和 close 的事件註冊給 @conn 物件。

# app/network/protocol/websocket.rb
module Protocol
  class WebSocket
   # ...
   
   def setup
      raise InvalidRequest unless websocket?

      @ws = Faye::WebSocket.new(@env)
      @ws.on :open, @conn.method(:open)
      @ws.on :message, method(:receive)
      @ws.on :close, @conn.method(:close)
    end
  end
end

廣播

在我們知道全部上線玩家的列表之後,就能夠實作廣播這個動作。依照不同類型的遊戲會需要通知的玩家數量不太一樣,以我們這次目標要製作的多玩家在地圖上互動的類型來看,大多數操作都是需要「廣播」給其他玩家知道的。

# app/network/connection.rb

class Connection
  # ...
  
  def broadcast(data)
    Connection.pool.each do |conn|
      conn.write(data)
    end
  end
end

實作上也相對的簡單,只需要將連線列表抓取出來後對每一個連線都進行發送資料的指令即可。

這邊的 ConnectionPool 透過 #each 方法實作了迭代器,並利用 Enumerable 模組將迭代器相關的行為都實作到裡面,因此可以向陣列一樣使用 #take #map 等方法。

我的個人部落格是弦而時習之平常會把自己發現的一些新技巧紀錄在上面,也歡迎大家來逛逛。


上一篇
Day 24 - 實作練習 - 架構客戶端
下一篇
Day 26 - 實作練習 - 加入地圖
系列文
從讀遊戲原始碼學做連線遊戲33

尚未有邦友留言

立即登入留言