Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Rename `client_event_registry` module into `registry`.
- Replace "event" with "message" and "trigger" with "event" in types, methods and modules according to the new naming in Bevy.
- Split `RemoteMessageRegistry::*_channel` methods for messages and events into dedicated methods for each type.

### Removed

- `trigger_*_targets`. Targets in Bevy now stored inside events. When you move entities to your events, dont't forget to use `register_*_event_mapped` in order to properly map them.

### Added

Expand Down
16 changes: 7 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ all-features = true
members = ["bevy_replicon_example_backend"]

[dependencies]
bevy = { version = "0.16.0", default-features = false, features = [
"bevy_state",
] }
bevy = { version = "0.17", default-features = false, features = ["bevy_state"] }
log = "0.4" # Directly depend on `log` like other `no_std` Bevy crates, since `bevy_log` currently requires `std`.
petgraph = { version = "0.8", default-features = false, features = [
"stable_graph",
Expand All @@ -52,9 +50,9 @@ bytes = { version = "1.10", default-features = false, features = [
] }

[dev-dependencies]
bevy = { version = "0.16.0", default-features = false, features = [
bevy = { version = "0.17", default-features = false, features = [
"bevy_log",
"bevy_sprite",
"bevy_sprite_render",
"serialize",
] }
test-log = "0.2"
Expand Down Expand Up @@ -90,11 +88,11 @@ name = "mutations"
required-features = ["client", "server"]

[[test]]
name = "client_event"
name = "client_message"
required-features = ["client", "server"]

[[test]]
name = "client_trigger"
name = "client_event"
required-features = ["client", "server"]

[[test]]
Expand Down Expand Up @@ -122,11 +120,11 @@ name = "scene"
required-features = ["scene"]

[[test]]
name = "server_event"
name = "server_message"
required-features = ["client", "server"]

[[test]]
name = "server_trigger"
name = "server_event"
required-features = ["client", "server"]

[[test]]
Expand Down
7 changes: 4 additions & 3 deletions bevy_replicon_example_backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,21 @@ license = "MIT OR Apache-2.0"
include = ["/src", "../LICENSE*"]

[dependencies]
bevy = { version = "0.16.0", default-features = false, features = ["bevy_log"] }
bevy = { version = "0.17", default-features = false, features = ["bevy_log"] }
bevy_replicon = { path = "..", version = "0.35.0", default-features = false }
fastrand = "2.3"

[dev-dependencies]
bevy = { version = "0.16.0", default-features = false, features = [
bevy = { version = "0.17", default-features = false, features = [
"bevy_gizmos",
"bevy_text",
"bevy_ui_picking_backend",
"bevy_ui",
"bevy_ui_render",
"bevy_window",
"default_font",
"serialize",
"x11",
"zstd_rust",
] }
clap = { version = "4.1", features = ["derive"] }
fastrand-contrib = "0.1"
Expand Down
100 changes: 49 additions & 51 deletions bevy_replicon_example_backend/examples/authoritative_rts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ use std::{
};

use bevy::{
camera::primitives::Aabb,
color::palettes::tailwind::{
BLUE_500, GREEN_500, LIME_500, ORANGE_500, PINK_500, PURPLE_500, RED_500, TEAL_500,
YELLOW_500,
},
ecs::entity::MapEntities,
platform::collections::HashMap,
prelude::*,
render::primitives::Aabb,
};
use bevy_replicon::prelude::*;
use bevy_replicon_example_backend::{ExampleClient, ExampleServer, RepliconExampleBackendPlugins};
Expand All @@ -44,9 +44,9 @@ fn main() {
.replicate::<Unit>()
.replicate::<Command>()
.replicate_as::<Transform, Transform2DWithoutScale>()
.add_client_trigger::<TeamRequest>(Channel::Unordered)
.add_client_trigger::<UnitSpawn>(Channel::Unordered)
.add_mapped_client_trigger::<UnitsMove>(Channel::Unordered)
.add_client_event::<TeamRequest>(Channel::Unordered)
.add_client_event::<UnitSpawn>(Channel::Unordered)
.add_mapped_client_event::<MoveUnits>(Channel::Unordered)
.add_observer(apply_team_request)
.add_observer(trigger_unit_spawn)
.add_observer(apply_unit_spawn)
Expand Down Expand Up @@ -131,29 +131,29 @@ fn trigger_team_request(mut commands: Commands, team: Res<Team>) {

/// Assigns a team to a player.
fn apply_team_request(
trigger: Trigger<FromClient<TeamRequest>>,
mut events: EventWriter<DisconnectRequest>,
team_request: On<FromClient<TeamRequest>>,
mut disconnects: MessageWriter<DisconnectRequest>,
mut teams: ResMut<ClientTeams>,
) {
if let Some((client_id, team)) = teams.iter().find(|&(_, team)| *team == trigger.team) {
if let Some((client_id, team)) = teams.iter().find(|&(_, team)| *team == team_request.team) {
error!(
"`{}` requested team `{team:?}`, but it's already taken by `{client_id}`",
trigger.client_id,
team_request.client_id,
);
let client = trigger
let client = team_request
.client_id
.entity()
.expect("server can't request an invalid team");
events.write(DisconnectRequest { client });
disconnects.write(DisconnectRequest { client });
return;
}

info!(
"associating `{}` with team `{:?}`",
trigger.client_id, trigger.team
team_request.client_id, team_request.team
);

teams.insert(trigger.client_id, trigger.team);
teams.insert(team_request.client_id, team_request.team);
}

/// Requests spawning a unit at the click location.
Expand All @@ -165,16 +165,16 @@ fn apply_team_request(
/// This also makes the logic independent of camera position - even though in
/// this demo the camera cannot move.
fn trigger_unit_spawn(
trigger: Trigger<Pointer<Pressed>>,
press: On<Pointer<Press>>,
mut commands: Commands,
camera: Single<(&Camera, &GlobalTransform)>,
) -> Result<()> {
if trigger.button != PointerButton::Middle {
if press.button != PointerButton::Middle {
return Ok(());
}

let (camera, transform) = *camera;
let position = camera.viewport_to_world_2d(transform, trigger.pointer_location.position)?;
let position = camera.viewport_to_world_2d(transform, press.pointer_location.position)?;

commands.client_trigger(UnitSpawn { position });

Expand All @@ -186,21 +186,21 @@ fn trigger_unit_spawn(
/// Executed on server and singleplayer.
/// The unit will be replicated back to clients.
fn apply_unit_spawn(
trigger: Trigger<FromClient<UnitSpawn>>,
spawn: On<FromClient<UnitSpawn>>,
mut commands: Commands,
teams: Res<ClientTeams>,
) {
let Some(&team) = teams.get(&trigger.client_id) else {
let Some(&team) = teams.get(&spawn.client_id) else {
error!(
"`{}` attempted to spawn a unit but has no team",
trigger.client_id
spawn.client_id
);
return;
};

commands.spawn((
Unit { team },
Transform::from_translation(trigger.position.extend(0.0)),
Transform::from_translation(spawn.position.extend(0.0)),
));
}

Expand All @@ -215,12 +215,12 @@ fn apply_unit_spawn(
/// Works for initialization on both the server (when a unit
/// is spawned) and the client (when the unit is replicated).
fn init_unit(
trigger: Trigger<OnInsert, Unit>,
insert: On<Insert, Unit>,
unit_mesh: Local<UnitMesh>,
unit_materials: Local<UnitMaterials>,
mut units: Query<(&Unit, &mut Mesh2d, &mut MeshMaterial2d<ColorMaterial>)>,
) {
let (unit, mut mesh, mut material) = units.get_mut(trigger.target()).unwrap();
let (unit, mut mesh, mut material) = units.get_mut(insert.entity).unwrap();
**mesh = unit_mesh.0.clone();
**material = unit_materials.get(&unit.team).unwrap().clone();
}
Expand All @@ -229,24 +229,22 @@ fn init_unit(
///
/// The selection is local to the player and is not networked.
fn select_units(
trigger: Trigger<Pointer<Drag>>,
drag: On<Pointer<Drag>>,
mut commands: Commands,
mut selection: ResMut<Selection>,
team: Res<Team>,
camera: Single<(&Camera, &GlobalTransform)>,
units: Query<(Entity, &Unit, &GlobalTransform, &Aabb, Has<Selected>)>,
) -> Result<()> {
if trigger.button != PointerButton::Primary {
if drag.button != PointerButton::Primary {
return Ok(());
}

let (camera, transform) = *camera;

let origin = camera.viewport_to_world_2d(
transform,
trigger.pointer_location.position - trigger.distance,
)?;
let end = camera.viewport_to_world_2d(transform, trigger.pointer_location.position)?;
let origin =
camera.viewport_to_world_2d(transform, drag.pointer_location.position - drag.distance)?;
let end = camera.viewport_to_world_2d(transform, drag.pointer_location.position)?;

selection.rect = Rect::from_corners(origin, end);
selection.active = true;
Expand All @@ -268,16 +266,16 @@ fn select_units(
}

/// Stops displaying the selection rectangle.
fn end_selection(_trigger: Trigger<Pointer<DragEnd>>, mut rect: ResMut<Selection>) {
fn end_selection(_on: On<Pointer<DragEnd>>, mut rect: ResMut<Selection>) {
rect.active = false;
}

fn clear_selection(
trigger: Trigger<Pointer<Pressed>>,
press: On<Pointer<Press>>,
mut commands: Commands,
units: Query<Entity, With<Selected>>,
) {
if trigger.button != PointerButton::Primary {
if press.button != PointerButton::Primary {
return;
}
for unit in &units {
Expand All @@ -287,19 +285,19 @@ fn clear_selection(

/// Requests movement into a location for previously the selected units.
fn trigger_units_move(
trigger: Trigger<Pointer<Pressed>>,
press: On<Pointer<Press>>,
mut commands: Commands,
camera: Single<(&Camera, &GlobalTransform)>,
units: Populated<Entity, With<Selected>>,
) -> Result<()> {
if trigger.button != PointerButton::Secondary {
if press.button != PointerButton::Secondary {
return Ok(());
}

let (camera, transform) = *camera;
let position = camera.viewport_to_world_2d(transform, trigger.pointer_location.position)?;
let position = camera.viewport_to_world_2d(transform, press.pointer_location.position)?;

commands.client_trigger(UnitsMove {
commands.client_trigger(MoveUnits {
units: units.iter().collect(),
position,
});
Expand All @@ -314,7 +312,7 @@ const MOVE_SPACING: f32 = 30.0;
/// Each unit receives a unique `Command::Move`, arranged in a grid formation
/// centered on the requested position. The grid is oriented toward that position.
fn apply_units_move(
trigger: Trigger<FromClient<UnitsMove>>,
move_units: On<FromClient<MoveUnits>>,
teams: Res<ClientTeams>,
mut slots: Local<Vec<Vec2>>,
mut positions: Local<Vec<Vec2>>,
Expand All @@ -323,42 +321,42 @@ fn apply_units_move(
// Validate the received data since the client could be malicious.
// For example, on the client side we skip empty selections, but a modified
// client could bypass this and cause a division by zero on the server.
if trigger.units.is_empty() {
error!("`{}` attempted to move zero units", trigger.client_id);
if move_units.units.is_empty() {
error!("`{}` attempted to move zero units", move_units.client_id);
return;
}

let Some(&client_team) = teams.get(&trigger.client_id) else {
let Some(&client_team) = teams.get(&move_units.client_id) else {
error!(
"`{}` attempted to move units but has no team",
trigger.client_id
move_units.client_id
);
return;
};

positions.clear();
positions.reserve(trigger.units.len());
for (unit, transform, _) in units.iter_many(&trigger.units) {
positions.reserve(move_units.units.len());
for (unit, transform, _) in units.iter_many(&move_units.units) {
if unit.team != client_team {
error!(
"`{}` has team `{client_team:?}`, but tried to move unit with team `{:?}`",
trigger.client_id, unit.team
move_units.client_id, unit.team
);
return;
}

positions.push(transform.translation().truncate());
}

let units_count = trigger.units.len();
let units_count = move_units.units.len();
let cols = (units_count as f32).sqrt().ceil() as usize;
let rows = units_count.div_ceil(cols);
let centering_offset = -Vec2::new(cols as f32 - 1.0, rows as f32 - 1.0) / 2.0 * MOVE_SPACING;

// Orientation basis to make grid facing from group centroid toward the click.
let positions_sum = positions.iter().sum::<Vec2>();
let centroid = positions_sum / units_count as f32;
let forward = (trigger.position - centroid).normalize_or(Vec2::Y);
let forward = (move_units.position - centroid).normalize_or(Vec2::Y);
let right = Vec2::new(forward.y, -forward.x);
let rotation = Mat2::from_cols(right, forward);

Expand All @@ -369,7 +367,7 @@ fn apply_units_move(
let col = index % cols;

let grid_position = centering_offset + Vec2::new(col as f32, row as f32) * MOVE_SPACING;
slots.push(trigger.position + rotation * grid_position);
slots.push(move_units.position + rotation * grid_position);
}

// Pick closest slot for each unit using
Expand All @@ -385,7 +383,7 @@ fn apply_units_move(
.collect();
let (_, unit_to_slot) = kuhn_munkres_min(&weights);

let mut iter = units.iter_many_mut(&trigger.units);
let mut iter = units.iter_many_mut(&move_units.units);
for &slot_index in &unit_to_slot {
let (.., mut command) = iter.fetch_next().unwrap();
*command = Command::Move(slots[slot_index]);
Expand Down Expand Up @@ -649,21 +647,21 @@ impl FromWorld for UnitMaterials {
}
}

/// A trigger to join a specific team for a client.
/// Request to join a team.
#[derive(Event, Serialize, Deserialize)]
struct TeamRequest {
team: Team,
}

/// A trigger that spawns a unit at a location.
/// Request to spawn a unit at a location.
#[derive(Event, Serialize, Deserialize)]
struct UnitSpawn {
position: Vec2,
}

/// A trigger that orders units to move to a specified location.
/// Orders units to move to a specified location.
#[derive(Event, Serialize, Deserialize, MapEntities, Clone)]
struct UnitsMove {
struct MoveUnits {
#[entities]
units: Vec<Entity>,
position: Vec2,
Expand Down
Loading