Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

State deltas for game updates of players and projectiles #888

Merged
merged 22 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 36 additions & 36 deletions apps/arena/lib/arena/entities.ex
Original file line number Diff line number Diff line change
Expand Up @@ -377,82 +377,82 @@ defmodule Arena.Entities do
def maybe_add_custom_info(entity) when entity.category == :player do
{:player,
%Arena.Serialization.Player{
health: entity.aditional_info.health,
current_actions: entity.aditional_info.current_actions,
kill_count: entity.aditional_info.kill_count,
available_stamina: entity.aditional_info.available_stamina,
max_stamina: entity.aditional_info.max_stamina,
stamina_interval: entity.aditional_info.stamina_interval,
recharging_stamina: entity.aditional_info.recharging_stamina,
character_name: entity.aditional_info.character_name,
effects: entity.aditional_info.effects,
power_ups: entity.aditional_info.power_ups,
inventory: entity.aditional_info.inventory,
cooldowns: entity.aditional_info.cooldowns,
visible_players: entity.aditional_info.visible_players,
on_bush: entity.aditional_info.on_bush,
forced_movement: entity.aditional_info.forced_movement,
bounty_completed: entity.aditional_info.bounty_completed,
mana: entity.aditional_info.mana,
current_basic_animation: entity.aditional_info.current_basic_animation
health: get_in(entity, [:aditional_info, :health]),
current_actions: get_in(entity, [:aditional_info, :current_actions]),
kill_count: get_in(entity, [:aditional_info, :kill_count]),
available_stamina: get_in(entity, [:aditional_info, :available_stamina]),
max_stamina: get_in(entity, [:aditional_info, :max_stamina]),
stamina_interval: get_in(entity, [:aditional_info, :stamina_interval]),
recharging_stamina: get_in(entity, [:aditional_info, :recharging_stamina]),
character_name: get_in(entity, [:aditional_info, :character_name]),
effects: get_in(entity, [:aditional_info, :effects]),
power_ups: get_in(entity, [:aditional_info, :power_ups]),
inventory: get_in(entity, [:aditional_info, :inventory]),
cooldowns: get_in(entity, [:aditional_info, :cooldowns]),
visible_players: get_in(entity, [:aditional_info, :visible_players]),
on_bush: get_in(entity, [:aditional_info, :on_bush]),
forced_movement: get_in(entity, [:aditional_info, :forced_movement]),
bounty_completed: get_in(entity, [:aditional_info, :bounty_completed]),
mana: get_in(entity, [:aditional_info, :mana]),
current_basic_animation: get_in(entity, [:aditional_info, :current_basic_animation])
}}
end

def maybe_add_custom_info(entity) when entity.category == :projectile do
{:projectile,
%Arena.Serialization.Projectile{
damage: entity.aditional_info.damage,
owner_id: entity.aditional_info.owner_id,
status: entity.aditional_info.status,
skill_key: entity.aditional_info.skill_key
damage: get_in(entity, [:aditional_info, :damage]),
owner_id: get_in(entity, [:aditional_info, :owner_id]),
status: get_in(entity, [:aditional_info, :status]),
skill_key: get_in(entity, [:aditional_info, :skill_key])
}}
end

def maybe_add_custom_info(entity) when entity.category == :power_up do
{:power_up,
%Arena.Serialization.PowerUp{
owner_id: entity.aditional_info.owner_id,
status: entity.aditional_info.status
owner_id: get_in(entity, [:aditional_info, :owner_id]),
status: get_in(entity, [:aditional_info, :status])
}}
end

def maybe_add_custom_info(entity) when entity.category == :obstacle do
{:obstacle,
%Arena.Serialization.Obstacle{
color: "red",
collisionable: entity.aditional_info.collisionable,
status: entity.aditional_info.status,
type: entity.aditional_info.type
collisionable: get_in(entity, [:aditional_info, :collisionable]),
status: get_in(entity, [:aditional_info, :status]),
type: get_in(entity, [:aditional_info, :type])
}}
end

def maybe_add_custom_info(entity) when entity.category == :pool do
{:pool,
%Arena.Serialization.Pool{
owner_id: entity.aditional_info.owner_id,
status: entity.aditional_info.status,
effects: entity.aditional_info.effects,
skill_key: entity.aditional_info.skill_key
owner_id: get_in(entity, [:aditional_info, :owner_id]),
status: get_in(entity, [:aditional_info, :status]),
effects: get_in(entity, [:aditional_info, :effects]),
skill_key: get_in(entity, [:aditional_info, :skill_key])
}}
end

def maybe_add_custom_info(entity) when entity.category == :item do
{:item,
%Arena.Serialization.Item{
name: entity.aditional_info.name
name: get_in(entity, [:aditional_info, :name])
}}
end

def maybe_add_custom_info(entity) when entity.category == :crate do
{:crate,
%Arena.Serialization.Crate{
health: entity.aditional_info.health,
amount_of_power_ups: entity.aditional_info.amount_of_power_ups,
status: entity.aditional_info.status
health: get_in(entity, [:aditional_info, :health]),
amount_of_power_ups: get_in(entity, [:aditional_info, :amount_of_power_ups]),
status: get_in(entity, [:aditional_info, :status])
}}
end

def maybe_add_custom_info(_entity) do
def maybe_add_custom_info(entity) when entity.category in [:bush, :trap] do
nil
end

Expand Down
14 changes: 0 additions & 14 deletions apps/arena/lib/arena/game_socket_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ defmodule Arena.GameSocketHandler do
require Logger
alias Arena.Authentication.GatewaySigner
alias Arena.Authentication.GatewayTokenManager
alias Arena.Utils
alias Arena.Serialization
alias Arena.GameUpdater
alias Arena.Serialization.GameEvent
Expand Down Expand Up @@ -169,19 +168,6 @@ defmodule Arena.GameSocketHandler do
end

@impl true
def terminate(_reason, _req, %{game_finished: false, player_alive: true} = state) do
:telemetry.execute([:arena, :clients], %{count: -1})

if Application.get_env(:arena, :spawn_bots) do
spawn(fn ->
Finch.build(:get, Utils.get_bot_connection_url(state.game_id, state.client_id))
|> Finch.request(Arena.Finch)
end)
end

:ok
end

def terminate(_reason, _req, _state) do
:telemetry.execute([:arena, :clients], %{count: -1})
:ok
Expand Down
157 changes: 123 additions & 34 deletions apps/arena/lib/arena/game_updater.ex
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ defmodule Arena.GameUpdater do
bot_clients: bot_clients_ids,
game_config: game_config,
bounties_enabled?: bounties_enabled?,
game_state: game_state
game_state: game_state,
last_broadcasted_game_state: %{}
}}
end

Expand Down Expand Up @@ -286,12 +287,28 @@ defmodule Arena.GameUpdater do
# Obstacles
|> handle_obstacles_transitions()

broadcast_game_update(game_state)
{:ok, state_diff} = diff(state.last_broadcasted_game_state, game_state)

state_diff =
Map.put(game_state, :obstacles, state_diff[:obstacles])
|> Map.put(:bushes, state_diff[:bushes])
|> Map.put(:crates, state_diff[:crates])

broadcast_game_update(state_diff, game_state.game_id)

## We need this check cause there is some unexpected behaviour from the client
## when we start sending deltas before the game state changes to RUNNING
last_broadcasted_game_state =
case get_in(state, [:game_state, :status]) do
:RUNNING -> game_state
_ -> %{}
end

game_state = %{game_state | killfeed: [], damage_taken: %{}, damage_done: %{}}

tick_duration = System.monotonic_time() - tick_duration_start_at
:telemetry.execute([:arena, :game, :tick], %{duration: tick_duration, duration_measure: tick_duration})
{:noreply, %{state | game_state: game_state}}
{:noreply, %{state | game_state: game_state, last_broadcasted_game_state: last_broadcasted_game_state}}
end

def handle_info(:send_ping, state) do
Expand Down Expand Up @@ -684,35 +701,28 @@ defmodule Arena.GameUpdater do
PubSub.broadcast(Arena.PubSub, game_id, :enable_incomming_messages)
end

defp broadcast_game_update(state) do
defp broadcast_game_update(state, game_id) do
game_state = struct(GameState, state)

encoded_state =
GameEvent.encode(%GameEvent{
event:
{:update,
%GameState{
game_id: state.game_id,
players: complete_entities(state.players),
projectiles: complete_entities(state.projectiles),
power_ups: complete_entities(state.power_ups),
pools: complete_entities(state.pools),
bushes: complete_entities(state.bushes),
items: complete_entities(state.items),
server_timestamp: state.server_timestamp,
player_timestamps: state.player_timestamps,
zone: state.zone,
killfeed: state.killfeed,
damage_taken: state.damage_taken,
damage_done: state.damage_done,
status: state.status,
start_game_timestamp: state.start_game_timestamp,
obstacles: complete_entities(state.obstacles),
crates: complete_entities(state.crates),
traps: complete_entities(state.traps),
external_wall: complete_entity(state.external_wall)
}}
Map.merge(game_state, %{
players: complete_entities(state[:players], :player),
projectiles: complete_entities(state[:projectiles], :projectile),
power_ups: complete_entities(state[:power_ups], :power_up),
pools: complete_entities(state[:pools], :pool),
bushes: complete_entities(state[:bushes], :bush),
items: complete_entities(state[:items], :item),
obstacles: complete_entities(state[:obstacles], :obstacle),
crates: complete_entities(state[:crates], :crate),
traps: complete_entities(state[:traps], :trap),
external_wall: complete_entity(state[:external_wall], :obstacle)
})}
})

PubSub.broadcast(Arena.PubSub, state.game_id, {:game_update, encoded_state})
PubSub.broadcast(Arena.PubSub, game_id, {:game_update, encoded_state})
end

defp broadcast_ping(state) do
Expand All @@ -728,27 +738,32 @@ defmodule Arena.GameUpdater do

defp broadcast_game_ended(winner, state) do
game_state = %GameFinished{
winner: complete_entity(winner),
players: complete_entities(state.players)
winner: complete_entity(winner, :player),
players: complete_entities(state.players, :player)
}

encoded_state = GameEvent.encode(%GameEvent{event: {:finished, game_state}})
PubSub.broadcast(Arena.PubSub, state.game_id, {:game_finished, encoded_state})
end

defp complete_entities(entities) do
defp complete_entities(nil, _), do: []

defp complete_entities(entities, category) do
entities
|> Enum.reduce(%{}, fn {entity_id, entity}, entities ->
entity = complete_entity(entity)
entity = complete_entity(entity, category)

Map.put(entities, entity_id, entity)
end)
end

defp complete_entity(entity) do
Map.put(entity, :category, to_string(entity.category))
|> Map.put(:shape, to_string(entity.shape))
|> Map.put(:aditional_info, entity |> Entities.maybe_add_custom_info())
defp complete_entity(nil, _), do: nil

defp complete_entity(entity, category) do
Map.update(entity, :category, nil, &to_string/1)
|> Map.update(:shape, nil, &to_string/1)
|> Map.update(:vertices, nil, fn vertices -> %{positions: vertices} end)
|> Map.put(:aditional_info, Entities.maybe_add_custom_info(Map.put(entity, :category, category)))
end

##########################
Expand Down Expand Up @@ -1874,6 +1889,80 @@ defmodule Arena.GameUpdater do
end)
end

@spec diff(t, t) :: :no_diff | {:ok, t} when t: any()
def diff(old, new) when is_map(old) and is_map(new) do
value =
Enum.reduce(new, %{}, fn {key, new_value}, acc ->
case Map.has_key?(old, key) do
true ->
case diff(Map.get(old, key), new_value) do
:no_diff -> acc
{:ok, value_diff} -> Map.put(acc, key, value_diff)
end

false ->
Map.put(acc, key, new_value)
end
end)

case map_size(value) do
0 -> :no_diff
_ -> {:ok, value}
end
end

def diff(old, new) when is_list(old) and is_list(new) do
## TODO: Figure out if there is a way to calculate the diff of lists
## so far this involves figuring out both was is there and what is not there which is simple,
## but how do you differentiate between a thing that hasn't changed and a thing that was removed?
## For example this lists [1, 5, 8] and [1, 2, 6, 7, 8], what is the diff?
## - 2, 6, 7 were added
## - 5 was removed
## - 1, 8 were not changed
## So, how do we give back this information? Because we have this representations
## - [2, 6, 7, 5] this represents all changes, but how do you know 5 was removed?
## - [2, 6, 7] this is only new things, but then how do we know 5 was removed?
##
## To complicate things further what about ordering? Is [1,2,3] and [3,2,1] the same list?
## Since I don't have answers to these questions and the amount of lists we send back that could benefit
## from a diff is a couple, I'll think returning the `new` one for now is good enough

## Following somen discussions we are going to have special handling for certain cases and essentially
## check the lists for exact matches (returning :no_diff) or for differences (returning the entire new list)
case {old, new} do
# ## TODO: to enable this we need a fix similar to the ListPositionPB in protobuf for the other fields
# ## Lists of the same scalars we can compare directly. One assumption we currently do here is that lists
# ## are homogeneous, all elements are of same type (they should, but FYI)
# {[elem | _], [elem | _]} when is_atom(elem) or is_binary(elem) or is_boolean(elem) or is_number(elem) ->
# case old === new do
# true -> :no_diff
# false -> {:ok, new}
# end

## Lists containing %{x: _, y: _} are treated as points (vertices) and this case we know we can
## do ===/2 comparison and it will verify the exactness. At the moment we don't want to do this
## for all lists of maps cause the exactness of this comparison of maps hasn't been
## verified (is it a deep === comparison for all keys and values?) and we don't know the performance impact
{[%{x: _, y: _} | _], [%{x: _, y: _} | _]} ->
case old === new do
true -> :no_diff
false -> {:ok, new}
end

## Anything else we still consider to risky to try and compare, so we return the new list
_ ->
{:ok, new}
end
end

## At this point only simple values remain so a normal comparisson is enough
def diff(old, new) do
case old == new do
true -> :no_diff
false -> {:ok, new}
end
end

##########################
# End Helpers
##########################
Expand Down
Loading
Loading