Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions examples/large_scenes/bevy_city/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ edition = "2024"
publish = false
license = "MIT OR Apache-2.0"

[features]
traffic = []

[dependencies]
bevy = { path = "../../../", features = [
"https",
Expand Down
5 changes: 5 additions & 0 deletions examples/large_scenes/bevy_city/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub struct CityAssets {
pub tree_large: Handle<Scene>,
pub path_stones_long: Handle<Scene>,
pub fence: Handle<Scene>,
pub traffic_light: Handle<Scene>,
}

impl CityAssets {
Expand Down Expand Up @@ -219,6 +220,9 @@ pub fn load_assets(
GltfAssetLabel::Scene(0).from_asset(format!("{base_url}/city-kit-suburban/fence.glb"))
);

let traffic_light: Handle<Scene> =
load_asset!(GltfAssetLabel::Scene(0).from_asset("traffic_assets/traffic_light.glb"));

commands.insert_resource(CityAssets {
untyped_assets,
cars,
Expand All @@ -232,5 +236,6 @@ pub fn load_assets(
tree_large,
path_stones_long,
fence,
traffic_light,
});
}
41 changes: 35 additions & 6 deletions examples/large_scenes/bevy_city/src/generate_city.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ use bevy::prelude::*;
use noise::{NoiseFn, OpenSimplex};
use rand::{rngs::SmallRng, RngExt, SeedableRng};

use crate::{assets::CityAssets, Car, Road};

use crate::{assets::CityAssets, traffic, Car, CarAtStopState, CarState, Road};
#[derive(Component)]
pub struct CityRoot;

Expand Down Expand Up @@ -87,10 +86,26 @@ fn spawn_roads_and_cars<R: RngExt>(
let x = offset.x;
let z = offset.z;

commands.spawn((
SceneRoot(assets.crossroad.clone()),
Transform::from_xyz(x, 0.0, z),
));
let crossroad = commands
.spawn((
SceneRoot(assets.crossroad.clone()),
Transform::from_xyz(x, 0.0, z),
traffic::TrafficLight::new((x * 7.3 + z * 3.7).rem_euclid(traffic::CYCLE_DURATION)),
))
.id();

// 2 traffic lights at diagonal corners
for (dx, dz, y_rot) in [
(-0.5_f32, -0.5_f32, 0.0_f32),
(0.5_f32, -0.5_f32, std::f32::consts::FRAC_PI_2),
] {
commands.spawn((
SceneRoot(assets.traffic_light.clone()),
Transform::from_translation(Vec3::new(x + dx, 0.0, z + dz))
.with_rotation(Quat::from_rotation_y(y_rot))
.with_scale(Vec3::splat(0.3)),
));
}

let max_car_density = 0.4;

Expand All @@ -108,6 +123,7 @@ fn spawn_roads_and_cars<R: RngExt>(
Road {
start: Vec3::new(0.75, 0.0, 0.0),
end: Vec3::new(0.75 + (0.5 * car_count as f32), 0.0, 0.0),
intersection: crossroad,
},
))
.with_children(|commands| {
Expand All @@ -133,6 +149,9 @@ fn spawn_roads_and_cars<R: RngExt>(
distance_traveled: i as f32 * 0.5,
dir: -1.0,
offset: Vec3::new(4.25, 0.0, -0.15),
car_state: CarState::Driving,
at_stop_state: CarAtStopState::Default,
next_lane: None,
},
));
}
Expand All @@ -150,6 +169,9 @@ fn spawn_roads_and_cars<R: RngExt>(
distance_traveled: i as f32 * 0.5,
dir: 1.0,
offset: Vec3::new(-0.25, 0.0, 0.15),
car_state: CarState::Driving,
at_stop_state: CarAtStopState::Default,
next_lane: None,
},
));
}
Expand All @@ -165,6 +187,7 @@ fn spawn_roads_and_cars<R: RngExt>(
Road {
start: Vec3::new(0.0, 0.0, 0.75),
end: Vec3::new(0.0, 0.0, 0.75 + (0.5 * car_count as f32)),
intersection: crossroad,
},
))
.with_children(|commands| {
Expand All @@ -187,6 +210,9 @@ fn spawn_roads_and_cars<R: RngExt>(
distance_traveled: i as f32 * 0.5,
dir: 1.0,
offset: Vec3::new(-0.15, 0.0, -0.25),
car_state: CarState::Driving,
at_stop_state: CarAtStopState::Default,
next_lane: None,
},
));
}
Expand All @@ -201,6 +227,9 @@ fn spawn_roads_and_cars<R: RngExt>(
distance_traveled: i as f32 * 0.5,
dir: -1.0,
offset: Vec3::new(0.15, 0.0, 2.75),
car_state: CarState::Driving,
at_stop_state: CarAtStopState::Default,
next_lane: None,
},
));
}
Expand Down
186 changes: 149 additions & 37 deletions examples/large_scenes/bevy_city/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ mod assets;
mod generate_city;
mod settings;

mod traffic;

#[derive(FromArgs, Resource, Clone)]
/// Config
pub struct Args {
Expand All @@ -41,14 +43,23 @@ pub struct Args {
/// adds NoCpuCulling to all meshes
#[argh(switch)]
no_cpu_culling: bool,
// forces Vulkan backend and clears instance flags for RenderDoc capture
//#[argh(switch)]
//renderdoc: bool,
}

fn main() {
let args: Args = argh::from_env();

App::new()
.add_plugins((
DefaultPlugins.set(WindowPlugin {
let mut app = App::new();

app.add_plugins((
DefaultPlugins
.set(AssetPlugin {
file_path: "./".to_string(),
..default()
})
.set(WindowPlugin {
primary_window: Some(Window {
title: "bevy_city".into(),
resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
Expand All @@ -58,30 +69,34 @@ fn main() {
}),
..default()
}),
FreeCameraPlugin,
FeathersPlugins,
WireframePlugin::default(),
))
.insert_resource(args.clone())
.insert_resource(ClearColor(Color::BLACK))
.insert_resource(WinitSettings::continuous())
.init_resource::<Settings>()
.insert_resource(UiTheme(create_dark_theme()))
.insert_resource(WireframeConfig {
global: false,
default_color: WHITE.into(),
..default()
})
// Like in many realistic large scenes, many of the objects don't move
// We can accelerate transform propagation by optimizing for this case
.insert_resource(StaticTransformOptimizations::Enabled)
.add_systems(Startup, (setup, load_assets))
.add_systems(Update, (simulate_cars, loading_screen))
.add_observer(add_no_cpu_culling)
.add_observer(add_no_cpu_culling_on_scene_ready)
.add_observer(on_city_assets_ready)
.add_observer(setup_settings_ui)
.run();
traffic::TrafficPlugin,
FreeCameraPlugin,
FeathersPlugins,
WireframePlugin::default(),
))
.insert_resource(args.clone())
.insert_resource(ClearColor(Color::BLACK))
.insert_resource(WinitSettings::continuous())
.init_resource::<Settings>()
.insert_resource(UiTheme(create_dark_theme()))
.insert_resource(WireframeConfig {
global: false,
default_color: WHITE.into(),
..default()
})
// Like in many realistic large scenes, many of the objects don't move
// We can accelerate transform propagation by optimizing for this case
.insert_resource(StaticTransformOptimizations::Enabled)
.add_systems(Startup, (setup, load_assets))
.add_systems(Update, (simulate_cars, loading_screen))
.add_observer(add_no_cpu_culling)
.add_observer(add_no_cpu_culling_on_scene_ready)
.add_observer(on_city_assets_ready)
.add_observer(setup_settings_ui);

app.add_systems(Update, apply_traffic_rules.before(simulate_cars));

app.run();
}

fn setup(mut commands: Commands, mut scattering_mediums: ResMut<Assets<ScatteringMedium>>) {
Expand Down Expand Up @@ -225,41 +240,138 @@ fn on_city_assets_ready(
struct Road {
start: Vec3,
end: Vec3,
intersection: Entity,
}

#[derive(Component)]
struct Car {
offset: Vec3,
distance_traveled: f32,
dir: f32,
car_state: CarState,
at_stop_state: CarAtStopState,
next_lane: Option<Entity>,
}

#[derive(PartialEq, Eq, Default)]
pub enum CarState {
Accelerating,
#[default]
Driving,
Breaking,
Stopped,
}

#[derive(PartialEq, Eq, Default)]
pub enum CarAtStopState {
#[default]
Default,
WaitForIntersection,
MoveOnIntersection,
WaitProtectedIntersection,
MoveOnProtectedIntersection,
}

fn simulate_cars(
settings: Res<Settings>,
roads: Query<(&Road, &Transform, &Children), Without<Car>>,
roads: Query<(&Road, &Children), Without<Car>>,
car_positions: Query<&Car, Without<Road>>,
mut cars: Query<(&mut Car, &mut Transform), Without<Road>>,
time: Res<Time>,
) {
if !settings.simulate_cars {
return;
}
let speed = 1.5;
let minimum_gap = 0.5;

for (road, children) in &roads {
let road_len = (road.end - road.start).length();
let direction = (road.end - road.start).normalize();

// Snapshot distances from a read-only query before mutating anything
let distances: Vec<(Entity, f32, f32)> = children
.iter()
.filter_map(|e| {
car_positions
.get(e)
.ok()
.map(|car| (e, car.distance_traveled, car.dir))
})
.collect();

for (road, _, children) in &roads {
for child in children {
let Ok((mut car, mut car_transform)) = cars.get_mut(*child) else {
for child in children.iter() {
let Ok((mut car, mut car_transform)) = cars.get_mut(child) else {
continue;
};

car.distance_traveled += speed * time.delta_secs();
let road_len = (road.end - road.start).length();
if car.distance_traveled > road_len {
car.distance_traveled = 0.0;
if matches!(car.car_state, CarState::Stopped) {
continue;
}

let blocked = distances.iter().any(|&(e, other_d, other_dir)| {
e != child && other_dir == car.dir && {
let gap = (other_d - car.distance_traveled) * car.dir;
gap > 0.0 && gap < minimum_gap
}
});

if matches!(car.car_state, CarState::Breaking | CarState::Stopped)
&& car.at_stop_state == CarAtStopState::Default
{
car.at_stop_state = CarAtStopState::WaitForIntersection;
}
let protected_turn =
car.next_lane.is_some() && car.next_lane != Some(road.intersection);
if matches!(car.at_stop_state, CarAtStopState::WaitForIntersection)
&& !protected_turn
&& !blocked
{
car.at_stop_state = CarAtStopState::MoveOnIntersection;
}

if !blocked {
car.distance_traveled += speed * time.delta_secs();
if car.distance_traveled > road_len {
car.distance_traveled = 0.0;
}
}
let direction = (road.end - road.start).normalize() * car.dir;

let progress = car.distance_traveled / road_len;
car_transform.translation = (road.start + car.offset) + direction * road_len * progress;
car_transform.translation =
(road.start + car.offset) + direction * car.dir * road_len * progress;
}
}
}

fn apply_traffic_rules(
settings: Res<Settings>,
roads: Query<(&Road, &Children), Without<Car>>,
mut cars: Query<&mut Car>,
traffic_lights: Query<&traffic::TrafficLight>,
time: Res<Time>,
) {
let elapsed = time.elapsed_secs();
let stop_distance = 0.4;
for (road, children) in &roads {
let Ok(light) = traffic_lights.get(road.intersection) else {
continue;
};
let phase = light.phase(elapsed);
for child in children.iter() {
let Ok(mut car) = cars.get_mut(child) else {
continue;
};
let road_len = (road.end - road.start).length();
let remaining = road_len - car.distance_traveled;
car.car_state = if settings.traffic_enabled
&& remaining < stop_distance
&& phase == traffic::TrafficLightPhase::Red
{
CarState::Stopped
} else {
CarState::Driving
};
}
}
}
Expand Down
Loading