iT邦幫忙

2021 iThome 鐵人賽

DAY 6
0
Modern Web

連線網頁卡牌遊戲(Elixir, Phoenix, Liveview)系列 第 6

6 用 GenServer 做 server?

GenServer 跟我們遊戲有什麼關西?

我來試試看直接套用我們的場景來解釋 GenServer 怎麼使用好了,
如果想要讀的話可以看 elixir 文件裡的 GenServer 或是 elixir school 的 OTP 章節

在我們伺服器上面,每一局新遊戲會是一個獨立的 process ,遊戲的狀態暫存與更新都在這裡進行,
另外我們也需要寫一系列的方法來操作每一個遊戲 process,
在這邊暫時稱 process 內的行為為 Server 端,
用來操作 process 的外層方法為 Client 。

https://ithelp.ithome.com.tw/upload/images/20210920/20141054OVYD9TIlnK.png

這裡的 pid 是指 process identifier ,每次建立一個新的遊戲 process 都會回傳該 process 的 pid,
我們便可以把它存起來,有了 pid 就可以對他下命令,
像是圖裡面的出牌,便是使用新增 process 回傳的 pid 來對他下出牌要求

另外這邊要提的是,上圖的出牌是用 cast ,Client 發出要求之後就不會管了,繼續做別的事,
後面會盡量使用這個非同步的方式。
那查看遊戲狀態則是用 call, Client 在發出要求後會等待 Server 端回覆。

實作建立新遊戲

我們先建立 Server 端的好了,寫在上次建立的 game.ex 裡面

defmodule Game do
  defstruct rounds: [], host_hand: [], guest_hand: [], round: 1, winner: nil

  use GenServer

  def init(status) do
    {:ok, status}
  end

  def handle_call(:get_status, _from, status) do
    {:reply, status, status}
  end
end

要在 module 使用 GenServer 就要 use GenServer (好像廢話
用了之後他會預期我們在 module 裡面定義一系列的方法,在收到來自 Client 端的請求時,
會依據請求執行相對應的方法,例如建立新的 process 就會用到 init 方法,
process 暫存初始狀態後回傳 pid 回去,
我們趕緊來 iex 試試

$ iex game.ex

iex(1)> GenServer.start_link(Game, %Game{})
{:ok, #PID<0.115.0>}

成功了,我們使用 start_link/2 方法開啟了在 Game module 的 game process,
並給他我們昨天設定好的 %Game{} struct

那我們再來來看看剛剛寫來查看遊戲狀態的 handle_call

  def handle_call(:get_status, _from, status) do
    {:reply, status, status}
  end

handle_call/3 收三個變數 分別是事件名稱、要求來源 還有目前在 process 上的 遊戲狀態
而在方法裡面的要回傳的格式是 {:reply, 要回傳的東西, 更新後的狀態}
因為這次只需要回傳狀態,不需要更新狀態,所以第二三個是一樣的。

我們來 iex 使用看看
剛剛 start_link 的時候忘記存 pid 我們在建立一次

iex(2)> {:ok, game_pid} = GenServer.start_link(Game, %Game{})
{:ok, #PID<0.117.0>}
iex(3)> GenServer.call(game_pid, :get_status)
%Game{guest_hand: [], host_hand: [], round: 1, rounds: [], winner: nil}

成功了,我們得到了目前的遊戲狀態
當然每次都要呼叫 GenServer.call(game_pid, :get_status) 有點冗長,
我們明天來幫他包裝一下成為 Client 端的方法

是我的錯,應該先講的 elixir 語法們

補充 pattern matching

在 elixir 裡面 = 是 pattern matching
他會盡量把右邊的東西比對到左邊
例如 [name, age] = ["John", 18]
這樣 name 的值就會是 "John"

如果雙方的架構不合 如 [name, age, gender] = ["John", 18]
或是左邊有固定的衝突值 如 [name, 19] = ["John", 18]
都會錯誤

這次的用法是 {:ok, game_pid} = {:ok, #PID<0.117.0>}
所以 game_pid 就會是 #PID<0.117.0>

定義方法

elixir 定義方法是使用 def 接著方法名稱(小括號變數) do
end 中間則是方法內容

方法必須要在 module 裡面
例如

defmodule MyMath do
  def add(a, b) do
    # #符號是行註解
    # elixir 會回傳方法內的最後一行結果
    # 在這個方法就是回傳 a + b
    a + b
  end
end

# 這樣子使用:
MyMath.add(13, 5)

一不小心就冒出超多新東西,對沒有看過 elixir 的朋友覺得滿不好意思的,
有任何問題就算是基本語法的都可以在下面問,我會盡量回答。


上一篇
5 開始把結構寫成程式吧!
下一篇
7 重做 Game struct 與 出牌方法
系列文
連線網頁卡牌遊戲(Elixir, Phoenix, Liveview)32

尚未有邦友留言

立即登入留言