Skip to content

Commit

Permalink
Multiplayer: Make non-local entity updates possible (#730)
Browse files Browse the repository at this point in the history
This PR makes it possible to send a network message regarding a
non-locally simulated entity. This will be needed during e.g. health
updates (triggered by a locally simulated attacking entity).
  • Loading branch information
Indy2222 authored Sep 16, 2023
1 parent fbc9420 commit f233645
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 29 deletions.
21 changes: 14 additions & 7 deletions crates/messages/src/players/entity.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
#[cfg(feature = "bevy")]
use bevy::ecs::entity::Entity;
use bincode::{Decode, Encode};
use de_types::player::Player;

/// Bevy ECS Entity derived identification of an entity.
#[derive(Clone, Copy, Debug, Encode, Decode, Hash, PartialEq, Eq)]
pub struct EntityNet(u32);
pub struct EntityNet {
player: Player,
index: u32,
}

#[cfg(feature = "bevy")]
impl From<Entity> for EntityNet {
fn from(entity: Entity) -> Self {
Self(entity.index())
impl EntityNet {
/// # Arguments
///
/// * `player` - the human player executing the entity simulating game
/// instance.
///
/// * `index` - locally unique index of the entity.
pub fn new(player: Player, index: u32) -> Self {
Self { player, index }
}
}
2 changes: 1 addition & 1 deletion crates/multiplayer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub use crate::{
lifecycle::{MultiplayerShuttingDownEvent, ShutdownMultiplayerEvent, StartMultiplayerEvent},
messages::{MessagesSet, ToPlayersEvent},
netstate::NetState,
playermsg::{GameNetSet, NetRecvDespawnActiveEvent, NetRecvSpawnActiveEvent},
playermsg::{GameNetSet, NetEntities, NetRecvDespawnActiveEvent, NetRecvSpawnActiveEvent},
};
use crate::{netstate::NetStatePlugin, network::NetworkPlugin};

Expand Down
68 changes: 51 additions & 17 deletions crates/multiplayer/src/playermsg.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use ahash::AHashMap;
use bevy::prelude::*;
use de_core::{schedule::PreMovement, state::AppState};
use bevy::{ecs::system::SystemParam, prelude::*};
use de_core::{gconfig::GameConfig, schedule::PreMovement, state::AppState};
use de_messages::{EntityNet, ToPlayers};
use de_types::{objects::ActiveObjectType, player::Player};

Expand Down Expand Up @@ -96,39 +96,65 @@ impl NetRecvDespawnActiveEvent {
}
}

/// Mapping between remote and local entity IDs.
#[derive(SystemParam)]
pub struct NetEntities<'w> {
config: Res<'w, GameConfig>,
map: Res<'w, EntityIdMapRes>,
}

impl<'w> NetEntities<'w> {
/// Translates a local entity ID to a remote entity ID. This works for both
/// locally simulated and non-local entities.
///
/// It is assumed that the entity exists.
pub fn net_id(&self, entity: Entity) -> EntityNet {
match self.map.translate_local(entity) {
Some(id) => id,
None => self.local_net_id(entity),
}
}

/// Translates a local entity ID to a remote entity ID. This works only for
/// locally simulated entities.
///
/// It is assumed that the entity exists.
pub fn local_net_id(&self, entity: Entity) -> EntityNet {
let player = self.config.locals().playable();
EntityNet::new(player, entity.index())
}
}

/// Mapping between remote and local entity IDs for non-locally simulated
/// entities.
#[derive(Resource)]
struct EntityIdMapRes {
/// Associated player is not the player owning the entity but the only
/// human player co-located with the player of the entity. Thus the player
/// is either the same (if it is a human) or a different player (if the
/// owning player is AI).
remote_to_local: AHashMap<(Player, EntityNet), Entity>,
remote_to_local: AHashMap<EntityNet, Entity>,
local_to_remote: AHashMap<Entity, EntityNet>,
}

impl EntityIdMapRes {
fn new() -> Self {
Self {
remote_to_local: AHashMap::new(),
local_to_remote: AHashMap::new(),
}
}

/// Registers a new remote entity.
///
/// # Arguments
///
/// * `source` - human player executing the remote side (not necessarily
/// the player of the registered entity which may be AI simulated).
///
/// * `remote` - remote entity identification.
///
/// * `local` - local entity (present in the local ECS).
///
/// # Panics
///
/// Panics if the remote entity is already registered.
fn register(&mut self, source: Player, remote: EntityNet, local: Entity) {
let result = self.remote_to_local.insert((source, remote), local);
fn register(&mut self, remote: EntityNet, local: Entity) {
let result = self.remote_to_local.insert(remote, local);
assert!(result.is_none());
let result = self.local_to_remote.insert(local, remote);
assert!(result.is_none());
}

Expand All @@ -139,8 +165,16 @@ impl EntityIdMapRes {
/// # Panics
///
/// Panics if the entity is not registered.
fn deregister(&mut self, source: Player, remote: EntityNet) -> Entity {
self.remote_to_local.remove(&(source, remote)).unwrap()
fn deregister(&mut self, remote: EntityNet) -> Entity {
let local = self.remote_to_local.remove(&remote).unwrap();
self.local_to_remote.remove(&local).unwrap();
local
}

/// Translates local entity ID to a remote entity ID in case the entity is
/// not locally simulated.
fn translate_local(&self, local: Entity) -> Option<EntityNet> {
self.local_to_remote.get(&local).copied()
}
}

Expand Down Expand Up @@ -168,7 +202,7 @@ fn recv_messages(
transform,
} => {
let local = commands.spawn_empty().id();
map.register(input.source(), *entity, local);
map.register(*entity, local);

spawn_events.send(NetRecvSpawnActiveEvent::new(
*player,
Expand All @@ -178,7 +212,7 @@ fn recv_messages(
));
}
ToPlayers::Despawn { entity } => {
let local = map.deregister(input.source(), *entity);
let local = map.deregister(*entity);
despawn_events.send(NetRecvDespawnActiveEvent::new(local));
}
_ => (),
Expand Down
5 changes: 3 additions & 2 deletions crates/spawner/src/despawner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use de_core::{
state::AppState,
};
use de_messages::ToPlayers;
use de_multiplayer::{NetRecvDespawnActiveEvent, ToPlayersEvent};
use de_multiplayer::{NetEntities, NetRecvDespawnActiveEvent, ToPlayersEvent};
use de_objects::Health;
use de_types::objects::{ActiveObjectType, ObjectType};

Expand Down Expand Up @@ -77,6 +77,7 @@ fn find_dead(

fn despawn_active_local(
config: Res<GameConfig>,
net_entities: NetEntities,
mut event_reader: EventReader<DespawnActiveLocalEvent>,
mut event_writer: EventWriter<DespawnActiveEvent>,
mut net_events: EventWriter<ToPlayersEvent>,
Expand All @@ -86,7 +87,7 @@ fn despawn_active_local(

if config.multiplayer() {
net_events.send(ToPlayersEvent::new(ToPlayers::Despawn {
entity: event.0.into(),
entity: net_entities.local_net_id(event.0),
}));
}
}
Expand Down
5 changes: 3 additions & 2 deletions crates/spawner/src/spawner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use de_core::{
};
use de_energy::Battery;
use de_messages::ToPlayers;
use de_multiplayer::{NetRecvSpawnActiveEvent, ToPlayersEvent};
use de_multiplayer::{NetEntities, NetRecvSpawnActiveEvent, ToPlayersEvent};
use de_objects::{AssetCollection, InitialHealths, SceneType, Scenes, SolidObjects};
use de_pathing::{PathTarget, UpdateEntityPathEvent};
use de_terrain::{CircleMarker, MarkerVisibility, RectangleMarker};
Expand Down Expand Up @@ -141,6 +141,7 @@ impl SpawnEvent {
fn spawn_local_active(
mut commands: Commands,
config: Res<GameConfig>,
net_entities: NetEntities,
mut event_reader: EventReader<SpawnLocalActiveEvent>,
mut event_writer: EventWriter<SpawnActiveEvent>,
mut path_events: EventWriter<UpdateEntityPathEvent>,
Expand All @@ -167,7 +168,7 @@ fn spawn_local_active(

if config.multiplayer() {
net_events.send(ToPlayersEvent::new(ToPlayers::Spawn {
entity: entity.into(),
entity: net_entities.local_net_id(entity),
player: event.player,
object_type: event.object_type,
transform: event.transform.into(),
Expand Down

0 comments on commit f233645

Please sign in to comment.