Hello! After World!! - 初心者がelixirでオンラインゲーム製作に挑戦してみた サーバ編(第0章)


# ・はじめに

最近elixirをはじめたGKBRです。elixirは、
- 耐障害が高い
- 平行・分散処理を実装しやすい
- ネットワーク処理を標準搭載

などの特徴を有し、大量のデータを安定的に処理し続ける必要があるIot分野などへの活用が期待されています。

今大注目の言語です！

早速流行に乗るためお約束のアレをやってみました。

```elixir
iex> IO.puts("Hello World!")
Hello World!
```

おー！感動です。
ただ、いまいちモチベーションがわかない。どうしよう。



そうだ！個人的にの興味ある分野（ゲーム制作）を題材にすればやる気みなぎり学習もはかどるはず！ジャンルはelixirの特徴を生かせる??オンラインゲームにしよう！最終目標はWOWやFF14の類似作品にすることです。

無謀ですか？

私もそう思います。
でも何事も挑戦です！
当たって砕けてみたいと思います。

ちなみにqiita初投稿です。
緊張してきた…


# ・目次
### サーバ編
- 第0章　Hello! After World!! - 初心者がelixirでオンラインゲーム製作に挑戦してみた(2018/12/21公開)

- [第1章　チャットの実装(2019/1/21公開)](https://qiita.com/GKBR/items/38610ab5990f58f8967d)
- [第2章　スキル・道具の実装(2019/2/21公開)](https://qiita.com/GKBR/items/64915e1fc5e442a38817)
- [第3章　セーブ機能の実装(2019/3/27公開)](https://qiita.com/GKBR/items/ccbb97b31a52610ac7b2)
- [第4章　サーバ間移動の実装(2019/5/18公開)](https://qiita.com/GKBR/items/151d5401a609bb55327d)
- [第5章　パーティ機能の実装(2019/10/14)](https://qiita.com/GKBR/items/1032d656a7816613f4a8)
- 第6章　ダンジョン機能の実装(2019/10月下旬予定)
- 第7章　ボス機能の実装(2019/11月予定)
- 第8章　ジョブ機能の実装(2019/11月予定)
- 第9章　トレード機能の実装(2019/11月予定)
- 第10章　未定(2019/11月予定)
- 最終章　ゲーム公開！(2019/12/21予定)

### クライアント編
- [こちらからどうぞ](https://qiita.com/GKBR/items/37045f7392a2103399c0)

### 開発ツール編
- 2019年公開予定

# ・参考文献 
|No.|書籍名|著者|出版年|
|:--|:--|:--|:--|
|1|MMORPGゲームサーバープログラミング|ナム ジェウク|2005|
|2|オンラインゲームを支える技術　壮大なプレイ空間の舞台裏|中嶋 謙互|2011|
|3|クラウドゲームを作る技術　マルチプレイゲーム開発の新戦力|中嶋 謙互|2018|
|4|ドラゴンクエストXを支える技術　大規模オンラインRPGの舞台裏|青山 公士|2018|
|5|プログラミングElixir|Dave Thomas|2016|

# ・開発環境

|PC|Mac Book Pro (Retina, 13-inch, Late 2013) |
|:--|:--|
|OS|Mojave|
|言語|elixir v1.7|


# ・今回の目標
今回は複数のユーザが同一マップ上を歩き回れるようにすることです。

# ・サーバ構成
サーバ構成です。ほぼすべてのゲームロジックをサーバサイドに実装するサーバ集約型を採用します。（参考文献1,2,3,4）


 ![fig01.png](https://qiita-image-store.s3.amazonaws.com/0/256958/e4de95df-4cf4-1126-d55e-d994ce40f402.png)

- RelaySvr:リレーサーバ 
  - クライアント-サーバ間のTCP/IP送受信処理を行う 
  - パケットの暗号化・復号化を行う 
  - パケットのシリアライズ・デシリアライズを行う。
  
- GmSvr:ゲームサーバ
  - ゲーム内でキャラクタ移動や道具使用時のロジックを実装する 
  
- DBサーバ 
  - プレイヤ情報の永続化を行う  
 
- ZONEサーバ 
  - プレイヤの位置情報管理を行う 
　 
- チャットサーバ 
  - ゲーム内のチャット処理を行う 

今回は複数のユーザが同一マップ上を歩行できるのを目標としているため、上図の点線枠内についてのみ実装対象としました。その他のサーバについては後日追加実装する予定です。

# ・実装
- RelaySvr

```elixir:relay_svr.ex
defmodule RelaySvr do

  def accept(port) do
    {:ok, listen} = :gen_tcp.listen(port, [:binary, packet: 0, active: false, reuseaddr: true])
    loop_accept(listen)
  end

  defp loop_accept(listen) do

    {:ok, socket} = :gen_tcp.accept(listen)
    IO.puts "client: #{inspect socket}"

    #===========================
    # TCP Process start
    #===========================
    pid = spawn_link(RelaySvr.Svr,:start_link,[socket,[]])
    IO.puts "RlaySvr.Svr: #{inspect pid}"

    loop_accept(listen)
  end

end

defmodule RelaySvr.Svr do

  use GenServer

  def start_link(state,opts) do
    {_,pid} = GenServer.start_link(__MODULE__,state,opts)

    self_pid = self()
    

    GenServer.cast(:gmsvr1,{:svrin,pid})
    task_pid =spawn_link(RelaySvr.Svr, :serve,[state,pid])

    
    {status,msg} = :gen_tcp.controlling_process(state, task_pid)
    
    :ok

  end

  def init(state) do
    self_pid = self()
    
    {:ok,state}
  end

  #================================
  # サーバ接続時の処理
  #================================
  def handle_cast({:svrin},state) do
    IO.puts("RelaySvr.Svr:cast :svrin")
    self_pid = self()
    GenServer.cast(:gmsvr1,{:svrin,{self_pid}})
    {:noreply,state}
  end

  #================================
  # 処理結果をクライアントに返信
  #================================
  def handle_cast({:res,msg},state) do
    :gen_tcp.send(state, msg)
    {:noreply,state}
  end

  #===============================
  # クライアントに通知
  #===============================
  def handle_cast({:notify,msg},state) do
    :gen_tcp.send(state, msg)
    {:noreply,state}
  end



  #==============================
  # TCP Server
  #==============================
  def serve(socket,pid) do   
    socket |> read_line |> write_line(socket,pid)
    serve(socket,pid)
  end

  defp read_line(socket) do    
    {:ok, data} = :gen_tcp.recv(socket, 0)    
    data
  end

  defp write_line(line, socket,pid) do
    packet = Relay.Packet.conv(line)
    GenServer.cast(packet.svr_id,{:parse, {pid,packet.data}})

  end

end

#================================
# RelaySvr内のパケット定義
#================================
defmodule Relay.Packet do
  @enforce_keys [:svr_id, :data]
  defstruct [:svr_id, :data]

  def conv(
    <<
      svr_id::size(16),
      data::binary
    >> = packet
  )do
    case svr_id do
      1-> svr_id = :gmsvr1
      2-> svr_id = :gmsvr2
      3-> svr_id = :gmsvr3
      4-> svr_id = :gmsvr4
    end
    %Relay.Packet{svr_id: svr_id , data: data}
  end
end

defmodule Relay.RelayPacket do
  @enforce_keys [:svr_id, :data]
  defstruct [:svr_id, :data]

  def new(
    <<
      svr_id::size(16),
      data::binary
    >> = packet
  )do
    %Relay.RelayPacket{svr_id: svr_id, data: data}
  end
end
```
- GmSvr

```elixir:gm_svr.ex
defmodule GmSvr do

  use GenServer

  def start_link(init,opts) do
    {_,pid}=GenServer.start_link(__MODULE__, init , opts )

  end

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

  #=============================
  # Svrin
  #=============================
  def handle_cast({:svrin,pid_from},state) do

    Notification.add(:notify,pid_from)
    IO.puts "GmSvr:svrin"

    {:noreply,state}
  end

  #=============================
  # Packet Handling
  #=============================
  def handle_cast({:parse, {pid_from, data}},state) do
      GmSvr.Packet.conv(data)
      |> run(pid_from)
      #IO.puts("#{inspect packet}")
      #run(pid_from,packet)
      {:noreply,state}
  end

  #=============================
  # Move
  #=============================  
  def run(%GmSvr.MvPacket{}=packet, pid_from) do
    
    IO.puts "mv id=#{packet.id} (x,y)=(#{packet.pos_x},#{packet.pos_y})"

    Notification.notify(:notify,:notify,GmSvr.Packet.to_bin(packet))

  end

  #=============================
  # Jump
  #=============================
  def run(%GmSvr.JumpPacket{}=packet, pid_from) do
  
    IO.puts "jump id=#{packet.id} (x,y)=(#{packet.pos_x},#{packet.pos_y})"

    Notification.notify(:notify,:notify,GmSvr.Packet.to_bin(packet))

  end


  #=============================
  # Skill
  #=============================
  def run(%GmSvr.SkillPacket{}=packet, pid_from) do
   
    IO.puts "skill id=#{packet.id} (skill_id,to_id)=(#{packet.skill_id},#{packet.to_id})"
    
    Notification.notify(:notify,:notify,GmSvr.Packet.to_bin(packet))
  end

end

# =========================
# GmSvr内のパケット定義
# =========================

# =========================
# Packet MOVE
# =========================
defmodule GmSvr.MvPacket do
  @enforce_keys [:id, :pos_x, :pos_y, :pos_z]
  defstruct [:id, :pos_x, :pos_y, :pos_z]

  def new(
        <<
          id::unsigned-integer-size(16),
          pos_x::unsigned-integer-size(16),
          pos_y::unsigned-integer-size(16),
          pos_z::unsigned-integer-size(16)
        >> = packet
      ) do
    %GmSvr.MvPacket{id: id, pos_x: pos_x, pos_y: pos_y, pos_z: pos_z}
  end

end

# =========================
# Packet JUMP
# =========================
defmodule GmSvr.JumpPacket do
  @enforce_keys [:id, :pos_x, :pos_y, :pos_z]
  defstruct [:id, :pos_x, :pos_y, :pos_z]

  def new(
        <<
          id::unsigned-integer-size(16),
          pos_x::unsigned-integer-size(16),
          pos_y::unsigned-integer-size(16),
          pos_z::unsigned-integer-size(16)
        >> = packet
      ) do
    %GmSvr.JumpPacket{id: id, pos_x: pos_x, pos_y: pos_y, pos_z: pos_z}
  end
end

# =========================
# Packet  SKILL
#=========================
defmodule GmSvr.SkillPacket do
  @enforce_keys [:id, :skill_id,:to_id]
  defstruct [:id, :skill_id,:to_id]
  def new(
    <<
      id::unsigned-integer-size(16),
      skill_id::unsigned-integer-size(16),
      to_id::unsigned-integer-size(16)
    >> = packet
  )do
    %GmSvr.SkillPacket{id: id, skill_id: skill_id, to_id: to_id}
  end
end

defmodule GmSvr.Packet do
  def conv(
        <<
          pk_type::unsigned-integer-size(16),
          temp::binary
        >> = packet
      ) do
    case pk_type do
      1 -> p = GmSvr.MvPacket.new(temp)
      2 -> p = GmSvr.JumpPacket.new(temp)
      3 -> p = GmSvr.SkillPacket.new(temp)
    end
  end

  def to_bin(%GmSvr.MvPacket{}=packet)do
    <<
        1::unsigned-integer-size(16),
        packet.id::unsigned-integer-size(16),
        packet.pos_x::unsigned-integer-size(16),
        packet.pos_y::unsigned-integer-size(16),
        packet.pos_z::unsigned-integer-size(16)
    >>
  end

  def to_bin(%GmSvr.JumpPacket{}=packet)do
    <<
        2::unsigned-integer-size(16),
        packet.id::unsigned-integer-size(16),
        packet.pos_x::unsigned-integer-size(16),
        packet.pos_y::unsigned-integer-size(16),
        packet.pos_z::unsigned-integer-size(16)
    >>
  end

  def to_bin(%GmSvr.SkillPacket{}=packet)do
    <<
      3::unsigned-integer-size(16),
      packet.id::unsigned-integer-size(16),
      packet.skill_id::unsigned-integer-size(16),
      packet.to_id::unsigned-integer-size(16)
    >>
  end
end

```

- Notification

```elixir:notification.ex
defmodule Notification do
  def add(topic,val) do
    {:ok, _} = Registry.register(:reg, topic, val)
  end

  def notify(topic,type,val) do
    Registry.dispatch(:reg, topic, fn entries -> for {_, pid} <- entries, do: GenServer.cast(pid, {type, val}) end)
  end
end
```

```elixir:application.ex
defmodule MmoSvr.Application do
 
  use Application
  def start(_type, _args) do
    # List all child processes to be supervised
    import Supervisor.Spec
    children = [
      supervisor(Task.Supervisor, [[name: RelaySvr.TaskSupervisor]]),
      worker(Registry,[ :duplicate,  :reg, [partitions: System.schedulers_online]]),
      worker(Task,[RelaySvr,:accept,[4040]]),
      %{
        id: :gmsvr_1,
        start: {GmSvr,:start_link,[1,[name: :gmsvr1]]},
        modules: [GmSvr]
      },
      %{
        id: :gmsvr_2,
        start: {GmSvr,:start_link,[2,[name: :gmsvr2]]},
        modules: [GmSvr]
      },
      %{
        id: :gmsvr_3,
        start: {GmSvr,:start_link,[3,[name: :gmsvr3]]},
        modules: [GmSvr]
      },
      %{
        id: :gmsvr_4,
        start: {GmSvr,:start_link,[4,[name: :gmsvr4]]},
        modules: [GmSvr]
      }
    ]
    opts = [strategy: :one_for_one, name: MmoSvr.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
```

# ・テスト
RelaySvr,GmSvrを起動し、RelaySvr宛に下表のパケットをTCP/IPで送信します。

|No.|送信パケット(16進数)|予想される出力結果|
|:--|:--|:--|
|1|00010001000100030004000a|mv id=1 (x,y)=(3,4)|
|2|00010002000200130014000a|jump id=2 (x,y)=(19,20)|
|3|00010003000300230024|skill id=3 (skill_id,to_id)=(35,36)|

```elixir
iex > mv id=1 (x,y)=(3,4)
iex > jump id=2 (x,y)=(19,20)
iex > skill id=3 (skill_id,to_id)=(35,36)
```
問題なさそうです。
 
# ・テストプレイ
 簡単なクライアントプログラムを作成し、複数のクライアントをサーバに接続し移動してみます。
[Hello! After World! 第0章　テストプレイ](https://youtu.be/TxbCxijDE4k "sample movie")

 
 
# ・まとめ 
- 今回は複数のクライアントが同一マップ内を自由に歩行が可能な段階まで実装しました。
- 実装したサーバ群は実績十分な古典的なオンラインゲームの実装手法(rpc)をelixirにほぼそのまま適用し実装しました。そのため現状ではelixirのメリットを活かした実装になっていません。 
- 今後はelixirの長所を最大限生かした実装に順次変更予定です。 
- オンラインゲームとして必要な機能を追加実装していきます。進捗はQiitaで毎月21日に報告いたします。 
- 来年度のアドベントカレンダで一年間の進捗/完成版報告を目標とします！ 

# ・最後に
- Elixirで仕事したいです。

最後まで読んで頂き有難うございます。