・目次
サーバ編
-
第0章 Hello! After World!! - 初心者がelixirでオンラインゲーム製作に挑戦してみた(2018/12/21公開)
-
第1章 チャットの実装(2019/1/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 |
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で接続します。
・ダイレクトチャットの場合
- 各クライアントのNo.1をRelaySvrに送信する。
- クライアント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
グループチャットの場合
- 各クライアントのNo.1をRelaySvr送信する。
- クライアント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
範囲チャットの場合
- 各クライアントのNo.1をRelaySvrに送信する。
- クライアント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サーバ実装した後、本機能と連携させログ保存を実装予定です。
最後まで読んでいただきありがとうございました。