2
0

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でオンラインゲーム製作に挑戦してみた サーバ編(第2章)

Last updated at Posted at 2019-02-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

・今回の目標

今回はゲーム内でプレーヤがスキル(特殊技能や魔法等)を扱えるようにします。
スキルはクールダウンタイム(CG:再使用待機時間)後に再度使用可能とします。

実装対象は下図の点線枠内です。

図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))
  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

  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

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)
    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
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/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]
      },

      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を起動し、3クライアントをRelaySvrにTCP/IPで接続します。
  2. 各クライアントのNo.1をRelaySvrに送信する。
  3. クライアント1のNo.2をRelaySvrに送信する。
  4. クライアント1にサーバ側からリキャストタイムが送信される。
  5. 各クライアントにスキル発動結果が出力される。
  • クライアント1
No. 送信パケット(16進数)
1 000100000001
2 0003000100230024
  • クライアント2
No. 送信パケット(16進数)
1 000100000024
  • クライアント3
No. 送信パケット(16進数)
1 000100000030
//クライアント1 
//リキャストタイム
>73656e645f6166746572
>73656e645f6166746572
>73656e645f6166746572
>73656e645f6166746572
//スキル発動結果
>00040024000a0014

//クライアント2 
//スキル発動結果
>00040024000a0014

//クライアント3 
//スキル発動結果
>00040024000a0014

・まとめ

  • ゲーム内のスキルを実装し、動作確認をおこないました。
  • 現状ではスキル効果の算出式は1種類のみですが、今後はスキル種別に応じた算出式を追加予定です。
  • 道具の処理間に合いませんでした。申し訳ありません。。次回実装します。

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

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?