・はじめに
最近elixirをはじめたGKBRです。elixirは、
- 耐障害が高い
- 平行・分散処理を実装しやすい
- ネットワーク処理を標準搭載
などの特徴を有し、大量のデータを安定的に処理し続ける必要があるIot分野などへの活用が期待されています。
今大注目の言語です!
早速流行に乗るためお約束のアレをやってみました。
iex> IO.puts("Hello World!")
Hello World!
おー!感動です。
ただ、いまいちモチベーションがわかない。どうしよう。
そうだ!個人的にの興味ある分野(ゲーム制作)を題材にすればやる気みなぎり学習もはかどるはず!ジャンルはelixirの特徴を生かせる??オンラインゲームにしよう!最終目標はWOWやFF14の類似作品にすることです。
無謀ですか?
私もそう思います。
でも何事も挑戦です!
当たって砕けてみたいと思います。
ちなみにqiita初投稿です。
緊張してきた…
・目次
サーバ編
-
第0章 Hello! After World!! - 初心者がelixirでオンラインゲーム製作に挑戦してみた(2018/12/21公開)
-
第6章 ダンジョン機能の実装(2019/10月下旬予定)
-
第7章 ボス機能の実装(2019/11月予定)
-
第8章 ジョブ機能の実装(2019/11月予定)
-
第9章 トレード機能の実装(2019/11月予定)
-
第10章 未定(2019/11月予定)
-
最終章 ゲーム公開!(2019/12/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 |
言語 | elixir v1.7 |
・今回の目標
今回は複数のユーザが同一マップ上を歩き回れるようにすることです。
・サーバ構成
サーバ構成です。ほぼすべてのゲームロジックをサーバサイドに実装するサーバ集約型を採用します。(参考文献1,2,3,4)
-
RelaySvr:リレーサーバ
- クライアント-サーバ間のTCP/IP送受信処理を行う
- パケットの暗号化・復号化を行う
- パケットのシリアライズ・デシリアライズを行う。
-
GmSvr:ゲームサーバ
- ゲーム内でキャラクタ移動や道具使用時のロジックを実装する
-
DBサーバ
- プレイヤ情報の永続化を行う
-
ZONEサーバ
- プレイヤの位置情報管理を行う
- プレイヤの位置情報管理を行う
-
チャットサーバ
- ゲーム内のチャット処理を行う
今回は複数のユーザが同一マップ上を歩行できるのを目標としているため、上図の点線枠内についてのみ実装対象としました。その他のサーバについては後日追加実装する予定です。
・実装
- RelaySvr
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
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
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
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で仕事したいです。
最後まで読んで頂き有難うございます。