147 lines
3.8 KiB
Elixir
147 lines
3.8 KiB
Elixir
defmodule Backend.GameRunner do
|
|
@moduledoc """
|
|
GenServer to track all players and their positions in the game.
|
|
Uses :global registry for distributed singleton pattern - only one instance
|
|
runs across the entire cluster, with automatic failover.
|
|
"""
|
|
use GenServer
|
|
require Logger
|
|
|
|
@name {:global, __MODULE__}
|
|
@pubsub Backend.PubSub
|
|
@topic "game_state"
|
|
@fps 30
|
|
|
|
# Client API
|
|
|
|
def start_link(_opts) do
|
|
case GenServer.start_link(__MODULE__, %{}, name: @name) do
|
|
{:ok, pid} ->
|
|
{:ok, pid}
|
|
|
|
{:error, {:already_started, pid}} ->
|
|
# Another instance is already running globally
|
|
:ignore
|
|
end
|
|
end
|
|
|
|
def add_player(player_id) do
|
|
Logger.info("Player #{player_id} connected")
|
|
GenServer.cast(@name, {:add_player, player_id})
|
|
end
|
|
|
|
def remove_player(player_id) do
|
|
Logger.info("Player #{player_id} removed")
|
|
GenServer.cast(@name, {:remove_player, player_id})
|
|
end
|
|
|
|
def move_player(player_id, directions) do
|
|
Logger.info("Player #{player_id} moved #{inspect(directions)}")
|
|
GenServer.cast(@name, {:move_player, player_id, directions})
|
|
end
|
|
|
|
def update_player_keys(player_id, keys_pressed) do
|
|
GenServer.cast(@name, {:update_player_keys, player_id, keys_pressed})
|
|
end
|
|
|
|
def get_state do
|
|
GenServer.call(@name, :get_state)
|
|
end
|
|
|
|
# Server Callbacks
|
|
|
|
@impl true
|
|
def init(_) do
|
|
Logger.info("GameState starting on node: #{node()}")
|
|
sleep_delay = round(1000 / @fps)
|
|
:timer.send_interval(sleep_delay, :tick)
|
|
|
|
{:ok, %{players: %{}, tick_number: 0}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_cast({:add_player, player_id}, _from, state) do
|
|
# Start players at random position
|
|
x = :rand.uniform(800)
|
|
y = :rand.uniform(600)
|
|
|
|
new_state = %{
|
|
state
|
|
| players: state.players |> Map.put(player_id, %{x: x, y: y, keys_pressed: []})
|
|
}
|
|
|
|
{:noreply, broadcast_state(new_state)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_cast({:update_player_keys, player_id, keys_pressed}, state) do
|
|
player =
|
|
case Map.get(state.players, player_id) do
|
|
nil ->
|
|
Logger.warning(
|
|
"Key update for non-existent player: #{player_id}, creating at random position"
|
|
)
|
|
|
|
%{x: :rand.uniform(800), y: :rand.uniform(600), keys_pressed: keys_pressed}
|
|
|
|
existing_player ->
|
|
Map.put(existing_player, :keys_pressed, keys_pressed)
|
|
end
|
|
|
|
new_state = %{state | players: Map.put(state.players, player_id, player)}
|
|
|
|
{:noreply, broadcast_state(new_state)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info(:tick, state) do
|
|
if rem(state.tick_number, 100) == 0 do
|
|
Logger.info("Game tick #{state.tick_number} on #{node()}, updating player positions")
|
|
end
|
|
|
|
step = 10
|
|
|
|
new_players =
|
|
state.players
|
|
|> Enum.map(fn {player_id, player} ->
|
|
directions = player.keys_pressed || []
|
|
|
|
new_player =
|
|
Enum.reduce(directions, player, fn direction, acc ->
|
|
case direction do
|
|
"w" -> %{acc | y: max(0, acc.y - step)}
|
|
"a" -> %{acc | x: max(0, acc.x - step)}
|
|
"s" -> %{acc | y: min(600, acc.y + step)}
|
|
"d" -> %{acc | x: min(800, acc.x + step)}
|
|
_ -> acc
|
|
end
|
|
end)
|
|
|
|
{player_id, new_player}
|
|
end)
|
|
|> Enum.into(%{})
|
|
|
|
new_state = %{state | players: new_players, tick_number: state.tick_number + 1}
|
|
|
|
{:noreply, broadcast_state(new_state)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:get_state, _from, state) do
|
|
{:reply, state, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_cast({:remove_player, player_id}, state) do
|
|
players = Map.delete(state.players, player_id)
|
|
new_state = %{state | players: players}
|
|
|
|
{:noreply, broadcast_state(new_state)}
|
|
end
|
|
|
|
defp broadcast_state(state) do
|
|
Phoenix.PubSub.broadcast(@pubsub, @topic, {:game_state_updated, state})
|
|
state
|
|
end
|
|
end
|