iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 26
0

當我們已經能夠管理所有線上的玩家後,需要再進一步做的處理就是將其他玩家都顯示出來。因此我們要先增一個叫做 join 的指令表示有一個玩家進入了地圖。

Player Model

為了要能夠實現加入指令,我們需要多處理好幾個項目,第一個就是我們要在伺服器上對每個玩家作識別。最簡單的方式就是建立一個 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 失效,已經補上在該篇文章中。

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


上一篇
Day - 25. 實作練習 - 連線池
下一篇
Day. 27 - 實作練習 - 顯示其他玩家
系列文
從讀遊戲原始碼學做連線遊戲33

尚未有邦友留言

立即登入留言