lyrix/src/character_controller.rs

280 lines
9.1 KiB
Rust

use bevy::{prelude::*, render::camera::Camera};
use bevy_physimple::prelude::*;
#[derive(Default)]
pub struct Player {
double_jump: bool,
on_wall: Option<Vec2>,
on_floor: bool,
}
pub struct Gravity(Vec2);
pub struct ModManager;
impl Plugin for ModManager {
fn build(&self, app: &mut AppBuilder) {
app // Basic setup of the app
.add_plugin(Physics2dPlugin)
// .add_system(bevy::input::system::exit_on_esc_system.system())
.add_startup_system(setup.system())
.add_system(controller_on_stuff.system())
.add_system(character_system.system())
.add_system(change_sensor_color.system())
.add_system(gravity.system())
.add_system(ray_head.system());
}
}
#[derive(Debug)]
pub struct PlayerCam;
fn setup(mut coms: Commands, mut mats: ResMut<Assets<ColorMaterial>>, a_server: Res<AssetServer>) {
let wall = mats.add(Color::BLACK.into());
// insert a gravity struct
coms.insert_resource(Gravity(Vec2::new(0.0, -540.0)));
// Spawn character
coms.spawn_bundle(SpriteBundle {
sprite: Sprite::new(Vec2::splat(28.0)),
material: mats.add(Color::ALICE_BLUE.into()),
..Default::default()
})
.insert_bundle(KinematicBundle {
shape: CollisionShape::Square(Square::size(Vec2::splat(28.0))),
..Default::default()
})
.insert(Player::default());
// Spawn the damn camera
// coms.spawn_bundle(OrthographicCameraBundle::new_2d());
/*coms.spawn_bundle(OrthographicCameraBundle::new_2d())
.insert(PlayerCam);*/
// Controls
let style = TextStyle {
font: a_server.load("fonts/Roboto/Roboto-Black.ttf"),
font_size: 32.0,
color: Color::ANTIQUE_WHITE,
};
let alignment = TextAlignment {
vertical: VerticalAlign::Bottom,
horizontal: HorizontalAlign::Left,
};
let text = "A/D - Movement\nSpace/W - Jump/Double jump\nS - Stomp(when mid air)";
coms.spawn_bundle(Text2dBundle {
text: Text::with_section(text, style, alignment),
transform: Transform::from_xyz(-270.0, 360.0, 0.0),
..Default::default()
});
coms.spawn_bundle(OrthographicCameraBundle::new_2d());
// center floor
coms.spawn_bundle(SpriteBundle {
sprite: Sprite::new(Vec2::new(2000.0, 230.0)),
material: wall.clone(),
transform: Transform::from_xyz(0.0, -400.0, 0.0),
..Default::default()
})
.insert_bundle(StaticBundle {
shape: CollisionShape::Square(Square::size(Vec2::new(2000.0, 230.0))),
..Default::default()
});
// side wall
coms.spawn_bundle(SpriteBundle {
sprite: Sprite::new(Vec2::new(40.0, 300.0)),
material: wall.clone(),
transform: {
let mut t = Transform::from_xyz(450.0, 0.0, 0.0);
t.rotation = Quat::from_rotation_z(-0.1 * 3.14);
t
},
..Default::default()
})
.insert_bundle(StaticBundle {
shape: CollisionShape::Square(Square::size(Vec2::new(40.0, 300.0))),
..Default::default()
});
// smaller other side wall
coms.spawn_bundle(SpriteBundle {
sprite: Sprite::new(Vec2::new(30.0, 90.0)),
material: wall.clone(),
transform: Transform::from_xyz(-150.0, -160.0, 0.0),
..Default::default()
})
.insert_bundle(StaticBundle {
shape: CollisionShape::Square(Square::size(Vec2::new(30.0, 90.0))),
..Default::default()
});
// Floating platform
coms.spawn_bundle(SpriteBundle {
sprite: Sprite::new(Vec2::new(200.0, 30.0)),
material: wall.clone(),
transform: Transform::from_xyz(-150.0, 0.0, 0.0),
..Default::default()
})
.insert_bundle(StaticBundle {
shape: CollisionShape::Square(Square::size(Vec2::new(200.0, 30.0))),
..Default::default()
});
// Spawn the sensor
const SENSOR_SIZE: f32 = 50.0;
coms.spawn_bundle(SpriteBundle {
sprite: Sprite::new(Vec2::splat(SENSOR_SIZE)),
material: mats.add(Color::GOLD.into()),
transform: Transform::from_xyz(30.0, -150.0, 0.0),
..Default::default()
})
.insert_bundle(SensorBundle {
shape: CollisionShape::Square(Square::size(Vec2::splat(SENSOR_SIZE))),
..Default::default()
});
// Spawn another cube which we will try to push or something
const CUBE_SIZE: f32 = 35.0;
coms.spawn_bundle(SpriteBundle {
sprite: Sprite::new(Vec2::splat(CUBE_SIZE)),
material: mats.add(Color::CRIMSON.into()),
transform: Transform::from_xyz(100.0, 0.0, 0.0),
..Default::default()
})
.insert_bundle(KinematicBundle {
shape: CollisionShape::Square(Square::size(Vec2::splat(CUBE_SIZE))),
..Default::default()
});
}
fn gravity(time: Res<Time>, grav: Res<Gravity>, mut q: Query<&mut Vel>) {
// Since the lib itself doesnt take care of gravity(for obv reasons) we need to do it here
let g = grav.0;
let t = time.delta_seconds();
for mut v in q.iter_mut() {
v.0 += t * g;
}
}
fn controller_on_stuff(
mut query: Query<(Entity, &mut Player)>,
mut colls: EventReader<CollisionEvent>,
) {
// Iterate over the collisions and check if the player is on a wall/floor
let (e, mut c) = query
.single_mut()
.expect("should be only one player :shrug:");
// clear the current data on c
c.on_floor = false;
c.on_wall = None;
for coll in colls.iter().filter(|&c| c.is_b_static) {
if coll.entity_a == e {
let n = coll.normal.dot(Vec2::Y);
if n > 0.7 {
c.on_floor = true;
} else if n.abs() <= 0.7 {
c.on_wall = Some(coll.normal);
}
}
}
}
fn character_system(
input: Res<Input<KeyCode>>,
time: Res<Time>,
gravity: Res<Gravity>,
mut query: Query<(&mut Player, &mut Vel)>,
) {
let gravity = gravity.0;
for (mut controller, mut vel) in query.iter_mut() {
if let Some(normal) = controller.on_wall {
// If we are colliding with a wall, make sure to stick
vel.0 -= normal * 0.1;
// and limit our speed downwards
if vel.0.y < -1.0 {
vel.0.y = -1.0;
}
}
// There are 2 places in which we apply a jump, so i made a little colsure for code reusability
let jump = |body: &Player, vel: &mut Vel| {
vel.0 = vel.0.slide(gravity.normalize()) - gravity * 0.6;
let wall = body.on_wall.unwrap_or(Vec2::ZERO) * 250.0;
vel.0 += wall;
};
let should_jump = input.just_pressed(KeyCode::Space) || input.just_pressed(KeyCode::W);
if controller.on_floor || controller.on_wall.is_some() {
controller.double_jump = true;
if should_jump {
jump(&controller, &mut vel);
}
} else if controller.double_jump && should_jump {
controller.double_jump = false;
jump(&controller, &mut vel);
}
// This is for the testing purpose of the continuous collision - aka "The Stomp"
if input.just_pressed(KeyCode::S) && !controller.on_floor {
vel.0 = Vec2::new(0.0, -500.0);
}
// REMINDER: Dont forget to multiply by `time.delta_seconds()` when messing with movement
let acc = Vec2::new(1000.0, 0.0) * time.delta_seconds();
if input.pressed(KeyCode::A) {
vel.0 -= acc;
} else if input.pressed(KeyCode::D) {
vel.0 += acc;
} else {
// This is not a good way to do friction
vel.0.x *= 1.0 - (10.0 * time.delta_seconds());
}
// terminal velocity
const TERMINAL_X: f32 = 500.0;
if vel.0.x.abs() > TERMINAL_X {
vel.0.x = TERMINAL_X.copysign(vel.0.x); // you can also do `TERMINAL_X * vel.0.x.signum()`
}
}
}
fn change_sensor_color(
mut materials: ResMut<Assets<ColorMaterial>>,
q: Query<(&Sensor, &Handle<ColorMaterial>)>,
) {
// Simply change the color of the sensor if something is inside it
for (s, h) in q.iter() {
if let Some(mut m) = materials.get_mut(h) {
m.color = if s.bodies.len() == 0 {
Color::GOLD
} else {
Color::ALICE_BLUE
}
}
}
}
fn ray_head(
mut ts: Query<&mut Transform, Without<RayCast>>,
q: Query<(&RayCast, &Children, &Transform)>,
) {
for (r, c, rt) in q.iter() {
if let Some(c) = c.first() {
if let Ok(mut t) = ts.get_mut(*c) {
// We use the offset in the `unwrap_or` because we want to offset the position to be where the ray "ends"
// while in the `map`(and `pos` by extension) we want the position relative to the transform component
// since `a.collision_point` is in global space
let pos = Vec2::new(rt.translation.x, rt.translation.y);
t.translation = r
.collision
.map(|a| a.collision_point - pos)
.unwrap_or(r.cast + r.offset)
.extend(0.0);
}
}
}
}