Skip to content

Commit

Permalink
State deltas for game updates of players and projectiles (#888)
Browse files Browse the repository at this point in the history
* WIP state diffs

* Commented out logic for game state diff

* Formatting

* Adapt broadcast code for the possibility of missing fields

* Fix broadcast_game_update/2

* Wait for the match to be running to actually keep track of diffs

* Bring back category when serializing entity

* Commented code to only send part of diff

* Add optional to protobuf fields

* Diff for projectiles

* Only optional protobuf fields for player and projectiles

* Send delta for obstacles, remove delta for players and projectiles

* Send delta for bushes

* Make vertices diffable

* Send delta for crates

* Cleanup for PR and formatting

* Generate all protobufs

* Fix warning

* Restore bots when disconnected

* Add 1 number to arena's version

* Reduce comments in diff functions and create its issue

---------

Co-authored-by: agustinesco <[email protected]>
Co-authored-by: Nicolas Sanchez <[email protected]>
  • Loading branch information
3 people authored Sep 3, 2024
1 parent 8e3bb97 commit e18a904
Show file tree
Hide file tree
Showing 9 changed files with 1,596 additions and 647 deletions.
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
133 changes: 99 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,56 @@ 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: Since we don't know a way to calculate the diff of lists, we'll just handle
## specific cases or return always the new list.
## More info in -> https://github.com/lambdaclass/mirra_backend/issues/897
case {old, new} do
## 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

_ ->
{: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

0 comments on commit e18a904

Please sign in to comment.