From 7609eb3e4b8c87b5b3801bfc09c88430c7eb7b9b Mon Sep 17 00:00:00 2001 From: SimonFJ20 Date: Wed, 10 Apr 2024 15:15:59 +0200 Subject: [PATCH] collision so hawd >~< --- src/engine.rs | 51 +++++- src/main.rs | 323 ++++++++++++++++++++++++++++-------- textures/literally_dprk.png | Bin 0 -> 21499 bytes 3 files changed, 292 insertions(+), 82 deletions(-) create mode 100644 textures/literally_dprk.png diff --git a/src/engine.rs b/src/engine.rs index dd6da21..dd82e9c 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,6 +3,7 @@ use std::collections::HashSet; use std::rc::Rc; use std::time::{Duration, Instant}; +pub use component_macro::Component; pub use sdl2::keyboard::Keycode; use sdl2::{ @@ -69,19 +70,19 @@ where currently_pressed_keys: &'context HashSet, } -pub struct Quwi(std::marker::PhantomData); +pub struct ComponentQuery(std::marker::PhantomData); -impl Quwi { +impl ComponentQuery { pub fn new() -> Self { Self(std::marker::PhantomData) } } -pub trait QuwiQuwi { +pub trait QueryRunner { fn run(&self, context: &Context) -> Vec; } -impl QuwiQuwi for Quwi +impl QueryRunner for ComponentQuery where T0: 'static + Component, { @@ -90,7 +91,7 @@ where } } -impl QuwiQuwi for Quwi<(T0, T1)> +impl QueryRunner for ComponentQuery<(T0, T1)> where T0: 'static + Component, T1: 'static + Component, @@ -104,7 +105,7 @@ where } } -impl QuwiQuwi for Quwi<(T0, T1, T2)> +impl QueryRunner for ComponentQuery<(T0, T1, T2)> where T0: 'static + Component, T1: 'static + Component, @@ -123,6 +124,33 @@ where } } +#[macro_export] +macro_rules! query { + ($ctx:expr, $t:ty) => { + { + use engine::QueryRunner; + engine::ComponentQuery::<$t>::new().run($ctx) + } + }; + ($ctx:expr, $($ts:ty),+) => { + { + #[allow(unused_imports)] + use engine::QueryRunner; + engine::ComponentQuery::<($($ts),+)>::new().run($ctx) + } + }; +} + +#[macro_export] +macro_rules! spawn { + ($ctx:expr, [$($ts:expr),+ $(,)?]) => { + engine::Context::spawn($ctx, vec![$(Box::new($ts)),+]) + }; + ($ctx:expr, $($ts:expr),+ $(,)?) => { + engine::Context::spawn($ctx, vec![$(Box::new($ts)),+]) + }; +} + impl<'context, 'game> Context<'context, 'game> { pub fn entities_with_component(&self) -> Vec { let entity_type_id = TypeId::of::(); @@ -203,9 +231,9 @@ impl<'context, 'game> Context<'context, 'game> { .collect(); } - pub fn add_system(&mut self, system: Rc) { + pub fn add_system(&mut self, system: S) { system.on_add(self); - self.systems.push(system) + self.systems.push(Rc::new(system)) } pub fn key_pressed(&self, keycode: Keycode) -> bool { @@ -248,7 +276,7 @@ impl<'game> Game<'game> { let mut canvas = window.into_canvas().build()?; let texture_creator = canvas.texture_creator(); - canvas.set_draw_color(Color::RGB(60, 180, 180)); + canvas.set_draw_color(Color::BLACK); canvas.clear(); canvas.present(); let event_pump = sdl_context.event_pump()?; @@ -317,4 +345,9 @@ impl<'game> Game<'game> { currently_pressed_keys: &mut self.currently_pressed_keys, } } + + pub fn add_system(&mut self, system: S) { + system.on_add(&mut self.context()); + self.systems.push(Rc::new(system)) + } } diff --git a/src/main.rs b/src/main.rs index 71cfdb6..4f421d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,63 +2,210 @@ mod engine; -use component_macro::Component; -use engine::Component; -use engine::QuwiQuwi; -use engine::System; -use std::rc::Rc; +use engine::{Component, System}; #[derive(Component)] struct Sprite { sprite: engine::Sprite, } -macro_rules! tuplify { - ($t:ty) => { - $t - }; - ($t:ty, $($ts:ty),+) => { - $t, tuplify!($($ts),+) - }; +#[derive(Component, Default, Clone)] +struct RigidBody { + pos: (f64, f64), + vel: (f64, f64), + gravity: bool, + cowision: bool, } -macro_rules! run_quwi { - ($ctx:expr, $t:ty) => { - engine::Quwi::<$t>::new().run($ctx) - }; - ($ctx:expr, $t:ty, $($ts:ty),+) => { - engine::Quwi::<($t, tuplify!($($ts),+))>::new().run($ctx) - }; -} - -#[derive(Component)] -struct Position(f64, f64); - -#[derive(Clone, Component)] -struct Velocity(f64, f64); - struct VelocitySystem; impl System for VelocitySystem { fn on_update(&self, ctx: &mut engine::Context, delta: f64) -> Result<(), engine::Error> { - for id in run_quwi!(ctx, Velocity, Position) { - let vel = ctx.entity_component::(id).clone(); - let Position(x, y) = ctx.entity_component::(id); - *x += vel.0 * delta; - *y += vel.1 * delta; + for id in query!(ctx, RigidBody) { + let body = ctx.entity_component::(id); + body.pos.0 += body.vel.0 * delta; + body.pos.1 += body.vel.1 * delta; } Ok(()) } } -#[derive(Component)] -struct Gravity; +#[derive(Component, Clone)] +struct Rect { + width: f64, + height: f64, +} +impl Rect { + pub fn rect_collides(&self, pos: (f64, f64), other: &Rect, other_pos: (f64, f64)) -> bool { + pos.0 + self.width > other_pos.0 + && pos.0 <= other_pos.0 + other.width + && pos.1 + self.height > other_pos.1 + && pos.1 <= other_pos.1 + other.height + } + + pub fn point_collides(&self, pos: (f64, f64), point: (f64, f64)) -> bool { + pos.0 + self.width > point.0 + && pos.0 <= point.0 + && pos.1 + self.height > point.1 + && pos.1 <= point.1 + } +} +// +// enum CollisionGroup { +// Player, +// World, +// } +// +// impl CollisionGroup { +// pub fn should_collide(&self, other: &Self) -> bool { +// match (self, other) { +// (CollisionGroup::Player, CollisionGroup::Player) => todo!(), +// (CollisionGroup::Player, CollisionGroup::World) => todo!(), +// (CollisionGroup::World, CollisionGroup::Player) => todo!(), +// (CollisionGroup::World, CollisionGroup::World) => todo!(), +// } +// } +// } +// + +#[derive(Component, Clone, Default)] +struct Collider { + resolve: bool, +} + +fn horizontal_line_intersect(p0: (f64, f64), vel: (f64, f64), line_y: f64) -> (f64, f64) { + // y = ax + b + let a = vel.1 / vel.0; + let b = p0.1 - p0.0 * a; + + let x = (line_y - b) / a; + (x, line_y) +} + +fn vertical_line_intersect(p0: (f64, f64), vel: (f64, f64), line_x: f64) -> (f64, f64) { + // y = ax + b + let a = vel.1 / vel.0; + let b = p0.1 - p0.0 * a; + + let y = a * line_x + b; + (line_x, y) +} + +enum Surface { + Top, + Bottom, + Left, + Right, +} + +fn point_distance(a: (f64, f64), b: (f64, f64)) -> f64 { + ((a.0 - b.0).abs().powi(2) + (a.1 - b.1).abs().powi(2)).sqrt() +} + +fn closest_surface_for_point_and_rectangle_and_your_mom( + p0: (f64, f64), + vel: (f64, f64), + rect_pos: (f64, f64), + rect: &Rect, +) -> Option<(f64, Surface)> { + [ + (horizontal_line_intersect(p0, vel, rect_pos.1), Surface::Top), + ( + horizontal_line_intersect(p0, vel, rect_pos.1 + rect.height), + Surface::Bottom, + ), + (vertical_line_intersect(p0, vel, rect_pos.0), Surface::Left), + ( + horizontal_line_intersect(p0, vel, rect_pos.0 + rect.width), + Surface::Right, + ), + ] + .into_iter() + .filter(|(point, _)| rect.point_collides(rect_pos, *point)) + .map(|(point, surface)| (point_distance(p0, point), surface)) + .min_by(|(dist_a, _), (dist_b, _)| dist_a.total_cmp(&dist_b)) +} + +struct CollisionSystem; +impl System for CollisionSystem { + fn on_update(&self, ctx: &mut engine::Context, delta: f64) -> Result<(), engine::Error> { + for id in query!(ctx, RigidBody, Rect, Collider) { + let collider = ctx.entity_component::(id).clone(); + if !collider.resolve { + continue; + } + let body = ctx.entity_component::(id).clone(); + let rect = ctx.entity_component::(id).clone(); + for other_id in query!(ctx, RigidBody, Rect, Collider) { + if id == other_id { + continue; + } + let other_rect = ctx.entity_component::(id).clone(); + let other_body = ctx.entity_component::(id).clone(); + if rect.rect_collides(body.pos, &other_rect, other_body.pos) { + println!("collider vi?"); + let last_pos = ( + body.pos.0 - body.vel.0 * delta, + body.pos.1 - body.vel.1 * delta, + ); + let closest_surface = [ + (last_pos.0, last_pos.1), + (last_pos.0, last_pos.1 + rect.height), + (last_pos.0 + rect.width, last_pos.1), + (last_pos.0 + rect.width, last_pos.1 + rect.height), + ] + .into_iter() + .map(|p0| { + closest_surface_for_point_and_rectangle_and_your_mom( + p0, + body.vel, + other_body.pos, + &other_rect, + ) + }) + .filter_map(std::convert::identity) + .min_by(|(dist_a, _), (dist_b, _)| dist_a.total_cmp(&dist_b)) + .map(|(_, surface)| surface) + .ok_or_else(|| "we already checked if collision happens")?; + + let body = ctx.entity_component::(id); + match closest_surface { + Surface::Top => { + body.vel.1 = 0.0; + body.pos.1 = other_body.pos.1 - rect.height; + } + Surface::Bottom => { + body.vel.1 = 0.0; + body.pos.1 = other_body.pos.1 + other_rect.height; + } + Surface::Left => { + body.vel.0 = 0.0; + body.pos.0 = other_body.pos.0 + other_rect.width; + } + Surface::Right => { + body.vel.0 = 0.0; + body.pos.0 = other_body.pos.0 - rect.width; + } + } + } + } + } + Ok(()) + } +} struct GravitySystem; impl System for GravitySystem { fn on_update(&self, ctx: &mut engine::Context, delta: f64) -> Result<(), engine::Error> { - for id in run_quwi!(ctx, Gravity, Velocity) { - let Velocity(_, y) = ctx.entity_component::(id); - *y = if *y < 800.0 { *y + 400.0 * delta } else { *y }; + for id in query!(ctx, RigidBody) { + let body = ctx.entity_component::(id); + if !body.gravity { + continue; + } + body.vel.1 = if body.vel.1 < 800.0 { + body.vel.1 + 400.0 * delta + } else { + body.vel.1 + }; } Ok(()) } @@ -68,28 +215,34 @@ impl System for GravitySystem { struct Cloud; struct CloudSystem; - impl System for CloudSystem { fn on_update(&self, ctx: &mut engine::Context, delta: f64) -> Result<(), engine::Error> { let cloud_amount = ctx.entities_with_component::().len(); if cloud_amount < 1 { let cloud = ctx.load_sprite("textures/clouds.png").unwrap(); - ctx.spawn(vec![ - Box::new(Cloud), - Box::new(Sprite { sprite: cloud }), - Box::new(Position(-100.0, 150.0)), - Box::new(Velocity(0.0, 0.0)), - ]); + spawn!( + ctx, + Cloud, + Sprite { sprite: cloud }, + RigidBody { + pos: (-100.0, 150.0), + ..Default::default() + }, + ); } - for id in run_quwi!(ctx, Cloud, Velocity) { - let Velocity(x, _) = ctx.entity_component::(id); - *x = if *x < 200.0 { *x + 200.0 * delta } else { *x }; + for id in query!(ctx, Cloud, RigidBody) { + let body = ctx.entity_component::(id); + body.vel.0 = if body.vel.0 < 200.0 { + body.vel.0 + 200.0 * delta + } else { + body.vel.0 + }; } - for id in run_quwi!(ctx, Cloud, Velocity) { - let Position(x, _) = ctx.entity_component::(id); - if *x > 1400.0 { + for id in query!(ctx, Cloud, RigidBody) { + let body = ctx.entity_component::(id); + if body.pos.0 > 1400.0 { ctx.despawn(id); } } @@ -100,11 +253,11 @@ impl System for CloudSystem { struct SpriteRenderer; impl System for SpriteRenderer { fn on_update(&self, ctx: &mut engine::Context, _delta: f64) -> Result<(), engine::Error> { - for id in run_quwi!(ctx, Sprite, Position) { - let &mut Position(x, y) = ctx.entity_component::(id); + for id in query!(ctx, Sprite, RigidBody) { + let body = ctx.entity_component::(id).clone(); let sprite = ctx.entity_component::(id).sprite; - ctx.draw_sprite(sprite, x as i32, y as i32)?; + ctx.draw_sprite(sprite, body.pos.0 as i32, body.pos.1 as i32)?; } Ok(()) } @@ -116,11 +269,11 @@ struct PlayerMovement; struct PlayerMovementSystem; impl System for PlayerMovementSystem { fn on_update(&self, ctx: &mut engine::Context, _delta: f64) -> Result<(), engine::Error> { - for id in run_quwi!(ctx, PlayerMovement, Velocity) { + for id in query!(ctx, PlayerMovement, RigidBody) { let d_down = ctx.key_pressed(engine::Keycode::D); let a_down = ctx.key_pressed(engine::Keycode::A); - let Velocity(x, _) = ctx.entity_component::(id); - *x = if d_down && !a_down { + let body = ctx.entity_component::(id); + body.vel.0 = if d_down && !a_down { 400.0 } else if !d_down && a_down { -400.0 @@ -136,25 +289,49 @@ fn main() { let mut game = engine::Game::new().unwrap(); let mut context = game.context(); - context.add_system(Rc::new(VelocitySystem)); - context.add_system(Rc::new(SpriteRenderer)); - context.add_system(Rc::new(GravitySystem)); - context.add_system(Rc::new(PlayerMovementSystem)); - context.add_system(Rc::new(CloudSystem)); + context.add_system(VelocitySystem); + context.add_system(CollisionSystem); + context.add_system(SpriteRenderer); + context.add_system(GravitySystem); + context.add_system(PlayerMovementSystem); + context.add_system(CloudSystem); let player = context.load_sprite("textures/player.png").unwrap(); - let background = context.load_sprite("textures/mountains.png").unwrap(); + let background = context.load_sprite("textures/literally_dprk.png").unwrap(); - context.spawn(vec![ - Box::new(Sprite { sprite: background }), - Box::new(Position(0.0, 0.0)), - ]); + spawn!( + &mut context, + Sprite { sprite: background }, + RigidBody::default(), + ); + + spawn!( + &mut context, + Sprite { sprite: player }, + RigidBody { + pos: (400.0, 400.0), + gravity: true, + ..Default::default() + }, + Rect { + width: 256.0, + height: 256.0 + }, + Collider { resolve: true }, + PlayerMovement, + ); + + spawn!( + &mut context, + RigidBody { + pos: (0.0, 650.0), + ..Default::default() + }, + Rect { + width: 1000.0, + height: 25.0 + }, + Collider { resolve: false }, + ); - context.spawn(vec![ - Box::new(Sprite { sprite: player }), - Box::new(Position(16.0, 500.0)), - Box::new(Velocity(0.0, -600.0)), - Box::new(Gravity), - Box::new(PlayerMovement), - ]); game.run(); } diff --git a/textures/literally_dprk.png b/textures/literally_dprk.png new file mode 100644 index 0000000000000000000000000000000000000000..47308c9b59dc0453f9333dd0f1d777d64fbc642d GIT binary patch literal 21499 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 CQjhuo literal 0 HcmV?d00001