4 Commits
Author SHA1 Message Date
drake 64ab2cbaae Merge remote-tracking branch 'origin/world' into world 2026-06-03 15:52:40 -05:00
drake e7441dac5c Cleanup 2026-06-03 15:52:32 -05:00
drake ad0f6bad59 add world plugin 2026-06-03 15:51:05 -05:00
drake afd2647b17 add world plugin 2026-06-03 15:49:13 -05:00
9 changed files with 1641 additions and 739 deletions
+8
View File
@@ -0,0 +1,8 @@
# for Linux
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
# for Windows
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"
Generated
+1194 -707
View File
File diff suppressed because it is too large Load Diff
+12 -2
View File
@@ -4,5 +4,15 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
avian2d = "0.5.0" avian2d = "0.6.1"
bevy = "0.18.0" bevy = { version = "0.18.0", features = ["dynamic_linking"] }
# Enable a small amount of optimization in the dev profile.
[profile.dev]
opt-level = 1
# Enable a large amount of optimization in the dev profile for dependencies.
[profile.dev.package."*"]
opt-level = 3
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

-1
View File
@@ -1 +0,0 @@
/home/mitchell/Pictures/player.png

Before

Width:  |  Height:  |  Size: 34 B

After

Width:  |  Height:  |  Size: 16 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 B

After

Width:  |  Height:  |  Size: 16 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+35 -20
View File
@@ -1,6 +1,9 @@
use avian2d::{math::*, prelude::*}; use avian2d::{math::*, prelude::*};
use bevy::prelude::*; use bevy::prelude::*;
const PLAYER_SPEED: f32 = 300.0;
pub struct CharacterControllerPlugin; pub struct CharacterControllerPlugin;
impl Plugin for CharacterControllerPlugin { impl Plugin for CharacterControllerPlugin {
@@ -26,6 +29,10 @@ pub struct MaxSpeed(Scalar);
#[derive(Component, Default, Copy, Clone)] #[derive(Component, Default, Copy, Clone)]
pub struct MaxAcceleration(Scalar); pub struct MaxAcceleration(Scalar);
/// Whether inputs are enabled for a CharacterController.
#[derive(Component, Default, Copy, Clone)]
pub struct InputEnabled(pub bool);
/// A bundle that contains the components needed for a basic /// A bundle that contains the components needed for a basic
/// kinematic character controller. /// kinematic character controller.
#[derive(Bundle)] #[derive(Bundle)]
@@ -35,16 +42,18 @@ pub struct CharacterControllerBundle {
collider: Collider, collider: Collider,
speed: MaxSpeed, speed: MaxSpeed,
acceleration: MaxAcceleration, acceleration: MaxAcceleration,
enabled: InputEnabled,
} }
impl CharacterControllerBundle { impl CharacterControllerBundle {
pub fn new(collider: Collider) -> Self { pub fn new(collider: Collider) -> Self {
Self { Self {
character_controller: CharacterController, character_controller: CharacterController,
body: RigidBody::Kinematic, body: RigidBody::Dynamic,
collider, collider,
speed: MaxSpeed(300.), speed: MaxSpeed(PLAYER_SPEED),
acceleration: MaxAcceleration(5000.), acceleration: MaxAcceleration(5000.),
enabled: InputEnabled(true),
} }
} }
} }
@@ -93,31 +102,37 @@ fn gamepad_input(mut movement_writer: MessageWriter<MovementAction>, gamepads: Q
fn movement( fn movement(
time: Res<Time>, time: Res<Time>,
mut movement_reader: MessageReader<MovementAction>, mut movement_reader: MessageReader<MovementAction>,
mut controllers: Query<(&MaxSpeed, &MaxAcceleration, &mut LinearVelocity)>, mut controllers: Query<(&MaxSpeed, &MaxAcceleration, &mut LinearVelocity, &InputEnabled)>,
) { ) {
// Precision is adjusted so that the example works with // Precision is adjusted so that the example works with
// both the `f32` and `f64` features. Otherwise you don't need this. // both the `f32` and `f64` features. Otherwise you don't need this.
let delta_time = time.delta_secs_f64().adjust_precision(); let delta_time = time.delta_secs_f64().adjust_precision();
for (max_speed, max_acceleration, mut linear_velocity) in &mut controllers { for (max_speed, max_acceleration, mut linear_velocity, enabled) in &mut controllers {
while movement_reader.len() > 1 { if enabled.0 {
warn!("Extra movement message. Ignoring"); // let len = movement_reader.len();
movement_reader.read(); // while len > 1 {
} // warn!("Extra movement message. Ignoring");
// println!("{}", len);
// let _ = movement_reader.read().take(len - 1).count();
// }
let target = movement_reader let target = movement_reader
.read() .read()
.next() .last()
.map(|ma| ma.0) .map(|ma| ma.0)
.unwrap_or_default() .unwrap_or_default()
* max_speed.0; * max_speed.0;
let mut delta = target - **linear_velocity; let mut delta = target - **linear_velocity;
let delta_len = delta.length(); let delta_len = delta.length();
let max_acceleration = max_acceleration.0 * delta_time; let max_acceleration = max_acceleration.0 * delta_time;
if delta_len > max_acceleration { if delta_len > max_acceleration {
delta = delta.normalize() * max_acceleration; delta = delta.normalize() * max_acceleration;
}
**linear_velocity += delta;
} else {
**linear_velocity = Vec2::ZERO;
} }
**linear_velocity += delta;
} }
} }
+24 -10
View File
@@ -1,41 +1,51 @@
use crate::avian::{CharacterControllerBundle, CharacterControllerPlugin}; use crate::avian::{CharacterControllerBundle, CharacterControllerPlugin};
use crate::world::{MainCamera, Player, WorldPlugin};
use avian2d::parry::simba::simd::SimdComplexField;
use avian2d::{ use avian2d::{
prelude::*,
PhysicsPlugins, PhysicsPlugins,
math::Vector,
prelude::{Collider, Gravity, RigidBody},
}; };
use bevy::tasks::futures_lite::StreamExt;
use bevy::{camera::ScalingMode, color::palettes::css::GREEN, prelude::*}; use bevy::{camera::ScalingMode, color::palettes::css::GREEN, prelude::*};
pub mod avian; pub mod avian;
pub mod world;
fn main() { fn main() {
App::new() App::new()
.add_plugins(( .add_plugins((
DefaultPlugins, // DefaultPlugins,
DefaultPlugins.set(ImagePlugin::default_nearest()),
// Add physics plugins and specify a units-per-meter scaling factor, 1 meter = 20 pixels. // Add physics plugins and specify a units-per-meter scaling factor, 1 meter = 20 pixels.
// The unit allows the engine to tune its parameters for the scale of the world, improving stability. // The unit allows the engine to tune its parameters for the scale of the world, improving stability.
PhysicsPlugins::default().with_length_unit(20.0), PhysicsPlugins::default().with_length_unit(20.0),
CharacterControllerPlugin, CharacterControllerPlugin,
WorldPlugin,
)) ))
.add_systems(Startup, setup) .add_systems(Startup, setup)
.add_systems(Update, debug_border) .add_systems(Update, debug_border)
.insert_resource(Gravity(Vector::ZERO))
.run(); .run();
} }
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let mut projection = OrthographicProjection::default_2d(); let mut projection = OrthographicProjection::default_2d();
projection.scaling_mode = ScalingMode::AutoMin { projection.scaling_mode = ScalingMode::AutoMin {
min_width: 1920., min_width: 1920.,
min_height: 1080., min_height: 1080.,
}; };
commands.spawn((Camera2d, Projection::Orthographic(projection))); commands.spawn((Camera2d, MainCamera, Projection::Orthographic(projection)));
// player // player
commands.spawn(( commands.spawn((
Sprite::from_image(asset_server.load("player.png")), Sprite::from_image(asset_server.load("south.png")),
Transform::from_xyz(0.0, 0.0, 0.0),
CharacterControllerBundle::new(Collider::capsule(15., 27.5)), Player,
Transform::from_xyz(0.0, 0.0, 2.0).with_scale(Vec3::splat(4.0)),
CharacterControllerBundle::new(Collider::rectangle(14.0, 30.0)),
LockedAxes::ROTATION_LOCKED,
)); ));
// A cube to move around // A cube to move around
@@ -45,10 +55,14 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
custom_size: Some(Vec2::new(30.0, 30.0)), custom_size: Some(Vec2::new(30.0, 30.0)),
..default() ..default()
}, },
Transform::from_xyz(50.0, -100.0, 0.0), Transform::from_xyz(50.0, -100.0, 1.0),
RigidBody::Dynamic, RigidBody::Dynamic,
Collider::rectangle(30.0, 30.0), Collider::rectangle(30.0, 30.0),
)); ));
} }
fn debug_border(mut gizmos: Gizmos) { fn debug_border(mut gizmos: Gizmos) {
+368
View File
@@ -0,0 +1,368 @@
use avian2d::math::Vector;
use avian2d::prelude::{Collider, CollidingEntities, CollisionEventsEnabled, CollisionStart, Gravity, RigidBody, Sensor};
use bevy::camera::Viewport;
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use crate::avian::InputEnabled;
const ROOM_WIDTH: f32 = 1920.0;
const ROOM_HEIGHT: f32 = 1080.0;
const WALL_THICKNESS: f32 = 20.0;
const DOOR_WIDTH: f32 = 120.0;
const DOOR_DEPTH: f32 = 40.0;
const SPAWN_DISTANCE: f32 = 80.0;
const FADE_DURATION: f32 = 0.35;
const TARGET_ASPECT: f32 = ROOM_WIDTH / ROOM_HEIGHT;
const HALF_W: f32 = ROOM_WIDTH / 2.0;
const HALF_H: f32 = ROOM_HEIGHT / 2.0;
pub struct WorldPlugin;
impl Plugin for WorldPlugin {
fn build(&self, app: &mut App) {
app
.add_systems(Startup, setup)
.add_systems(
Update,
(
door_detection.run_if(in_state(GameState::Playing)),
transition_system.run_if(in_state(GameState::Transitioning)),
update_viewport,
),
)
.init_state::<GameState>()
.insert_resource(CurrentRoom(IVec2::ZERO))
.insert_resource(Gravity(Vector::ZERO))
.insert_resource(ClearColor(Color::BLACK));
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
spawn_room(&mut commands, IVec2::new(0, 0), vec![Direction::North, Direction::East], &asset_server);
spawn_room(&mut commands, IVec2::new(1, 0), vec![Direction::West], &asset_server);
spawn_room(&mut commands, IVec2::new(0, 1), vec![Direction::South], &asset_server);
commands.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
..default()
},
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.0)),
FadeOverlay,
));
}
#[derive(States, Default, Clone, Eq, PartialEq, Hash, Debug)]
enum GameState {
#[default]
Playing,
Transitioning,
}
#[derive(Resource)]
struct CurrentRoom(IVec2);
#[derive(Resource)]
struct RoomTransition {
target_room: IVec2,
target_player_position: Vec2,
timer: Timer,
phase: TransitionPhase,
}
#[derive(Clone, Copy, PartialEq)]
enum TransitionPhase {
FadeOut,
FadeIn,
}
#[derive(Component)]
pub struct Player;
#[derive(Component)]
pub struct MainCamera;
#[derive(Component)]
struct FadeOverlay;
#[derive(Component)]
struct Door {
room: IVec2,
travel_direction: Direction,
}
#[derive(Clone, Copy, PartialEq)]
enum Direction {
North,
South,
East,
West,
}
fn spawn_room(commands: &mut Commands, room: IVec2, door_directions: Vec<Direction>, asset_server: &Res<AssetServer>) {
let center = room_center(room);
commands.spawn((
Sprite {
image: asset_server.load("img.png"),
custom_size: Some(Vec2::new(ROOM_WIDTH, ROOM_HEIGHT)),
image_mode: SpriteImageMode::Tiled {
tile_x: true,
tile_y: true,
stretch_value: 1.0
},
..default()
},
// Sprite::from_color(color, Vec2::new(ROOM_WIDTH, ROOM_HEIGHT)),
Transform::from_translation(center),
));
spawn_doors(commands, room, door_directions);
spawn_room_walls(commands, center);
}
fn spawn_room_walls(commands: &mut Commands, center: Vec3) {
// LEFT
commands.spawn((
RigidBody::Static,
Sprite::from_color(Color::srgb(0.5, 0.5, 0.5), Vec2::new(WALL_THICKNESS, ROOM_HEIGHT)),
Collider::rectangle(WALL_THICKNESS, ROOM_HEIGHT),
Transform::from_xyz(
center.x - HALF_W + WALL_THICKNESS / 2.0,
center.y,
10.0,
),
));
// RIGHT
commands.spawn((
RigidBody::Static,
Sprite::from_color(Color::srgb(0.5, 0.5, 0.5), Vec2::new(WALL_THICKNESS, ROOM_HEIGHT)),
Collider::rectangle(WALL_THICKNESS, ROOM_HEIGHT),
Transform::from_xyz(
center.x + HALF_W - WALL_THICKNESS / 2.0,
center.y,
10.0,
),
));
// TOP
commands.spawn((
RigidBody::Static,
Sprite::from_color(Color::srgb(0.5, 0.5, 0.5), Vec2::new(ROOM_WIDTH, WALL_THICKNESS)),
Collider::rectangle(ROOM_WIDTH, WALL_THICKNESS),
Transform::from_xyz(
center.x,
center.y + HALF_H - WALL_THICKNESS / 2.0,
10.0,
),
));
// BOTTOM
commands.spawn((
RigidBody::Static,
Sprite::from_color(Color::srgb(0.5, 0.5, 0.5), Vec2::new(ROOM_WIDTH, WALL_THICKNESS)),
Collider::rectangle(ROOM_WIDTH, WALL_THICKNESS),
Transform::from_xyz(
center.x,
center.y - HALF_H + WALL_THICKNESS / 2.0,
10.0,
),
));
}
fn spawn_doors(commands: &mut Commands, room: IVec2, door_directions: Vec<Direction>) {
let room_center = room_center(room);
const HALF_DEPTH: f32 = DOOR_DEPTH / 2.0;
for direction in door_directions {
let transform: Transform;
let size: Vec2;
match direction {
Direction::North => {
transform = Transform::from_xyz(room_center.x, room_center.y + HALF_H - HALF_DEPTH, 1.0);
size = Vec2::new(DOOR_WIDTH, DOOR_DEPTH);
},
Direction::East => {
transform = Transform::from_xyz(room_center.x + HALF_W - HALF_DEPTH, room_center.y, 1.0);
size = Vec2::new(DOOR_DEPTH, DOOR_WIDTH);
},
Direction::South => {
transform = Transform::from_xyz(room_center.x, room_center.y - HALF_H + HALF_DEPTH, 1.0);
size = Vec2::new(DOOR_WIDTH, DOOR_DEPTH);
},
Direction::West => {
transform = Transform::from_xyz(room_center.x - HALF_W + HALF_DEPTH, room_center.y, 1.0);
size = Vec2::new(DOOR_DEPTH, DOOR_WIDTH);
}
}
commands.spawn((
Sprite::from_color(Color::srgb(0.1, 0.1, 0.1), size),
Collider::rectangle(size.x, size.y),
Sensor,
CollisionEventsEnabled,
CollidingEntities::default(),
Door {
room: IVec2::from(room),
travel_direction: direction,
},
transform,
GlobalTransform::default(),
));
}
}
fn door_detection(
mut commands: Commands,
mut next_state: ResMut<NextState<GameState>>,
player: Single<(Entity, &Transform), With<Player>>,
doors: Query<&Door>,
mut started: MessageReader<CollisionStart>,
) {
for event in started.read() {
println!("CollisionStart: {event:?}");
let door = doors.get(event.collider2);
if event.collider1 == player.0 && door.is_ok() {
commands.insert_resource(RoomTransition {
target_room: door_target_room(door.unwrap()),
target_player_position: door_target_position(door.unwrap()),
timer: Timer::from_seconds(
FADE_DURATION,
TimerMode::Once,
),
phase: TransitionPhase::FadeOut,
});
commands.entity(player.0).insert(InputEnabled(false));
next_state.set(GameState::Transitioning);
}
}
}
fn door_target_room(door: &Door) -> IVec2 {
match door.travel_direction {
Direction::North => IVec2::new(door.room.x, door.room.y + 1),
Direction::East => IVec2::new(door.room.x + 1, door.room.y),
Direction::South => IVec2::new(door.room.x, door.room.y - 1),
Direction::West => IVec2::new(door.room.x - 1, door.room.y),
}
}
fn door_target_position(door: &Door) -> Vec2 {
match door.travel_direction {
Direction::North => Vec2::new(0.0, -HALF_H + WALL_THICKNESS + DOOR_DEPTH + SPAWN_DISTANCE),
Direction::East => Vec2::new(-HALF_W + WALL_THICKNESS + DOOR_DEPTH + SPAWN_DISTANCE, 0.0),
Direction::South => Vec2::new(0.0, HALF_H - WALL_THICKNESS - DOOR_DEPTH - SPAWN_DISTANCE),
Direction::West => Vec2::new(HALF_W - WALL_THICKNESS - DOOR_DEPTH - SPAWN_DISTANCE, 0.0),
}
}
fn transition_system(
mut commands: Commands,
time: Res<Time>,
mut transition: ResMut<RoomTransition>,
mut next_state: ResMut<NextState<GameState>>,
mut current_room: ResMut<CurrentRoom>,
mut player: Single<(Entity, &mut Transform), With<Player>>,
mut camera: Query<&mut Transform, (With<MainCamera>, Without<Player>)>,
mut overlay: Query<&mut BackgroundColor, With<FadeOverlay>>,
) {
transition.timer.tick(time.delta());
let mut overlay_color = overlay.single_mut().unwrap();
match transition.phase {
TransitionPhase::FadeOut => {
let alpha = transition.timer.fraction();
overlay_color.0 = Color::srgba(0.0, 0.0, 0.0, alpha);
if transition.timer.is_finished() {
current_room.0 = transition.target_room;
let room_center =
room_center(transition.target_room);
player.1.translation =
room_center
+ transition.target_player_position.extend(10.0);
camera.single_mut().unwrap().translation =
room_center + Vec3::new(0.0, 0.0, 999.0);
transition.phase = TransitionPhase::FadeIn;
transition.timer.reset();
}
}
TransitionPhase::FadeIn => {
let alpha = 1.0 - transition.timer.fraction();
overlay_color.0 = Color::srgba(0.0, 0.0, 0.0, alpha);
if transition.timer.is_finished() {
commands.entity(player.0).insert(InputEnabled(true));
next_state.set(GameState::Playing);
}
}
}
}
fn update_viewport(
window: Single<&Window, With<PrimaryWindow>>,
mut camera: Single<&mut Camera>,
) {
let window_width = window.physical_width();
let window_height = window.physical_height();
let window_aspect = window_width as f32 / window_height as f32;
let (viewport_width, viewport_height) = if window_aspect > TARGET_ASPECT {
// Ultrawide: black bars left/right
let h = window_height;
let w = (h as f32 * TARGET_ASPECT) as u32;
(w, h)
} else {
// Too tall: black bars top/bottom
let w = window_width;
let h = (w as f32 / TARGET_ASPECT) as u32;
(w, h)
};
let x = (window_width - viewport_width) / 2;
let y = (window_height - viewport_height) / 2;
camera.viewport = Some(Viewport {
physical_position: UVec2::new(x, y),
physical_size: UVec2::new(viewport_width, viewport_height),
..default()
});
}
fn room_center(room: IVec2) -> Vec3 {
Vec3::new(
room.x as f32 * ROOM_WIDTH,
room.y as f32 * ROOM_HEIGHT,
0.0,
)
}