diff --git a/src/main.rs b/src/main.rs index ef3f61d..2770317 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod assets; mod dashboard; mod editor; mod mapping; +mod objectmap; mod physics; mod tilemap; mod util; @@ -69,6 +70,7 @@ fn main() { editor::Plugin, ecs_tilemap::TilemapPlugin, mapping::Plugin, + objectmap::Plugin, tilemap::TiledMapPlugin, dashboard::Plugin, )) @@ -92,6 +94,7 @@ fn main() { .after(physics::apply_velocity) .after(handle_keyboard) .after(handle_ai_players), + physics::fixed_collision_detection.after(physics::collision_detection), ), ) .run(); diff --git a/src/objectmap.rs b/src/objectmap.rs new file mode 100644 index 0000000..24cfc8e --- /dev/null +++ b/src/objectmap.rs @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2023-2024 Daniel Thompson + +#![allow(clippy::type_complexity)] + +use bevy::{ + log, + math::{vec2, vec3}, + prelude::*, +}; + +use crate::tilemap; + +#[derive(Default)] +pub struct Plugin; + +impl bevy::app::Plugin for Plugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, (handle_map_events,)); + } +} + +#[derive(Component, Debug)] +pub enum Collider { + Tree, + Block, +} + +fn spawn_object( + map: &tiled::Map, + obj: &tiled::Object, + img: &tiled::Image, + commands: &mut Commands, + texture_atlas: &mut Assets, + asset_server: &AssetServer, +) { + let (w, h) = ( + (map.width * map.tile_width) as f32, + (map.height * map.tile_height) as f32, + ); + let (x, y) = ( + obj.x - ((w - img.width as f32) / 2.0), + -obj.y + ((h + img.height as f32) / 2.0), + ); + let mut path = std::path::PathBuf::from("embedded://"); + path.push(&img.source); + + let atlas = TextureAtlas::from_grid( + asset_server.load(path.to_str().expect("tile_path is not UTF-8").to_string()), + vec2(img.width as f32, img.height as f32), + 1, + 1, + None, + None, + ); + + commands.spawn(( + if img.source.to_str().unwrap().contains("tree") { + Collider::Tree + } else { + Collider::Block + }, + SpriteSheetBundle { + texture_atlas: texture_atlas.add(atlas), + transform: Transform { + translation: vec3(x, y, 5.0), + scale: Vec3::splat(1.), + ..default() + }, + ..default() + }, + )); +} + +/// Grub about in the bowels of the tiled data, iterating over each +/// object and trying to figure out what sprite to create. +/// +/// Once we finish wading through the tiled data we call out to +/// `spawn_object()` to do the bevy actions! +fn spawn_objects( + map: &tiled::Map, + commands: &mut Commands, + texture_atlas: &mut Assets, + asset_server: &AssetServer, +) { + let layer = map + .layers() + .find(|layer| layer.name == "Objects") + .and_then(|layer| layer.as_object_layer()); + + if let Some(layer) = layer { + for object in layer.objects() { + let tile_data = object.tile_data(); + if tile_data.is_none() { + log::error!("Tile data is missing"); + continue; + } + let tile_data = tile_data.unwrap(); + + let tileset = tile_data.tileset_location(); + if let tiled::TilesetLocation::Map(tileset) = tileset { + let id = tile_data.id(); + let tile = map.tilesets()[*tileset].get_tile(id); + + if let Some(tile) = tile { + if let Some(image) = &tile.image { + spawn_object(map, &object, image, commands, texture_atlas, asset_server); + } else { + log::error!("Tile image missing from tile data"); + } + } else { + log::error!("Tile id missing from tile data"); + } + } else { + log::error!("Tile data isn't using a map ID as the tileset location"); + } + } + } +} + +pub fn handle_map_events( + mut map_events: EventReader>, + maps: Res>, + mut commands: Commands, + mut texture_atlas: ResMut>, + asset_server: Res, +) { + for event in map_events.read() { + match event { + AssetEvent::Added { id } => { + if let Some(map) = maps.get(*id) { + spawn_objects(&map.map, &mut commands, &mut texture_atlas, &asset_server); + } + } + _ => continue, + } + } +} diff --git a/src/physics.rs b/src/physics.rs index 969c385..ced6e42 100644 --- a/src/physics.rs +++ b/src/physics.rs @@ -7,7 +7,7 @@ use bevy::{math::vec2, prelude::*}; use slicetools::*; use std::f32::consts::PI; -use crate::{mapping, util::IteratorToArrayExt, Preferences}; +use crate::{mapping, objectmap, util::IteratorToArrayExt, Preferences}; #[derive(Component, Debug, Reflect)] pub struct Velocity(pub Vec2); @@ -69,6 +69,26 @@ fn same_side(p1: Vec2, p2: Vec2, line: (Vec2, Vec2)) -> bool { cp1.dot(cp2) >= 0.0 } +/// Calculate the length of a line between two points. +/// +/// This is is a simple application of the Pythagorean theorem. +fn length_of_line(line: (Vec2, Vec2)) -> f32 { + ((line.1.x - line.0.x).powi(2) + (line.1.y - line.0.y).powi(2)).sqrt() +} + +/// Calculate the area of a triangle defined by three points. +fn area_of_triangle(triangle: (Vec2, Vec2, Vec2)) -> f32 { + (((triangle.0.x - triangle.2.x) * (triangle.1.y - triangle.0.y)) + - ((triangle.0.x - triangle.1.x) * (triangle.2.y - triangle.0.y))) + .abs() + / 2.0 +} + +/// Calculate the shortest distance from the point to a line. +fn distance_to_line(pt: Vec2, line: (Vec2, Vec2)) -> f32 { + 2.0 * area_of_triangle((pt, line.0, line.1)) / length_of_line(line) +} + fn point_in_polygon(pt: Vec2, shape: &[Vec2]) -> bool { let n = shape.len(); shape @@ -82,11 +102,31 @@ fn point_in_polygon(pt: Vec2, shape: &[Vec2]) -> bool { .all(|x| same_side(pt, x[0], (x[1], x[2]))) } -pub struct CollisionBox { - points: [Vec2; 8], +fn closest_edge_to_point(pt: Vec2, shape: &[Vec2]) -> (Vec2, Vec2) { + let n = shape.len(); + shape + .windows(2) + .chain(std::iter::once([shape[n - 1], shape[0]].as_slice())) + .map(|line| (line[0], line[1])) + .min_by(|a, b| { + distance_to_line(pt, *a) + .partial_cmp(&distance_to_line(pt, *b)) + .expect("Floating point numbers must be comparable") + }) + .expect("Shape must not be empty") +} + +fn reflect_against_line(v: Vec2, line: (Vec2, Vec2)) -> Vec2 { + let normal = (line.1 - line.0).perp().normalize(); + + v - ((2.0 * v.dot(normal)) * normal) +} + +pub struct CollisionBox { + points: [Vec2; L], } -impl CollisionBox { +impl CollisionBox<8> { pub fn from_transform(tf: &Transform, sz: &Vec2) -> Self { let w = sz.x * 0.5; let h = sz.y * 0.5; @@ -117,9 +157,11 @@ impl CollisionBox { .to_array(), } } +} +impl CollisionBox { /// Test whether two rectangles are touching. - pub fn is_touching(&self, other: &CollisionBox) -> bool { + pub fn is_touching(&self, other: &CollisionBox) -> bool { other .points .iter() @@ -134,7 +176,7 @@ impl CollisionBox { for w in self.points.windows(2) { gizmos.line_2d(w[0], w[1], Color::BLUE); } - gizmos.line_2d(self.points[7], self.points[0], Color::BLUE); + gizmos.line_2d(self.points[L - 1], self.points[0], Color::BLUE); } } @@ -145,36 +187,124 @@ pub fn collision_detection( mut gizmos: Gizmos, ) { let mut colliders = query.iter_mut().collect::>(); - let mut it = colliders.pairs_mut(); - while let Some((a, b)) = it.next() { - let atx = match texture_atlases.get(a.1) { - Some(tx) => tx, - None => continue, - }; - let btx = match texture_atlases.get(b.1) { - Some(tx) => tx, - None => continue, + let mut pairs = colliders.pairs_mut(); + // pairs_mut() does not return an iterator (due to borrowing rules) but we + // create a similar loop using while-let + while let Some(((atf, atx, av), (btf, btx, bv))) = pairs.next() { + let (atx, btx) = match (texture_atlases.get(*atx), texture_atlases.get(*btx)) { + (Some(atx), Some(btx)) => (atx, btx), + _ => continue, }; - let mut abox = CollisionBox::from_transform(&a.0, &atx.size); - let mut bbox = CollisionBox::from_transform(&b.0, &btx.size); + let mut abox = CollisionBox::from_transform(&atf, &atx.size); + let mut bbox = CollisionBox::from_transform(&btf, &btx.size); if prefs.debug_low() { abox.draw(&mut gizmos); bbox.draw(&mut gizmos); } if abox.is_touching(&bbox) { - std::mem::swap(&mut a.2 .0, &mut b.2 .0); + std::mem::swap(&mut av.0, &mut bv.0); - let a2 = vec2(a.0.translation.x, a.0.translation.y); - let b2 = vec2(b.0.translation.x, b.0.translation.y); + let a2 = vec2(atf.translation.x, atf.translation.y); + let b2 = vec2(btf.translation.x, btf.translation.y); let nudge = Vec3::from(((b2 - a2).normalize() * 0.5, 0.0)); while abox.is_touching(&bbox) { - a.0.translation -= nudge; - b.0.translation += nudge; + atf.translation -= nudge; + btf.translation += nudge; + + abox = CollisionBox::from_transform(&atf, &atx.size); + bbox = CollisionBox::from_transform(&btf, &btx.size); + } + } + } +} + +pub fn fixed_collision_detection( + mut cars: Query<(&mut Transform, &Handle, &mut Velocity)>, + scenery: Query<(&mut Transform, &objectmap::Collider, Without)>, + texture_atlases: Res>, + _prefs: Res, + mut _gizmos: Gizmos, +) { + for (mut car_tf, car_tx, mut car_vel) in cars.iter_mut() { + let car_tx = match texture_atlases.get(car_tx) { + Some(car_tx) => car_tx, + _ => continue, + }; + let mut car_box = CollisionBox::from_transform(&car_tf, &car_tx.size); + + for (obj_tf, collider, _) in scenery.iter() { + let obj_box = match collider { + objectmap::Collider::Tree => CollisionBox::<8> { + points: [ + vec2(-25.0, 50.0), + vec2(25.0, 50.0), + vec2(50.0, 25.0), + vec2(50.0, -25.0), + vec2(25.0, -50.0), + vec2(-25.0, -50.0), + vec2(-50.0, -25.0), + vec2(-50.0, 25.0), + ] + .iter() + .map(|v2| { + let v3 = Vec3::from((*v2, 0.0)); + let pt = obj_tf.transform_point(v3); + vec2(pt.x, pt.y) + }) + .to_array(), + }, + objectmap::Collider::Block => CollisionBox::<8> { + points: [ + vec2(-224.0, 112.0), + vec2(0.0, 112.0), + vec2(224.0, 112.0), + vec2(224.0, 0.0), + vec2(224.0, -112.0), + vec2(0.0, -112.0), + vec2(-224.0, -112.0), + vec2(-224.0, 0.0), + ] + .iter() + .map(|v2| { + let v3 = Vec3::from((*v2, 0.0)); + let pt = obj_tf.transform_point(v3); + vec2(pt.x, pt.y) + }) + .to_array(), + }, + }; + + if car_box + .points + .iter() + .any(|pt| point_in_polygon(*pt, &obj_box.points)) + { + //car_vel.0 = vec2(-car_vel.0.x, -car_vel.0.y); + let pt = car_box + .points + .iter() + .find(|pt| point_in_polygon(**pt, &obj_box.points)) + .unwrap(); + let line = closest_edge_to_point(*pt, &obj_box.points); + car_vel.0 = reflect_against_line(car_vel.0, line); + + while car_box.is_touching(&obj_box) { + car_tf.translation += Vec3::from((car_vel.0.normalize(), 0.0)); + car_box = CollisionBox::from_transform(&car_tf, &car_tx.size); + } + } else if obj_box + .points + .iter() + .any(|pt| point_in_polygon(*pt, &car_box.points)) + { + car_vel.0 = vec2(-car_vel.0.x, -car_vel.0.y); - abox = CollisionBox::from_transform(&a.0, &atx.size); - bbox = CollisionBox::from_transform(&b.0, &btx.size); + while car_box.is_touching(&obj_box) { + car_tf.translation += Vec3::from((car_vel.0.normalize(), 0.0)); + car_box = CollisionBox::from_transform(&car_tf, &car_tx.size); + } } } }