iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 8
0
Software Development

從讀遊戲原始碼學做連線遊戲系列 第 8

Day 08 - 指令系統 - Unlight 的指令處理(三)

繼續往下看到 #init_receive 方法的部分,不過出現了一些比較少見的程式碼。

# 受信コマンドの初期化
def init_receive(cmd)
  cmd.each do |c|
    n = c[0].id2name+"_r"
    @method_list << n.intern
    ret = <<-EOF
          def #{n}(data)
           # p data
#{gen_receve_cmd(c[1],c[0])} 
          end
    EOF
    puts ret if OUTPUT_EVAL
    @klass.class_eval(ret)
  end
end

首先是 #id2name 這個方法,如果稍微查一下文件的話,會發現其實就是 #to_s 方法,這是因為想要產生一個叫做 xxx_r 的字串,例如 register_r 這樣的名稱,而 #intern 這個方法也是一樣的,只是再將她轉換回 Symbol 而以,跟 #to_sym 的意思是差不多的。

在 CRuby 實作中,我們熟悉的 Symbol 其實是用 intern 命名的,所以如果嘗試在原始碼裡面找 Symbol 反而會找不到太多線索。這邊 Unlight 沒有用大家熟悉的 #to_s#to_sym 的原因不知道是什麼,也許是有什麼特殊考量。

接下來的 ret 字串比較有趣,Unlight 將一段 Ruby 程式碼用字串存起來,並且帶入了生成的指令名稱(register_r),並且透過前面指令定義的資訊呼叫 #gen_receive_cmd 這個方法。

最後在對初始化指令的對象(AuthServer 物件)執行 .class_eval 方法,利用 Ruby DSL 的特性將這個字串轉變成 Ruby 程式碼執行。

register 指令生成 Ruby 指令為例子做 .class_eval,基本上跟下面這段程式碼的效果是一樣的。

class AuthServer
  def register_r
    # ... (gen_receive_cmd)
  end
end

在 Ruby on Rails 裡面也會應用這樣的技巧動態生成某些行為(Ex. Model 相關物件,像是 User_Relation 這種物件)不過在這邊 Unlight 是透過這樣的方式減少重複撰寫定義指令解析處理的行為,每個指令解析的行為跟邏輯是固定重複撰寫並沒有意義,而這種方式也可以避免動態的判斷跟處理,不用製作很多物件來解析跟判斷,反而能加快運行速度。

在其他語言上類似 C 語言的 Macro (巨集)或者 Golang 的 Generator 的使用方式,雖然原理跟定義上有些差異但是達到的效果是類似的。

了解 Unlight 如何動態生成指令之後,還需要分析 Unlight 為什麼要使用 #gen_receive_cmd 來動態產生指令的內容。

# 受信コマンドの中身の文字列を生成する
def gen_receve_cmd(val, name)
  ret = ''
  pos = 0
  s = ''
  q = ''
  if val
    val.each do|i|
      c = @cmd_val.new(i[0], i[1],i[2])
      if c.size == 0
        ret << "            #{c.name}_len = data[#{s unless s==""}#{pos},2].unpack('n*')[0]\n"
        #            ret << "p data[#{pos},2]\n"
        pos += 2
        #            ret << "p #{c.name}_len\n"
        ret << "            #{c.name} = data[#{s unless s==""}#{pos},#{c.name}_len]#{type_rec_res(c.type)}\n"
        s << "#{c.name}_len+"
      else
        ret << "            #{c.name} = data[#{s unless s==""}#{pos},#{c.size}]#{type_rec_res(c.type)}\n"
        pos += c.size
      end
      q << c.name+","
    end
  end
  ret << "            #{name}(#{q.chop!})"
  ret
end

這段程式碼看似複雜,不過實際上我們以 register 指令作為例子,在 register_r 這個被動態定義的方法來看,我們會收到一個 data 的指令的內容,但是註冊指令本身需要五個參數(請參考上一篇)才能夠完成,但是這個 data 的指令內容是一段 Byte Array 要怎麼拆解才能得到對的數值呢?

#gen_receive_cmd 裡面我們可以看到一些線索:

  1. val 的解析,也就是參數
  2. 指令的執行

我們先看指令執行的部分,也就是最後兩行:

# ...
  ret << "            #{name}(#{q.chop!})"
  ret
# ...

動作上來說很簡單,就是直接插入一個 register(name, ...) 這段程式碼,用來呼叫另一個 Ruby 方法。

因此前面生成接收方法會使用 register_r 是為了區分處理資料的部分跟實際上定義的邏輯 register

接下來就是指令解析的部分

# ...
    val.each do|i|
      c = @cmd_val.new(i[0], i[1],i[2])
      if c.size == 0
        ret << "            #{c.name}_len = data[#{s unless s==""}#{pos},2].unpack('n*')[0]\n"
        #            ret << "p data[#{pos},2]\n"
        pos += 2
        #            ret << "p #{c.name}_len\n"
        ret << "            #{c.name} = data[#{s unless s==""}#{pos},#{c.name}_len]#{type_rec_res(c.type)}\n"
        s << "#{c.name}_len+"
      else
        ret << "            #{c.name} = data[#{s unless s==""}#{pos},#{c.size}]#{type_rec_res(c.type)}\n"
        pos += c.size
      end
      q << c.name+","
    end
# ...

看起來似乎很複雜,實際上就是區分為「長度固定」跟「長度不固定」兩種情況。

前面在定義指令的時候應該有發現字串的大小(size)設定值是 0 明顯不合理,但是字串的長度肯定是不固定的,因此在 Unlight 終會定義大小為 0 來表示需要另外處理才能夠取得資料。

針對長度不固定的資料是基於下面這段處理的:

# ...
        ret << "            #{c.name}_len = data[#{s unless s==""}#{pos},2].unpack('n*')[0]\n"
        #            ret << "p data[#{pos},2]\n"
        pos += 2
        #            ret << "p #{c.name}_len\n"
        ret << "            #{c.name} = data[#{s unless s==""}#{pos},#{c.name}_len]#{type_rec_res(c.type)}\n"
        s << "#{c.name}_len+"
# ...

不過這樣有點難以理解,我們以 register 指令的第一個參數 name 來當做例子,實際上會寫成這樣

name_len = data[pos,2].unpack('n*')[0]
pos += 2
name = data[pos,name_len]

如此一來就比較好懂了,在 Unlight 裡面定義長度不固定的類型的前 2 Bytes 就是表示這段資料的長度,因此要先抓出來獲取這段資料的長度,那麼從第三個 Byte 開始算到到指定的長度就是資料的本體。

至於其他類型的資料如果長度是固定的,就很容易處理:

ret << "            #{c.name} = data[#{s unless s==""}#{pos},#{c.size}]#{type_rec_res(c.type)}\n"

因為只需要基於目前的指標位置(pos)開始讀取當初設定的長度,然後依照類型 (#type_rec_res)查詢計裡方是再做解析就能正確地處理資料。

:char 類型為例子,查詢的到的處理方式是用 .unpack('c')[0] 組裡,因此就會是 data[pos,1].unpack('c')[0] 這樣的程式碼被執行。

到此為止,我們已經知道應該要如何產生指令封包給伺服器讀取跟解析。但是伺服器呼叫方法後是怎麼動作的,這就要來討論 Unlight 的 Controller 了。當然,這不會是我們平常討論的 MVC 的那個 Controller 而是在 Unlight 裡面表示指令動作的行為集合。

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


上一篇
Day 07 - 指令系統 - Unlight 的指令處理(二)
下一篇
Day 09 - 指令系統 - Unlight 的指令處理(四)
系列文
從讀遊戲原始碼學做連線遊戲33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言