Phoenix LiveView
Multiplayer Games & Collaborative editing
with

Two use-cases for LiveView
- 'snippets' (small self-contained components)
- Search bar with suggestions
- Sortable tables
- Updating some elements real-time
- Single Page Applications (SPAs)
- Manage the whole interface
Goals
- Go beyond the 'Hello World' / 'TodoMVC' / 'Chat'
- What happens when a LiveView SPA grows? How to keep:
- comprehensible
- maintainable
- extensible
- What happens when a LiveView SPA grows? How to keep:
- Answer/contextualize common LiveView usage question
→ Build a game!
Why do I talk about this?


Marten / Qqwy
LiveView-related projects
- Planga: Chat Service (Phoenix Channels)
- Powerchainger: Smart Energy measurement device (Nerves, LiveView)
- CyanView: Live video stream color-correction device (LiveView + MQTT)







When use LiveView for an SPA?
multiple users/devices/browser tabs need to see & edit the same state in real-time
"collaborative editing"
What about games?
- Multiple players manipulating the same 'world state'
Global Game Jam
- 48hrs
- Elixir/Phoenix Channels for controls,
- Unity for graphics on the TV screen
What does LiveView do?
Browser
-
connect to server
- send events
-
listen for responses
- interpret/render responses
-
mutate DOM
Server
-
listen for events
-
manage sessions
- handle events
-
send responses
'Vanilla' web-app
Browser
-
connect to server
- send events
-
listen for responses
- interpret/render responses
-
mutate DOM
- handle disconnects/reconnects
- handle error responses
Server
-
listen for events
-
manage sessions
- handle events
-
send responses
- handle disconnects/reconnects
- handle malformed events
'Vanilla' web-app
Browser
-
connect to server
- send events
-
listen for responses
- interpret/render responses
-
mutate DOM
- handle disconnects/reconnects
- handle error responses
Server
-
listen for events
-
manage sessions
- handle events
-
send responses
- handle disconnects/reconnects
- handle malformed events
'Vanilla' web-app
Browser
-
connect to server
send events-
listen for responses
-
mutate DOM
handle disconnects/reconnectshandle error responses
Server
-
listen for events
-
manage sessions
- handle events
send responses-
interpret/render responses
handle disconnects/reconnectshandle malformed events
LiveView
- (simplified) Functional Reactive Programming
- 'The Elm Architecture'
- Essentially what all GenServers do
- 'The Elm Architecture'
handle events & render responses
interpret event
interpret response
- 95% of JavaScript & communication logic abstracted away
- structured event handling
- testable
- matches other OTP processes
What does LiveView do?
LiveView's limits?
- It requires good connectivity
- Not offline
- Not high latency
front-end Elixir code is being worked on, c.f. Lumen
- Drive UI that does not work with DOM-diffing
- Animations (use CSS)
- Canvas, WebGL, etc.
- Some things still missing, it's still in beta
multi-user LiveView architecture
Let's build a game!




GenServer?
Database?
Message Queue?
Game: AgarEx
Based on Agar.io:
- Players are cells
- realtime movement
- eating 'agar' & 'smaller' players to become bigger
- size before being eaten = score
- rounds reset every couple minutes
Game Server
- receive player messages using call/cast elided below
- send updated state using Phoenix.PubSub
- update game state 30 times per second
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
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
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
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
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
Managing State
(both in LiveView processes and elsewhere)
- nested tree of structs ('product & sum types')
- nested event handling
- separate state changing from (side-)effects
1. Nested Tree of Structs
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
1. Nested Tree of Structs
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
2. Nested Event Handling
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 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 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 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 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 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
2. Nested Event Handling
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
- "controls/key_up"
- "controls/key_down"
- "game/tick"
- etc.
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
3. Separate state from effects
Goal: maintainability
- Rather than performing side-effect immediately, build declarative (symbolic) description(s)...
- ... and execute at the fringe
- When (nested) handling events,
return `{state, [list, of, effects]}`
3. Separate state from effects
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 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
3. Separate state from effects
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
3. Separate state from effects
module AgarexWeb.EffectInterpreter do def run(effect, socket) do case effect do %Agarex.Effect.BroadcastPlayerVelocity{} -> Phoenix.PubSub.broadcast( #... ) end end end
- Plain data, no protocol:
- Use a different interpreter for tests
- Very explicit what happens during state changes
- Very flexible in where/what/how effects are handled
3. Separate state from effects
(4. Contracts & property testing)
- Contracts: Prevent front-end from sending non-recognized events
- Property Testing: Check whether state can handle any (sequence of) valid event(s) without crashing
- C.f. the TypeCheck library https://hex.pm/packages/type_check
Stateful vs Stateless?
- Stateless: DRY, different levels of detail
- Stateful: Abstract details
examples:- keypresses → player movement
- form validations → submitted values
LiveView vs LiveComponent?
- LiveView = extra process:
- Fault-tolerance
- separate server-side events
- LiveComponent:
- everything else

"Can we visualize software complexity?", Rafał Studnicki!
Other things in AgarEx
which are too much to cover too this presentation
- Rendering using SVG + CSS animations
- More gamedev-specific tricks (2D collisions, etc)
Other Resources
- Be sure to read the full LiveView documentation!
- upcoming LiveView book by Bruce Tate
- https://pragmaticstudio.com/phoenix-liveview (paid video guides)
Thank you!
Time for questions


Phoenix LiveView Multiplayer Games & Collaborative editing with
Multiplayer Games & Collaborative Editing with Phoenix LiveView
By qqwy
Multiplayer Games & Collaborative Editing with Phoenix LiveView
- 1,447