Using GenServer to hold game state

I am planning to make a simple game using Phoenix LiveView. Originally I want to store game states directly into database but after watching Chris Mccord video I decided to use GenServer (I also want to practice and get used to OTP) to store the state.

My simple game flow:

  1. A user can create a game room, the created room will be visible in the home page (more like a lobby).
  2. When another use click that room, he will be added to the current participants in that room.
  3. The creator then starts the game when everyone is ready.

I get how GenServer works when it is not using the phoenix framework. I just run the GenServer module in the terminal, get the pid by pattern matching and perform GenServer calls.

In phoenix application I don’t understand how do each user (who joins a room) assign a pid that can be later be used to call GenServer functions.

If my application is not understandable, kindly tell me.

Thank you and hope somebody could help me.

You need named processes. Your room must have some id and users will join to the game using that id.
Read more here for detailed information and examples.

Maybe this is a fun source/talk to look at: https://daniel-azuma.com/articles/talks/elixirconf-2018

See the usage of registry here https://github.com/ElixirSeattle/tanx/blob/master/apps/tanx/lib/tanx/cluster.ex

The game/architecture could be a bit too complex (i.e. full cluster with hand-off when nodes go down) for your use case but still interesting to know about!

I would suggest building a Registry that keeps track of the room state gen_servers and use the via convention to call the assigned identifiers. Later, you can upgrade to a distributed system like horde or libcluster.

For something like a game session you usually need a combination of a GenServer (for holding and serializing state), the Registry (for finding the GenServer’s pid from an external id with via tuples), and a Dynamic Supervisor for spawning the GenServer game sessions as needed.

For the via tuple I like making a via/1 helper function in the GenServer module that we’re identifying:

# genserver module
def via(session_id) when is_binary(session_id) do
  {:via, Registry, {MyApp.MyGameContext.SessionRegistry, {__MODULE__, session_id}}}
end

Now we just generate an ID like a UUID 4 when we’re creating a new game session, then our web/front-end can send our game context messages with that id instead of knowing the PID which may change as our processes die/restart under dynamic supervision. The important thing is that your via tuple matches up with the Registry name you’re setting in the supervision tree.

The GenServer module might also expose an API that uses this via tuple and hides the OTP details of communicating like so:

def my_command(session_id, my_param) do
  GenServer.call(via(session_id), {:do_my_command, my_param})
end

You can see how I’ve wired up these sorts of features here for a live text analysis session feature, and here for a more classical business workflow state machine for how you could wire in persistence. Hope this helps.

6 Likes

Thank you so much.