LoginSignup
14
9

More than 3 years have passed since last update.

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

Last updated at Posted at 2018-12-21

・はじめに

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

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

今大注目の言語です!

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

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

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

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

無謀ですか?

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

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

・目次

サーバ編

クライアント編

開発ツール編

  • 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

  • RelaySvr:リレーサーバ

    • クライアント-サーバ間のTCP/IP送受信処理を行う
    • パケットの暗号化・復号化を行う
    • パケットのシリアライズ・デシリアライズを行う。
  • GmSvr:ゲームサーバ

    • ゲーム内でキャラクタ移動や道具使用時のロジックを実装する
  • DBサーバ

    • プレイヤ情報の永続化を行う
  • ZONEサーバ

    • プレイヤの位置情報管理を行う  
  • チャットサーバ

    • ゲーム内のチャット処理を行う

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

・実装

  • RelaySvr
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
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
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
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)
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章 テストプレイ

・まとめ

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

・最後に

  • Elixirで仕事したいです。

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

14
9
2

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
14
9