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



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

- 第1章　チャットの実装(2019/1/21公開)
- [第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|
|Memory|8GB|
|言語|elixir v1.7|


# ・今回の目標
今回はゲーム内でチャット(会話)を行えるようにします。
チャットは下記３種類を実装します。

- ダイレクトチャット  
  発言者と特定の1ユーザ間で会話を行う。
- グループチャット   
  発言者が所属するグループ内で会話を行う。
- 範囲チャット  
  発言者の周囲にいるユーザと会話を行う。


実装対象は下図の点線枠内です。
![サーバ構成](https://qiita-image-store.s3.amazonaws.com/0/256958/34b9dede-fba6-bb92-ec96-e44f53ae3149.png)



# ・実装
```elixir
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()       
    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({: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

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    
      5-> svr_id = :chatsvr
    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

```

```elixir
defmodule ChatSvr do

  use GenServer

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

  end

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

  #=============================
  # Packet Handling
  #=============================
    def handle_cast({:parse, {pid_from, data}},state) do
      ChatSvr.Packet.conv(data)
      |> run(pid_from)
      {:noreply,state}
  end

  #=============================
  # Direct Join
  #=============================
  def run(%ChatSvr.DirectJoin{}=packet, pid_from) do
    Notification.add(:reg_direct, packet.from_id,pid_from)
    Notification.add(:reg_shout , :shout        ,pid_from)
    IO.puts "ChatSvr:DirectJoin"
  end

  #=============================
  # Group Join
  #=============================
    def run(%ChatSvr.GroupJoin{}=packet, pid_from) do
      Notification.add(:reg_group, packet.group_id , pid_from)
      IO.puts "ChatSvr:GroupJoin"
    end


  #=============================
  # Direct
  #=============================
  def run(%ChatSvr.DirectPacket{}=packet, pid_from) do
    IO.puts "direct from_id=#{packet.from_id}, to_id=#{packet.to_id}, msg=#{packet.msg})"
    Notification.notify(:reg_direct, packet.to_id, :notify, ChatSvr.Packet.to_bin(packet))
  end

  #=============================
  # Group
  #=============================
  def run(%ChatSvr.GroupPacket{}=packet, pid_from) do
    IO.puts "group from_id=#{packet.from_id}, group_id=(#{packet.group_id}, msg=#{packet.msg})"
    Notification.notify(:reg_group, packet.group_id, :notify, ChatSvr.Packet.to_bin(packet))
  end

  #=============================
  # Shout
  #=============================
  def run(%ChatSvr.ShoutPacket{}=packet, pid_from) do
    IO.puts "shout from_id=#{packet.from_id}, msg=#{packet.msg})"
    Notification.notify(:reg_shout, :shout, :notify, ChatSvr.Packet.to_bin(packet))
  end
end

# =========================
# ChatPacket Direct Join
# =========================
defmodule ChatSvr.DirectJoin do
  @enforce_keys [:from_id]
  defstruct [:from_id]

  def new(
        <<
          from_id::unsigned-integer-size(16),
        >> = packet
      ) do
    %ChatSvr.DirectJoin{from_id: from_id}
  end

end

# =========================
# ChatPacket Group Join
# =========================
  defmodule ChatSvr.GroupJoin do
    @enforce_keys [:from_id,:group_id]
    defstruct [:from_id,:group_id]

    def new(
          <<
            from_id::unsigned-integer-size(16),
            group_id::unsigned-integer-size(16),
          >> = packet
        ) do
      %ChatSvr.GroupJoin{from_id: from_id, group_id: group_id}
    end
  end


# =========================
# ChatPacket Direct
# =========================
defmodule ChatSvr.DirectPacket do
  @enforce_keys [:from_id, :to_id, :msg]
  defstruct [:from_id, :to_id, :msg]

  def new(
        <<
          from_id::unsigned-integer-size(16),
          to_id::unsigned-integer-size(16),
          msg::binary
        >> = packet
      ) do
    %ChatSvr.DirectPacket{from_id: from_id, to_id: to_id, msg: msg}
  end

end

# =========================
# ChatPacket Group
# =========================
defmodule ChatSvr.GroupPacket do
  @enforce_keys [:from_id, :group_id, :msg]
  defstruct [:from_id, :group_id, :msg]

  def new(
        <<
          from_id::unsigned-integer-size(16),
          group_id::unsigned-integer-size(16),
          msg::binary
        >> = packet
      ) do
    %ChatSvr.GroupPacket{from_id: from_id, group_id: group_id, msg: msg}
  end
end


# =========================
# ChatPacket Shout
# =========================
  defmodule ChatSvr.ShoutPacket do
    @enforce_keys [:from_id, :msg]
    defstruct [:from_id, :msg]

    def new(
          <<
            from_id::unsigned-integer-size(16),
            msg::binary
          >> = packet
        ) do
      %ChatSvr.ShoutPacket{from_id: from_id, msg: msg}
    end
  end

defmodule ChatSvr.Packet do
  def conv(
        <<
          pk_type::unsigned-integer-size(16),
          temp::binary
        >> = packet
      ) do
    case pk_type do
      0 -> p = ChatSvr.DirectJoin.new(temp)
      1 -> p = ChatSvr.GroupJoin.new(temp)
      2 -> p = ChatSvr.DirectPacket.new(temp)
      3 -> p = ChatSvr.GroupPacket.new(temp)
      4 -> p = ChatSvr.ShoutPacket.new(temp)
    end
  end

  def to_bin(%ChatSvr.DirectPacket{}=packet)do
    <<
        1::unsigned-integer-size(16),
        packet.from_id::unsigned-integer-size(16),
        packet.to_id::unsigned-integer-size(16),
        packet.msg::binary
    >>
  end

  def to_bin(%ChatSvr.GroupPacket{}=packet)do
    <<
        2::unsigned-integer-size(16),
        packet.from_id::unsigned-integer-size(16),
        packet.group_id::unsigned-integer-size(16),
        packet.msg::binary
    >>
  end

  def to_bin(%ChatSvr.ShoutPacket{}=packet)do
    <<
      3::unsigned-integer-size(16),
      packet.from_id::unsigned-integer-size(16),
      packet.msg::binary
    >>
  end
end

```

```elixir
defmodule Notification do

  #=====================
  # Chat
  #=====================
  def add(reg_name,topic,val) do
    {:ok, _} = Registry.register(reg_name, topic, val)
  end

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

```

```elixir
defmodule MmoSvr.Application do 
  @moduledoc false

  use Application

  def start(_type, _args) do
    
    import Supervisor.Spec
    children = [      
      supervisor(Task.Supervisor, [[name: RelaySvr.TaskSupervisor]]),   

      #Chat    
      %{
        id: :reg_1,
        start: {Registry,:start_link,[ :duplicate,  :reg, [partitions: System.schedulers_online]]},
        modules: [Registry]
      },
      %{
        id: :reg_2,
        start: {Registry,:start_link,[ :duplicate,  :reg_direct, [partitions: System.schedulers_online]]},
        modules: [Registry]
      },
      %{
        id: :reg_3,
        start: {Registry,:start_link,[ :duplicate,  :reg_group, [partitions: System.schedulers_online]]},
        modules: [Registry]
      },
      %{
        id: :reg_4,
        start: {Registry,:start_link,[ :duplicate,  :reg_shout, [partitions: System.schedulers_online]]},
        modules: [Registry]
      },


      worker(Task,[RelaySvr,:accept,[4040]]),
      %{
        id: :chatsvr,
        start: {ChatSvr,:start_link,[5,[name: :chatsvr]]},
        modules: [ChatSvr]
      }     
    ]   
    opts = [strategy: :one_for_one, name: MmoSvr.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

```

# ・テスト
RelaySvr,ChatSvrを起動し、3クライアントをRelaySvrにTCP/IPで接続します。


### ・ダイレクトチャットの場合
 1. 各クライアントのNo.1をRelaySvrに送信する。
 1. クライアント1のNo.2をRelaySvrに送信する。クライアント２のみにメッセージが出力される


- クライアント1

|No.|送信パケット(16進数)|出力先|出力結果|
|:--|:--|:--|:--|:--|
|1|000500000000|-|-|
|2|000500020000000101|クライアント2|00010000000101|

- クライアント2

|No.|送信パケット(16進数)|出力先|出力結果|
|:--|:--|:--|:--|:--|
|1|000500000001|-|-|

- クライアント3

|No.|送信パケット(16進数)|出力先|出力結果|
|:--|:--|:--|:--|:--|
|1|000500000002|-|-|


```c++
//クライアント2
>00020000010002
```

### グループチャットの場合

 1. 各クライアントのNo.1をRelaySvr送信する。
 1. クライアント1のNo.2をRelaySvr送信する。クライアント1～3にメッセージが出力される


- クライアント1

|No.|送信パケット(16進数)|出力先|出力結果|
|:--|:--|:--|:--|:--|
|1|0005000100000100|-|-|
|2|000500030000010002|クライアント1,2,3|00020000010002|

- クライアント2

|No.|送信パケット(16進数)|出力先|出力結果|
|:--|:--|:--|:--|:--|
|1|0005000100010100|-|-|

- クライアント3

|No.|送信パケット(16進数)|出力先|出力結果|
|:--|:--|:--|:--|:--|
|1|0005000100020100|-|-|

```c++
//クライアント1
>00020000010002

//クライアント2
>00020000010002

//クライアント3
>00020000010002
```

### 範囲チャットの場合

 1. 各クライアントのNo.1をRelaySvrに送信する。
 1. クライアント1のNo.2をRelaySvrに送信する。クライアント1～3にメッセージが出力される


- クライアント1

|No.|送信パケット(16進数)|出力先|出力結果|
|:--|:--|:--|:--|:--|
|1|000500000000|-|-|
|2|000500040002010002|クライアント1,2,3|00030002010002|

- クライアント2

|No.|送信パケット(16進数)|出力先|出力結果|
|:--|:--|:--|:--|:--|
|1|000500000001|-|-|


- クライアント3

|No.|送信パケット(16進数)|出力先|出力結果|
|:--|:--|:--|:--|:--|
|1|000500000002|-|-|


```c++
//クライアント1
>00030002010002

//クライアント2
>00030002010002

//クライアント3
>00030002010002
```

# ・まとめ
- ゲーム内のチャットを３種類実装し、動作確認をおこないました。
- 現状ではDBサーバを実装していないため、ログ保存ができていません。
- DBサーバ実装した後、本機能と連携させログ保存を実装予定です。

最後まで読んでいただきありがとうございました。