5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-01-21

・目次

サーバ編

クライアント編

開発ツール編

  • 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

・今回の目標

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

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

実装対象は下図の点線枠内です。
サーバ構成

・実装

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

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

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

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に送信する。
  2. クライアント1のNo.2をRelaySvrに送信する。クライアント2のみにメッセージが出力される
  • クライアント1

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

  • クライアント2

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

  • クライアント3

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

//クライアント2
>00020000010002

グループチャットの場合

  1. 各クライアントのNo.1をRelaySvr送信する。
  2. クライアント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|-|-|

//クライアント1
>00020000010002

//クライアント2
>00020000010002

//クライアント3
>00020000010002

範囲チャットの場合

  1. 各クライアントのNo.1をRelaySvrに送信する。
  2. クライアント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|-|-|

//クライアント1
>00030002010002

//クライアント2
>00030002010002

//クライアント3
>00030002010002

・まとめ

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

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

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?