This repository contains a minimal Phoenix application used for experimenting with a simple multiplayer server.
- Elixir 1.15 and Erlang/OTP 26 or later
- PostgreSQL (Docker Compose is provided for convenience)
-
Start PostgreSQL using Docker Compose:
cd mmo_server docker-compose up -dThis will launch a local PostgreSQL instance listening on
localhost:5432with the default credentials defined indocker-compose.yml. -
Fetch Elixir dependencies:
mix deps.get
-
Create the development database (if it does not already exist) and load the development seed data:
mix ecto.create mix ecto.migrate mix seed # or: mix run priv/repo/seeds.exs -
Run the application:
mix phx.server
The API will be available on `http://localhost:4001`.
## Running Two Terminals
To experiment with a simple multi-node setup, open two terminal windows in the
`mmo_server` directory and run:
```bash
# Terminal 1
iex --sname node1 -S mix phx.server
# Terminal 2
MIX_ENV=test iex --sname node2 -S mix
The sandbox runs in manual mode so each case checks out a shared connection before spawning processes. Tests therefore run serially.
mix testThe production config expects DATABASE_URL, POOL_SIZE and SECRET_KEY_BASE environment variables. See config/runtime.exs for details.
To verify the phase 1 functionality, open two terminal windows and run two UDP clients. Each client sends move packets and you should see both players' movements in the server logs.
-
Start the server:
cd mmo_server mix deps.get mix phx.server -
In another terminal send a move packet. Example using the Erlang shell:
{ok, S} = gen_udp:open(0, []), Packet = <<1:32, 1:16, 1.0:32/float, 0.0:32/float, 0.0:32/float>>, gen_udp:send(S, {127,0,0,1}, 4000, Packet). -
Repeat with a different
player_idfrom a second terminal. The log output will display position updates for both players.
Start the Phoenix server:
cd mmo_server
mix phx.serverVisit http://localhost:4001/players to view live player positions.
From iex -S mix you can print all positions using:
MmoServer.CLI.LivePlayerTracker.print_all_positions()After launching the Phoenix server from the mmo_server directory, you can manually create zones and players in an interactive shell. Start iex alongside the server and then run:
iex -S mix phx.server
{:ok, _zone} =
DynamicSupervisor.start_child(MmoServer.ZoneSupervisor,
{MmoServer.Zone, "zone1"})
{:ok, _player} =
DynamicSupervisor.start_child(MmoServer.PlayerSupervisor,
{MmoServer.Player, %{player_id: "player3", zone_id: "zone1"}})Check player positions with:
MmoServer.Player.get_position("player1")Move player1 to a new position:
MmoServer.Player.move("player1", {1.0, 2.0, 3.0})get full zone state:
MmoServer.Quests.get_progress("player1", MmoServer.Quests.wolf_kill_id())These commands must be executed from the mmo_server directory after the server has started.
Chat messages are delivered over Phoenix Channels. Clients connect to
ws://localhost:4001/socket/websocket and join one or more topics:
"chat:global"– global chat for everyone"chat:zone:<zone_id>"– messages scoped to a zone"chat:whisper:<player_id>"– private 1:1 chat
To run the sample below you need the phoenix_client
library. The server does not depend on this package, so create a small
client project and add it as a dependency:
mix new chat_client
cd chat_client
# mix.exs
defp deps do
[
{:phoenix_client, "~> 0.1"}
]
end
mix deps.get
iex -S mixOnce the dependency is available you can connect to the running server:
# Join the channel and send a message (Elixir client example)
{:ok, socket} = PhoenixClient.Socket.start_link(url: "ws://localhost:4001/socket/websocket")
{:ok, _, chan} = PhoenixClient.Channel.join(socket, "chat:global")
PhoenixClient.Channel.push(chan, "message", %{
"from" => "player1",
"to" => "chat:global",
"text" => "Hello!"
})Unity clients can use similar logic via a WebSocket library:
var socket = new Websocket("ws://localhost:4001/socket/websocket");
socket.Connect();
socket.Join("chat:zone:elwynn");
socket.Push("message", new { from = "player1", to = "chat:zone:elwynn", text = "Hi" });You can also broadcast messages without a channel using:
Phoenix.PubSub.broadcast(MmoServer.PubSub, "chat:zone:elwynn", {:chat_msg, "gm", "Server restart soon"})Run this script in an IEx session to simulate player2 joining zone1 and moving along the X axis:
alias Phoenix.PubSub
alias MmoServerWeb.Endpoint
player_id = "player2"
zone_id = "zone1"
# Initial position
position = %{x: 5.0, y: 0.0, z: 2.0}
# Simulate player joining
join_event = %{
event: "player_joined",
payload: %{
id: player_id,
position: position
}
}
Phoenix.PubSub.broadcast(MmoServer.PubSub, "zone:#{zone_id}", join_event)
IO.puts("✅ Simulated player2 joining #{zone_id}")
# Simulate movement loop
spawn(fn ->
:timer.sleep(1000)
for _i <- 1..5 do
:timer.sleep(1000)
delta = %{x: 1.0, y: 0.0, z: 0.0}
move_event = %{
event: "player_moved",
payload: %{
id: player_id,
delta: delta
}
}
Phoenix.PubSub.broadcast(MmoServer.PubSub, "zone:#{zone_id}", move_event)
IO.puts("🚶 player2 moved +1 on X axis")
end
end)