with
Marten / Qqwy
multiple users/devices/browser tabs need to see & edit the same state in real-time
"collaborative editing"
'Vanilla' web-app
'Vanilla' web-app
'Vanilla' web-app
LiveView
handle events & render responses
interpret event
interpret response
Let's build a game!
GenServer?
Database?
Message Queue?
Based on Agar.io:
defmodule Agarex.Game.Server do
alias Agarex.Game.State
use GenServer
@fps 30
@tick_ms div(1000, @fps)
def start_link(_) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
@impl true
def init(_) do
Process.send_after(self(), :tick, @tick_ms)
{:ok, %{time: System.monotonic_time(), game_state: State.new()}}
end
# ...
@impl true
def handle_info(:tick, state) do
Process.send_after(self(), :tick, @tick_ms)
state = tick(state)
{:noreply, state}
end
defp tick(state) do
new_time = System.monotonic_time()
delta_time = new_time - state.time
game_state = State.tick(state.game_state, delta_time)
Phoenix.PubSub.broadcast(Agarex.PubSub, "game/tick", {"game/tick", game_state})
%{state | time: new_time, game_state: game_state}
end
end
(both in LiveView processes and elsewhere)
defmodule Agarex.Game.Server.State.World do
defstruct [:players, :agar]
def new() do
%__MODULE__{
players: Arrays.new(),
agar: Arrays.new(),
}
end
# ...
end
defmodule Agarex.Game.Server.State.World.Player do
defstruct [:name, :size, :position, :velocity, :alive?]
def new(name) do
%__MODULE__{
name: name,
size: 1,
position: {10, 20},
velocity: {0, 0},
alive?: true,
}
end
# ...
end
defmodule Agarex.Game.LocalState do
alias __MODULE__.Controls
defstruct [:controls, :game_state, :player_id]
def new(player_id) do
%__MODULE__{controls: Controls.new(), game_state: nil, player_id: player_id}
end
# ...
end
defmodule Agarex.Game.LocalState.Controls do
defstruct up: false, down: false, left: false, right: false
def new() do
%__MODULE__{}
end
def to_velocity(c) do
horizontal = bool_to_int(c.right) - bool_to_int(c.left)
vertical = bool_to_int(c.down) - bool_to_int(c.up)
{horizontal, vertical}
end
# ...
end
defmodule AgarexWeb.GameLive do
# ...
def handle_event(event, params, socket) do
do_handle_event(event, params, socket)
end
def handle_info({event, params}, socket) do
do_handle_event(event, params, socket)
end
defp do_handle_event(raw_event, params, socket) do
event = normalize_event(raw_event)
new_local = Agarex.Game.LocalState.handle_event(event, params, socket.assigns.local)
socket = assign(socket, :local, new_local)
{:noreply, socket}
end
defp normalize_event(event) do
String.split(event, "/")
end
end
defmodule Agarex.Game.LocalState do
# ...
def handle_event(event, params, struct) do
case event do
["controls" | rest] ->
struct = update_in(struct.controls, &Controls.handle_event(rest, params, &1))
Controls.broadcast_velocity(struct.controls, struct.player_id)
["game" , "tick"] ->
put_in(struct.game_state, params)
end
end
end
Routing, outside-in
defmodule Agarex.Game.LocalState.Controls do
# ...
def handle_event(event, params, struct) do
case event do
["key_up"] ->
key_up(struct, params["key"])
["key_down"] ->
key_down(struct, params["key"])
end
end
# ...
end
Goal: maintainability
defmodule Agarex.Effect do
def effectful_update_in(struct, access_list, function) do
{result, effects} =
struct
|> Access.get(access_list)
|> function.()
struct = put_in(struct, access_list, result)
{struct, effects}
end
# arity-2 variant elided
end
defmodule Agarex.Effect.BroadcastPlayerMovement do
defstruct [:player_id, :controls]
def new(player_id, controls) do
%__MODULE__{player_id: player_id, controls: controls}
end
end
defmodule Agarex.Game.LocalState do
# ...
def handle_event(event, params, struct) do
case event do
["controls" | rest] ->
struct = update_in(struct.controls, &Controls.handle_event(rest, params, &1))
Controls.broadcast_velocity(struct.controls, struct.player_id)
["game" , "tick"] ->
put_in(struct.game_state, params)
end
end
end
# ...
end
defmodule Agarex.Game.LocalState do
# ...
def handle_event(event, params, struct) do
case event do
["controls" | rest] ->
{struct, effects} =
Effect.effectful_update_in(struct.controls, &Controls.handle_event(rest, params, &1))
{
struct,
[Effect.BroadcastPlayerVelocity.new(struct.controls, struct.player_id)] ++ effects
}
["game" , "tick"] ->
struct = put_in(struct.game_state, params)
{struct, []}
end
end
end
# ...
end
module AgarexWeb.EffectInterpreter do
def run(effect, socket) do
case effect do
%Agarex.Effect.BroadcastPlayerVelocity{} ->
Phoenix.PubSub.broadcast(
#...
)
end
end
end
"Can we visualize software complexity?", Rafał Studnicki!
which are too much to cover too this presentation
Time for questions