Refactor done, still works.

Mostly resisted the urge to add or change functionality. Checks clean and should have the foundation
for real game-focused devel going forward.
This commit is contained in:
Joe Ardent 2022-01-12 00:18:13 -08:00
parent 5fba5af72e
commit 483dc3f864
9 changed files with 633 additions and 533 deletions

63
src/camera.rs Normal file
View file

@ -0,0 +1,63 @@
use bevy::prelude::*;
use crate::{geometry::CyberBike, input::InputState, physics::MovementSettings};
pub(crate) const CAM_DIST: f32 = 50.0;
#[derive(Component, Debug)]
pub struct CyberCam;
fn setup_cybercam(mut commands: Commands) {
use crate::geometry::PLAYER_DIST;
commands
.spawn_bundle(PerspectiveCameraBundle {
transform: Transform::from_xyz(PLAYER_DIST + CAM_DIST, 0.0, 0.0)
.looking_at(Vec3::ZERO, Vec3::Y),
..Default::default()
})
.insert(CyberCam);
}
fn follow_player(
bike_query: Query<(&Transform, &CyberBike), Without<CyberCam>>,
mut cam_query: Query<(&mut Transform, &CyberCam), Without<CyberBike>>,
) {
let (bike_xform, _) = bike_query.single();
let up = bike_xform.translation.normalize();
let look_at = bike_xform.translation;
let cam_pos = bike_xform.translation + (bike_xform.back() * CAM_DIST * 1.3) + (up * CAM_DIST);
let (mut cam_xform, _) = cam_query.single_mut();
cam_xform.translation = cam_pos;
cam_xform.look_at(look_at, up);
}
fn player_look(
settings: Res<MovementSettings>,
windows: Res<Windows>,
time: Res<Time>,
istate: Res<InputState>,
mut query: Query<(&mut Transform, &CyberBike)>,
) {
let window = windows.get_primary().unwrap();
let window_scale = window.height().min(window.width());
let dt = time.delta_seconds();
let (mut transform, _) = query.single_mut();
let d_alt = (settings.sensitivity * dt * window_scale * istate.pitch).to_radians();
let d_az = (settings.sensitivity * dt * window_scale * istate.yaw).to_radians();
let rotation = Quat::from_axis_angle(transform.local_y(), d_az)
* Quat::from_axis_angle(transform.local_x(), d_alt);
transform.rotate(rotation);
}
pub struct CyberCamPlugin;
impl Plugin for CyberCamPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_startup_system(setup_cybercam)
.add_system(follow_player)
.add_system(player_look);
}
}

View file

@ -1,295 +0,0 @@
use bevy::{
app::{Events, ManualEventReader},
input::{
gamepad::{GamepadAxisType, GamepadEvent, GamepadEventType},
Input,
},
prelude::*,
utils::tracing::info,
};
use heron::{
prelude::{CollisionShape, RigidBody},
CollisionEvent,
};
pub const PLANET_RADIUS: f32 = 150.0;
const PLAYER_DIST: f32 = PLANET_RADIUS + 100.0;
const CAM_DIST: f32 = 50.0;
#[derive(Component, Default)]
struct PlayerState {
velocity: Vec3,
colliding: bool,
}
#[derive(Default)]
struct InputState {
event_reader: ManualEventReader<GamepadEvent>,
pitch: f32,
yaw: f32,
throttle: f32,
}
/// Mouse sensitivity and movement speed
pub struct MovementSettings {
pub sensitivity: f32,
pub accel: f32,
pub drag: f32,
pub gravity: f32,
}
impl Default for MovementSettings {
fn default() -> Self {
Self {
sensitivity: 1.0,
accel: 40.,
drag: 0.0005,
gravity: 10.0,
}
}
}
/// Used in queries when you want flycams and not other cameras
#[derive(Component, Debug)]
pub struct FlyCam;
#[derive(Component, Debug)]
pub struct CyberBike;
#[derive(Component)]
struct UpText;
/// Spawns the `Camera3dBundle` to be controlled
fn setup_player(mut commands: Commands, asset_server: Res<AssetServer>) {
commands
.spawn_bundle(PerspectiveCameraBundle {
transform: Transform::from_xyz(PLAYER_DIST + CAM_DIST, 0.0, 0.0)
.looking_at(Vec3::ZERO, Vec3::Y),
..Default::default()
})
.insert(FlyCam);
commands
.spawn_bundle((
Transform {
translation: Vec3::new(PLAYER_DIST, 0.0, 0.0),
..Default::default()
}
.looking_at(Vec3::ZERO, Vec3::Y),
GlobalTransform::identity(),
))
.with_children(|rider| {
rider.spawn_scene(asset_server.load("cyber-bike_no_y_up.glb#Scene0"));
})
.insert(CyberBike)
.insert(RigidBody::Dynamic)
.insert(CollisionShape::Cone {
half_height: 2.0,
radius: 0.8,
})
.insert(PlayerState::default());
}
fn setup_ui(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn_bundle(UiCameraBundle::default());
commands
.spawn_bundle(TextBundle {
style: Style {
align_self: AlignSelf::FlexEnd,
..Default::default()
},
// Use `Text` directly
text: Text {
// Construct a `Vec` of `TextSection`s
sections: vec![TextSection {
value: "".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 40.0,
color: Color::GOLD,
},
}],
..Default::default()
},
..Default::default()
})
.insert(UpText);
}
fn ui_system(pstate_query: Query<&PlayerState>, mut text_query: Query<&mut Text, With<UpText>>) {
let mut text = text_query.single_mut();
let pstate = pstate_query.single();
text.sections[0].value = format!("{:.2}", pstate.velocity.length());
}
fn player_move(
time: Res<Time>,
mut bike_query: Query<(&mut Transform, &PlayerState), Without<FlyCam>>,
mut cam_query: Query<(&mut Transform, &FlyCam), Without<PlayerState>>,
) {
let dt = time.delta_seconds();
let (mut bike_xform, player_state) = bike_query.single_mut();
let up = bike_xform.translation.normalize();
let cam_up = bike_xform.up();
let cos = up.dot(cam_up);
let theta = cos.acos();
let rate = if !theta.is_normal() {
0.0
} else if theta.is_sign_negative() {
-0.4
} else {
0.4
} * dt;
let angle = if rate.is_sign_negative() {
rate.max(theta)
} else {
rate.min(theta)
};
let rot = Quat::from_axis_angle(cam_up.cross(up).normalize(), angle);
if rot.is_finite() && theta.abs() > 0.0085 {
bike_xform.rotate(rot);
}
if player_state.velocity.is_finite() {
bike_xform.translation += player_state.velocity * dt;
}
let look_at = bike_xform.translation;
let cam_pos = bike_xform.translation + (bike_xform.back() * CAM_DIST * 1.3) + (up * CAM_DIST);
let (mut cam_xform, _) = cam_query.single_mut();
cam_xform.translation = cam_pos;
cam_xform.look_at(look_at, up);
}
fn player_look(
settings: Res<MovementSettings>,
windows: Res<Windows>,
events: Res<Events<GamepadEvent>>,
time: Res<Time>,
mut istate: ResMut<InputState>,
mut query: Query<(&mut Transform, &mut PlayerState)>,
) {
let window = windows.get_primary().unwrap();
let window_scale = window.height().min(window.width());
let dt = time.delta_seconds();
let (mut transform, mut pstate) = query.single_mut();
let mut vel = pstate.velocity;
if pstate.velocity.is_finite() {
for GamepadEvent(_, ev) in istate.event_reader.iter(&events) {
match *ev {
GamepadEventType::ButtonChanged(GamepadButtonType::RightTrigger2, val) => {
istate.throttle = val;
}
GamepadEventType::ButtonChanged(GamepadButtonType::LeftTrigger2, val) => {
istate.throttle = -val;
}
GamepadEventType::ButtonChanged(GamepadButtonType::East, val) => {
if val > 0.5 {
vel = Vec3::ZERO;
}
}
GamepadEventType::AxisChanged(GamepadAxisType::LeftStickX, val) => {
istate.yaw = -val;
}
GamepadEventType::AxisChanged(GamepadAxisType::LeftStickY, val) => {
istate.pitch = -val;
}
_ => {
info!("unhandled gamepad event: {:?}", ev);
}
}
}
let accel = settings.accel * dt * istate.throttle;
vel += transform.forward() * accel;
// drag
let v2 = vel.length_squared().min(100_000.0);
if v2 < 0.05 {
vel = Vec3::ZERO;
} else {
let drag = vel * settings.drag * v2.sqrt() * time.delta_seconds();
vel -= drag;
}
} else {
vel = Vec3::ZERO;
}
let d_alt = (settings.sensitivity * dt * window_scale * istate.pitch).to_radians();
let d_az = (settings.sensitivity * dt * window_scale * istate.yaw).to_radians();
let rotation = Quat::from_axis_angle(transform.local_y(), d_az)
* Quat::from_axis_angle(transform.local_x(), d_alt);
transform.rotate(rotation);
pstate.velocity = vel;
}
fn collision_detection(
mut events: EventReader<CollisionEvent>,
mut query: Query<&mut PlayerState>,
) {
let mut pstate = query.single_mut();
for event in events.iter() {
if let CollisionEvent::Started(_, _) = event {
pstate.colliding = true;
} else {
pstate.colliding = false;
}
}
}
fn gravity_collision_reaction(
time: Res<Time>,
settings: Res<MovementSettings>,
mut query: Query<(&mut PlayerState, &Transform)>,
) {
let dt = time.delta_seconds();
let g = settings.gravity;
let (mut pstate, xform) = query.single_mut();
// first gravity
let down = -xform.translation.normalize();
let dvel = down * g * dt;
let mut vel = if pstate.velocity.is_finite() {
pstate.velocity + dvel
} else {
dvel
};
// now see if we're currently colliding
if pstate.colliding {
let dvel = down * vel.dot(down) * 1.02;
vel -= dvel;
}
pstate.velocity = vel;
}
/// Contains everything needed to add first-person fly camera behavior to your
/// game
pub struct PlayerPlugin;
impl Plugin for PlayerPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.init_resource::<PlayerState>()
.init_resource::<MovementSettings>()
.init_resource::<InputState>()
.add_startup_system(setup_player)
.add_startup_system(setup_ui)
.add_system(ui_system)
.add_system(player_move.label("move"))
.add_system(player_look.after("move"))
.add_system(collision_detection)
.add_system(gravity_collision_reaction);
}
}

67
src/geometry.rs Normal file
View file

@ -0,0 +1,67 @@
use bevy::prelude::*;
use heron::prelude::{CollisionShape, RigidBody};
pub const PLANET_RADIUS: f32 = 150.0;
pub(crate) const PLAYER_DIST: f32 = PLANET_RADIUS + 100.0;
pub struct CyberGeomPlugin;
impl Plugin for CyberGeomPlugin {
fn build(&self, app: &mut App) {
app.add_startup_system(setup_giant_sphere)
.add_startup_system(setup_player);
}
}
#[derive(Component, Debug)]
pub struct CyberBike;
fn setup_giant_sphere(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// world
commands
.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Icosphere {
radius: PLANET_RADIUS,
subdivisions: 6,
})),
material: materials.add(StandardMaterial {
base_color: Color::GRAY,
metallic: 0.7,
perceptual_roughness: 0.5,
..Default::default()
}),
..Default::default()
})
.insert(RigidBody::Static)
.insert(CollisionShape::Sphere {
radius: PLANET_RADIUS,
});
}
fn setup_player(mut commands: Commands, asset_server: Res<AssetServer>) {
use crate::physics::PlayerState;
commands
.spawn_bundle((
Transform {
translation: Vec3::new(PLAYER_DIST, 0.0, 0.0),
..Default::default()
}
.looking_at(Vec3::ZERO, Vec3::Y),
GlobalTransform::identity(),
))
.with_children(|rider| {
rider.spawn_scene(asset_server.load("cyber-bike_no_y_up.glb#Scene0"));
})
.insert(CyberBike)
.insert(RigidBody::Dynamic)
.insert(CollisionShape::Cone {
half_height: 2.0,
radius: 0.8,
})
.insert(PlayerState::default());
}

49
src/input.rs Normal file
View file

@ -0,0 +1,49 @@
use bevy::{
app::{Events, ManualEventReader},
prelude::*,
};
#[derive(Default)]
pub(crate) struct InputState {
event_reader: ManualEventReader<GamepadEvent>,
pub pitch: f32,
pub yaw: f32,
pub throttle: f32,
pub brake: bool,
}
fn update_input(events: Res<Events<GamepadEvent>>, mut istate: ResMut<InputState>) {
for GamepadEvent(_, ev) in istate.event_reader.iter(&events) {
match *ev {
GamepadEventType::ButtonChanged(GamepadButtonType::RightTrigger2, val) => {
istate.throttle = val;
}
GamepadEventType::ButtonChanged(GamepadButtonType::LeftTrigger2, val) => {
istate.throttle = -val;
}
GamepadEventType::ButtonChanged(GamepadButtonType::East, val) => {
if val > 0.5 {
istate.brake = true;
} else {
istate.brake = false;
}
}
GamepadEventType::AxisChanged(GamepadAxisType::LeftStickX, val) => {
istate.yaw = -val;
}
GamepadEventType::AxisChanged(GamepadAxisType::LeftStickY, val) => {
istate.pitch = -val;
}
_ => {
info!("unhandled gamepad event: {:?}", ev);
}
}
}
}
pub struct CyberInputPlugin;
impl Plugin for CyberInputPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<InputState>().add_system(update_input);
}
}

View file

@ -1 +1,14 @@
pub mod flycam;
use bevy::prelude::{ResMut, Windows};
pub mod camera;
pub mod geometry;
pub mod input;
pub mod lights;
pub mod physics;
pub mod ui;
pub fn disable_mouse_trap(mut windows: ResMut<Windows>) {
let window = windows.get_primary_mut().unwrap();
window.set_cursor_lock_mode(false);
window.set_cursor_visibility(true);
}

209
src/lights.rs Normal file
View file

@ -0,0 +1,209 @@
use bevy::prelude::*;
use crate::geometry::PLANET_RADIUS;
pub const LIGHT_RANGE: f32 = PLANET_RADIUS * 0.6;
pub const LIGHT_DIST: f32 = PLANET_RADIUS * 1.2;
pub struct CyberSpaceLightsPlugin;
impl Plugin for CyberSpaceLightsPlugin {
fn build(&self, app: &mut App) {
app.add_startup_system(setup).add_system(animate_lights);
}
}
#[derive(Component)]
struct Animate;
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let red_light = PointLight {
intensity: 1_000.0,
range: LIGHT_RANGE,
color: Color::RED,
radius: 1.0,
..Default::default()
};
let blue_light = PointLight {
intensity: 1_000.0,
range: LIGHT_RANGE,
color: Color::BLUE,
radius: 1.0,
..Default::default()
};
let purple_light = PointLight {
intensity: 1_000.0,
range: LIGHT_RANGE,
color: Color::PURPLE,
radius: 1.0,
..Default::default()
};
commands.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 0.32,
});
// east light
commands
.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(LIGHT_DIST, 0.0, 0.0),
point_light: purple_light,
..Default::default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
radius: 10.0,
..Default::default()
})),
material: materials.add(StandardMaterial {
base_color: Color::PURPLE,
emissive: Color::rgba_linear(50.0, 0.0, 50.0, 0.0),
..Default::default()
}),
..Default::default()
});
})
.insert(Animate);
// west light
commands
.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(-LIGHT_DIST, 0.0, 0.0),
point_light: purple_light,
..Default::default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
radius: 10.0,
..Default::default()
})),
material: materials.add(StandardMaterial {
base_color: Color::PURPLE,
emissive: Color::rgba_linear(50.0, 0.0, 50.0, 0.0),
..Default::default()
}),
..Default::default()
});
})
.insert(Animate);
// north light
commands
.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(0.0, 0.0, LIGHT_DIST),
point_light: purple_light,
..Default::default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
radius: 10.0,
..Default::default()
})),
material: materials.add(StandardMaterial {
base_color: Color::PURPLE,
emissive: Color::rgba_linear(50.0, 0.0, 50.0, 0.0),
..Default::default()
}),
..Default::default()
});
})
.insert(Animate);
// south light
commands
.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(0.0, 0.0, -LIGHT_DIST),
point_light: purple_light,
..Default::default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
radius: 10.0,
..Default::default()
})),
material: materials.add(StandardMaterial {
base_color: Color::PURPLE,
emissive: Color::rgba_linear(50.0, 0.0, 50.0, 0.0),
..Default::default()
}),
..Default::default()
});
})
.insert(Animate);
// up light
commands
.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(0.0, LIGHT_DIST, 0.0),
point_light: red_light,
..Default::default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Icosphere {
radius: 10.0,
subdivisions: 2,
})),
material: materials.add(StandardMaterial {
base_color: Color::RED,
emissive: Color::rgba_linear(100.0, 0.0, 0.0, 0.0),
..Default::default()
}),
..Default::default()
});
});
// down light
commands
.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(0.0, -LIGHT_DIST, 0.0),
point_light: blue_light,
..Default::default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Icosphere {
radius: 10.0,
subdivisions: 2,
})),
material: materials.add(StandardMaterial {
base_color: Color::BLUE,
emissive: Color::rgba_linear(0.0, 0.0, 100.0, 0.0),
..Default::default()
}),
..Default::default()
});
});
}
fn animate_lights(
time: Res<Time>,
mut query: Query<&mut Transform, (With<PointLight>, With<Animate>)>,
) {
let dt = time.delta_seconds();
for mut transform in query.iter_mut() {
let translation = &transform.translation;
let x = translation.x;
let y = translation.y;
let z = if translation.z.abs() < 0.1 {
translation.z + 0.15
} else {
translation.z
};
let rads = 10.0f32.to_radians();
let anim_rate = rads * dt;
let theta = z.atan2(x) + anim_rate;
let x_new = LIGHT_DIST * theta.cos();
let z_new = LIGHT_DIST * theta.sin();
*transform = Transform::from_xyz(x_new, y, z_new);
}
}

View file

@ -1,247 +1,33 @@
use bevy::prelude::*;
use cyber_rider::flycam::{MovementSettings, PlayerPlugin, PLANET_RADIUS};
use heron::prelude::*;
use cyber_rider::{
camera::CyberCamPlugin,
disable_mouse_trap,
geometry::CyberGeomPlugin,
input::CyberInputPlugin,
lights::CyberSpaceLightsPlugin,
physics::{CyberPhysicsPlugin, MovementSettings},
ui::CyberUIPlugin,
};
const LIGHT_RANGE: f32 = PLANET_RADIUS * 0.6;
const LIGHT_DIST: f32 = PLANET_RADIUS * 1.2;
const MOVEMENT_SETTINGS: MovementSettings = MovementSettings {
sensitivity: 0.3, // default: 1.0
accel: 20.0, // default: 40.0
drag: 0.0001, // default: 0.0005
gravity: 10.0, // default: 10.0
};
fn main() {
App::new()
.insert_resource(Msaa { samples: 4 })
.add_plugins(DefaultPlugins)
.add_plugin(PlayerPlugin)
.add_plugin(PhysicsPlugin::default())
.insert_resource(MovementSettings {
sensitivity: 0.3, // default: 1.0
accel: 20.0, // default: 40.0
drag: 0.01, // default: 0.0005
gravity: 10.0, // default: 10.0
})
.add_startup_system(setup.label("world"))
.add_startup_system(disable_cursor.after("world"))
.add_system(animate_lights)
.add_plugin(CyberGeomPlugin)
.add_plugin(CyberInputPlugin)
.add_plugin(CyberPhysicsPlugin)
.insert_resource(MOVEMENT_SETTINGS)
.add_plugin(CyberCamPlugin)
.add_plugin(CyberSpaceLightsPlugin)
.add_plugin(CyberUIPlugin)
.add_startup_system(disable_mouse_trap)
.add_system(bevy::input::system::exit_on_esc_system)
.run();
}
fn disable_cursor(mut windows: ResMut<Windows>) {
let window = windows.get_primary_mut().unwrap();
window.set_cursor_lock_mode(false);
window.set_cursor_visibility(true);
}
#[derive(Component)]
struct Animate;
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let red_light = PointLight {
intensity: 1_000.0,
range: LIGHT_RANGE,
color: Color::RED,
radius: 10.0,
..Default::default()
};
let blue_light = PointLight {
intensity: 1_000.0,
range: LIGHT_RANGE,
color: Color::BLUE,
radius: 10.0,
..Default::default()
};
let purple_light = PointLight {
intensity: 1_000.0,
range: LIGHT_RANGE,
color: Color::PURPLE,
radius: 10.0,
..Default::default()
};
commands.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 0.32,
});
// world
commands
.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Icosphere {
radius: PLANET_RADIUS,
subdivisions: 6,
})),
material: materials.add(StandardMaterial {
base_color: Color::GRAY,
metallic: 0.7,
perceptual_roughness: 0.5,
..Default::default()
}),
..Default::default()
})
.insert(RigidBody::Static)
.insert(CollisionShape::Sphere {
radius: PLANET_RADIUS,
});
// east light
commands
.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(LIGHT_DIST, 0.0, 0.0),
point_light: purple_light,
..Default::default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
radius: 10.0,
..Default::default()
})),
material: materials.add(StandardMaterial {
base_color: Color::PURPLE,
emissive: Color::rgba_linear(50.0, 0.0, 50.0, 0.0),
..Default::default()
}),
..Default::default()
});
})
.insert(Animate);
// west light
commands
.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(-LIGHT_DIST, 0.0, 0.0),
point_light: purple_light,
..Default::default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
radius: 10.0,
..Default::default()
})),
material: materials.add(StandardMaterial {
base_color: Color::PURPLE,
emissive: Color::rgba_linear(50.0, 0.0, 50.0, 0.0),
..Default::default()
}),
..Default::default()
});
})
.insert(Animate);
// north light
commands
.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(0.0, 0.0, LIGHT_DIST),
point_light: purple_light,
..Default::default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
radius: 10.0,
..Default::default()
})),
material: materials.add(StandardMaterial {
base_color: Color::PURPLE,
emissive: Color::rgba_linear(50.0, 0.0, 50.0, 0.0),
..Default::default()
}),
..Default::default()
});
})
.insert(Animate);
// south light
commands
.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(0.0, 0.0, -LIGHT_DIST),
point_light: purple_light,
..Default::default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
radius: 10.0,
..Default::default()
})),
material: materials.add(StandardMaterial {
base_color: Color::PURPLE,
emissive: Color::rgba_linear(50.0, 0.0, 50.0, 0.0),
..Default::default()
}),
..Default::default()
});
})
.insert(Animate);
// up light
commands
.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(0.0, LIGHT_DIST, 0.0),
point_light: red_light,
..Default::default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Icosphere {
radius: 10.0,
subdivisions: 2,
})),
material: materials.add(StandardMaterial {
base_color: Color::RED,
emissive: Color::rgba_linear(100.0, 0.0, 0.0, 0.0),
..Default::default()
}),
..Default::default()
});
});
// down light
commands
.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(0.0, -LIGHT_DIST, 0.0),
point_light: blue_light,
..Default::default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Icosphere {
radius: 10.0,
subdivisions: 2,
})),
material: materials.add(StandardMaterial {
base_color: Color::BLUE,
emissive: Color::rgba_linear(0.0, 0.0, 100.0, 0.0),
..Default::default()
}),
..Default::default()
});
});
}
fn animate_lights(
time: Res<Time>,
mut query: Query<&mut Transform, (With<PointLight>, With<Animate>)>,
) {
let dt = time.delta_seconds();
for mut transform in query.iter_mut() {
let translation = &transform.translation;
let x = translation.x;
let y = translation.y;
let z = if translation.z.abs() < 0.1 {
translation.z + 0.15
} else {
translation.z
};
// 10 degrees == 0.174533 radians
let anim_rate = 0.174533 * dt;
let theta = z.atan2(x) + anim_rate;
let x_new = LIGHT_DIST * theta.cos();
let z_new = LIGHT_DIST * theta.sin();
*transform = Transform::from_xyz(x_new, y, z_new);
}
}

161
src/physics.rs Normal file
View file

@ -0,0 +1,161 @@
use bevy::prelude::*;
use heron::prelude::*;
use crate::{geometry::CyberBike, input::InputState};
/// Mouse sensitivity and movement speed
pub struct MovementSettings {
pub sensitivity: f32,
pub accel: f32,
pub drag: f32,
pub gravity: f32,
}
impl Default for MovementSettings {
fn default() -> Self {
Self {
sensitivity: 1.0,
accel: 40.,
drag: 0.0005,
gravity: 10.0,
}
}
}
#[derive(Component, Default)]
pub(crate) struct PlayerState {
pub velocity: Vec3,
pub colliding: bool,
}
fn falling_cat(time: Res<Time>, mut bike_query: Query<(&mut Transform, &CyberBike)>) {
let dt = time.delta_seconds();
let (mut bike_xform, _) = bike_query.single_mut();
let up = bike_xform.translation.normalize();
let cam_up = bike_xform.up();
let cos = up.dot(cam_up);
let theta = cos.acos();
let rate = if !theta.is_normal() {
0.0
} else if theta.is_sign_negative() {
-0.4
} else {
0.4
} * dt;
let angle = if rate.is_sign_negative() {
rate.max(theta)
} else {
rate.min(theta)
};
let rot = Quat::from_axis_angle(cam_up.cross(up).normalize(), angle);
if rot.is_finite() && theta.abs() > 0.0085 {
bike_xform.rotate(rot);
}
}
fn apply_velocity(
time: Res<Time>,
mut bike_query: Query<(&mut Transform, &PlayerState, &CyberBike)>,
) {
let dt = time.delta_seconds();
let (mut bike_xform, player_state, _) = bike_query.single_mut();
if player_state.velocity.is_finite() {
bike_xform.translation += player_state.velocity * dt;
}
}
fn update_player_vel(
time: Res<Time>,
settings: Res<MovementSettings>,
input: Res<InputState>,
mut query: Query<(&Transform, &mut PlayerState, &CyberBike)>,
) {
let dt = time.delta_seconds();
let (xform, mut pstate, _) = query.single_mut();
// first gravity
let down = -xform.translation.normalize();
let dvel = down * settings.gravity * dt;
let mut vel = if pstate.velocity.is_finite() {
pstate.velocity + dvel
} else {
dvel
};
// thrust or brake
let accel = xform.forward() * input.throttle * dt * settings.accel;
if pstate.velocity.is_finite() {
vel += accel;
} else {
vel = accel;
}
// brake
if input.brake {
let s = vel.length_squared();
if s < 0.05 {
vel = Vec3::ZERO;
} else {
vel -= vel.normalize() * settings.accel * dt;
}
}
// drag
let v2 = vel.length_squared().min(100_000.0);
if v2 < 0.05 {
vel = Vec3::ZERO;
} else {
let drag = vel * settings.drag * v2 * dt;
vel -= drag;
}
pstate.velocity = vel;
}
fn collision_detection(
mut events: EventReader<CollisionEvent>,
mut query: Query<&mut PlayerState>,
) {
let mut pstate = query.single_mut();
for event in events.iter() {
if let CollisionEvent::Started(_, _) = event {
pstate.colliding = true;
} else {
pstate.colliding = false;
}
}
}
fn collision_reaction(mut query: Query<(&mut PlayerState, &Transform)>) {
let (mut pstate, xform) = query.single_mut();
let down = -xform.translation.normalize();
// now see if we're currently colliding
if pstate.colliding {
let vel = pstate.velocity;
let dvel = down * vel.dot(down) * 1.02;
pstate.velocity -= dvel;
}
}
pub struct CyberPhysicsPlugin;
impl Plugin for CyberPhysicsPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<PlayerState>()
.init_resource::<MovementSettings>()
.add_plugin(PhysicsPlugin::default())
.add_system(collision_detection)
.add_system(collision_reaction)
.add_system(falling_cat)
.add_system(update_player_vel)
.add_system(apply_velocity);
}
}

47
src/ui.rs Normal file
View file

@ -0,0 +1,47 @@
use bevy::prelude::*;
use crate::physics::PlayerState;
#[derive(Component)]
struct UpText;
fn setup_ui(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn_bundle(UiCameraBundle::default());
commands
.spawn_bundle(TextBundle {
style: Style {
align_self: AlignSelf::FlexEnd,
..Default::default()
},
// Use `Text` directly
text: Text {
// Construct a `Vec` of `TextSection`s
sections: vec![TextSection {
value: "".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 40.0,
color: Color::GOLD,
},
}],
..Default::default()
},
..Default::default()
})
.insert(UpText);
}
fn update_ui(pstate_query: Query<&PlayerState>, mut text_query: Query<&mut Text, With<UpText>>) {
let mut text = text_query.single_mut();
let pstate = pstate_query.single();
text.sections[0].value = format!("{:.2}", pstate.velocity.length());
}
pub struct CyberUIPlugin;
impl Plugin for CyberUIPlugin {
fn build(&self, app: &mut App) {
app.add_startup_system(setup_ui).add_system(update_ui);
}
}