LoginSignup
4
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-05-18

・目次

サーバ編

クライアント編

開発ツール編

  • 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.png

・実装

lib/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

  #===============================
  # リキャストタイムをクライアントに返信
  #===============================
  def handle_info({:res,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
      1-> svr_id = :gmsvr1
      2-> svr_id = :gmsvr2
      3-> svr_id = :gmsvr3
      4-> svr_id = :gmsvr4
      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

lib/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)
      |> exec(pid_from)
      #IO.puts("#{inspect packet}")
      #run(pid_from,packet)
      {:noreply,state}
  end

  #=============================
  # Login
  #=============================
  def exec(%GmSvr.LoginPacket{}=packet, pid_from) do
    IO.puts "login id=#{packet.id}"

    Character.start_link(packet.id,%Character.Data{id: packet.id, hp: 10, mp: 20, atk: 40, def: 30})

  end

  #=============================
  # Move
  #=============================
  def exec(%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))
    GenServer.cast(:dbsvr1,{:save,{packet.id,packet.pos_x,packet.pos_y,packet.pos_z}})
  end

  #=============================
  # Jump
  #=============================
  def exec(%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 exec(%GmSvr.SkillPacket{}=packet, pid_from) do
    IO.puts "skill id=#{packet.id} (skill_id,to_id)=(#{packet.skill_id},#{packet.to_id})"

    #発動者のステータス取得
    data1  = Character.get(packet.id)
    #対象者のステータス取得
    data2  = Character.get(packet.to_id)
    #スキル効果算出
    effect = Skill.calc(packet.skill_id,data1,data2)
    #対象者のステータス更新
    Character.update(packet.to_id,%{data2| hp: data2.hp-effect})

    #リキャストタイム
    Skill.genRecastTime(packet.skill_id)
    |>Enum.map(fn x ->(spawn(GmSvr,:sendRecast,[pid_from,x])) end)

    #スキル効果を周囲に通知
    Notification.notify(:notify,:notify,GmSvr.Packet.to_bin(%GmSvr.CharaPacket{id: data2.id, hp: data2.hp, mp: data2.mp}))

    IO.inspect(Character.get(packet.id))
  end


  #=============================
  # Transfer
  #=============================
  def exec(%GmSvr.TransferPacket{}=packet, pid_from) do
    IO.puts "transfer user_id=#{packet.id} (from_svr_id,to_svr_id)=(#{packet.from_svr_id},#{packet.to_svr_id})"
    pid = self()
    case packet.to_svr_id do
      1-> svr_id = :gmsvr1
      2-> svr_id = :gmsvr2
      3-> svr_id = :gmsvr3
      4-> svr_id = :gmsvr4
    end

    GenServer.cast(svr_id,{:transfer, {pid, %GmSvr.InnerPacket{id: packet.id, from_svr_id: packet.from_svr_id, to_svr_id: packet.to_svr_id, user_id: pid_from} }})
  end

  #=============================
  # サーバ間処理
  #=============================
  def handle_cast({:transfer,{ pid, %GmSvr.InnerPacket{}=packet}},state) do
    IO.puts "GmSvr:svrtransfer"

    #サーバ間認証を記述する
    #認証結果を返却
    GenServer.cast(pid, {:res_transfer,{:ok, packet}})
    {:noreply,state}
  end

  def handle_cast({:res_transfer,{ stat ,%GmSvr.InnerPacket{}=packet}},state) do
    IO.puts "GmSvr:svrtransfer ok"

    #移動先サーバをユーザに送信
    p = GmSvr.Packet.to_bin(%GmSvr.TransferPacket{id: packet.id, from_svr_id: packet.from_svr_id, to_svr_id: packet.to_svr_id})
    send(packet.user_id, {:res, p})
    {:noreply,state}
  end


  def sendRecast(pid,time) do
    receive do
    after time ->send(pid,{:res,"send_after"})
    end
  end
end

# =========================
# Packet LOGIN
# =========================
defmodule GmSvr.LoginPacket do
  @enforce_keys [:id]
  defstruct [:id]

  def new(
        <<
          id::unsigned-integer-size(16)
        >> = packet
      ) do
    %GmSvr.LoginPacket{id: id}
  end

end
# =========================
# 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  Chara
#=========================
defmodule GmSvr.CharaPacket do
  @enforce_keys [:id, :hp, :mp]
  defstruct [:id, :hp, :mp]
  def new(
    <<
      id::unsigned-integer-size(16),
      hp::unsigned-integer-size(16),
      mp::unsigned-integer-size(16)
    >> = packet
  )do
    %GmSvr.CharaPacket{id: id, hp: hp, mp: mp}
  end
end

# =========================
# Packet  Transfer
#=========================
defmodule GmSvr.TransferPacket do
  @enforce_keys [:id, :from_svr_id,:to_svr_id]
  defstruct [:id, :from_svr_id, :to_svr_id]
  def new(
    <<
      id::unsigned-integer-size(16),
      from_svr_id::unsigned-integer-size(16),
      to_svr_id::unsigned-integer-size(16)
    >> = packet
  )do
    %GmSvr.TransferPacket{id: id, from_svr_id: from_svr_id, to_svr_id: to_svr_id}
  end
end

# =========================
# Packet  InnerPacket
#=========================
defmodule GmSvr.InnerPacket do
  @enforce_keys [:id, :from_svr_id, :to_svr_id, :user_id]
  defstruct [:id, :from_svr_id, :to_svr_id, :user_id]
  def new(
    <<
      id::unsigned-integer-size(16),
      from_svr_id::unsigned-integer-size(16),
      to_svr_id::unsigned-integer-size(16),
      user_id::unsigned-integer-size(16)
    >> = packet
  )do
    %GmSvr.InnerPacket{id: id, from_svr_id: from_svr_id, to_svr_id: to_svr_id, user_id: user_id}
  end
end

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

  def to_bin(%GmSvr.LoginPacket{}=packet)do
    <<
        0::unsigned-integer-size(16)
    >>
  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

  def to_bin(%GmSvr.CharaPacket{}=packet)do
    <<
      4::unsigned-integer-size(16),
      packet.id::unsigned-integer-size(16),
      packet.hp::unsigned-integer-size(16),
      packet.mp::unsigned-integer-size(16)
    >>
  end

  def to_bin(%GmSvr.TransferPacket{}=packet)do
    <<
      5::unsigned-integer-size(16),
      packet.id::unsigned-integer-size(16),
      packet.from_svr_id::unsigned-integer-size(16),
      packet.to_svr_id::unsigned-integer-size(16)
    >>
  end

  def to_bin(%GmSvr.ResponsePacket{}=packet)do
    <<
      6::unsigned-integer-size(16),
      packet.from_svr_id::unsigned-integer-size(16),
      packet.to_svr_id::unsigned-integer-size(16)
    >>
  end
end

lib/charcter.ex

defmodule Character do
  def start_link(char_id,hp) do
    name = {:via, Registry, {:reg_charid, char_id}}
    {:ok, agent} = Agent.start_link(fn -> hp end , name: name)
    ret = Agent.get(name, &(&1))
    IO.inspect(ret)
  end

  def get(char_id) do
    name = {:via, Registry, {:reg_charid, char_id}}
    Agent.get(name, &(&1))
  end

  def update(char_id, data) do
    name = {:via, Registry, {:reg_charid, char_id}}
    Agent.update(name, fn x -> data end)
  end

  def stop(char_id) do
    name = {:via, Registry, {:reg_charid, char_id}}
    Agent.stop(name)
  end
end

defmodule Character.Data do
  @enforce_keys [:id,:hp,:mp,:atk,:def]
  defstruct [:id,:hp,:mp,:atk,:def]

end
lib/skill.ex
defmodule Skill do

  def genRecastTime(skillType) do
    case skillType do
      0 -> [1000,2000,3000,4000]
      1 -> [2000,3000,4000,5000]
      2 -> [3000,4000,5000,6000]
      3 -> [4000,5000,6000,7000]
      _ -> [5000,6000,7000,8000]
    end
  end

  def calc(skillType, %Character.Data{}=user_data, %Character.Data{}=target_data) do
    skillEffect = user_data.atk-target_data.def
    cond do
      skillEffect > 0 -> skillEffect
      true -> 0
    end
  end

end

lib/db_svr.ex
defmodule DbSvr do
  use GenServer

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

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

  def start_db() do
    :mnesia.create_schema([node()])
    :mnesia.start()
    :mnesia.create_table(Chardata,[{:disc_copies, [node()]}, attributes: [:id, :posx, :posy, :posz ]])    
  end

  def handle_cast({:save,{id,posx,posy,posz}},state) do
    :mnesia.transaction(fn() -> :mnesia.write({Chardata,id,posx,posy,posz}) end)
    {:noreply,state}
  end

  def save_db() do
    :mnesia.dump_to_textfile('savedata.txt')
  end

end

lib/mmo_svr/application.ex
defmodule MmoSvr.Application do 
  @moduledoc false

  use Application

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

      %{
        id: :reg_5,
        start: {Registry,:start_link,[ :unique,  :reg_charid, [partitions: System.schedulers_online]]},
        modules: [Registry]
      },
      %{
        id: :dbsvr1,
        start: {DbSvr,:start_link,[1,[name: :dbsvr1]]},
        modules: [DbSvr]
      },

      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

・テスト

サーバ間移動処理を正常実行し、ユーザに移動先を通知すること確認します。

  1. RelaySvr,GmSvr,DbSvrを起動します。
  2. 下表のNo.1のデータをRelaySvr宛てにTCPで送信します。
  3. サーバ間移動処理の結果が正常処理されているのを確認します。
No. 送信パケット(16進数) 送信内容
1 00010005000100010002 (svr_id, packet_type, user_id, from_svr_id, to_svr_id)=(1,5,1,1,2)
No. 受信パケット(16進数) 受信内容
1 0005000100010002 (packet_type, user_id, from_svr_id, to_svr_id)=(5,1,1,2)

・まとめ

  • サーバ間移動処理を実装し、動作確認をおこないました。
  • 今月から来月にかけて大幅なコード修正を行うため、今月予定していた「パーティ機能の実装」は来月に延期させていただきます。

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

4
3
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
4
3