From 3ec9b37eacd3fcf99a959122953726d293ac9785 Mon Sep 17 00:00:00 2001 From: Thilo Behnke Date: Mon, 2 May 2022 13:46:23 +0200 Subject: [PATCH] refactoring/game-obj-components --- pong/src/collision.rs | 171 ++++++++++-- pong/src/game_field.rs | 401 +++++++++++++++------------ pong/src/game_object.rs | 207 +++++++++++--- pong/src/geom.rs | 137 +++++++-- pong/src/lib.rs | 3 +- pong/src/pong.rs | 58 ++++ pong/src/utils.rs | 43 ++- pong/tests/bounding_box_tests.rs | 24 +- pong/tests/collision_handler_test.rs | 122 +++++--- pong/tests/collision_tests.rs | 110 +++++++- pong/tests/game_field_test.rs | 52 ++-- pong/tests/game_object_tests.rs | 34 ++- pong/tests/pong_tests.rs | 71 +++++ pong/tests/vector_tests.rs | 19 +- src/lib.rs | 136 ++++----- www/index.html | 14 + www/index.js | 102 ++++--- 17 files changed, 1204 insertions(+), 500 deletions(-) create mode 100644 pong/src/pong.rs create mode 100644 pong/tests/pong_tests.rs diff --git a/pong/src/collision.rs b/pong/src/collision.rs index c839198..9d40872 100644 --- a/pong/src/collision.rs +++ b/pong/src/collision.rs @@ -1,32 +1,82 @@ pub mod collision { - use std::fmt::Debug; use crate::game_object::game_object::GameObject; use crate::geom::geom::Vector; + use std::alloc::handle_alloc_error; + use std::any::Any; + use std::cell::{Ref, RefCell, RefMut}; + use std::collections::HashMap; + use std::fmt::Debug; + use std::rc::Rc; + use crate::utils::utils::{Logger, LoggerFactory}; - pub struct CollisionDetector {} + pub struct CollisionDetectorConfig { + groups: Vec + } - impl CollisionDetector { - pub fn new() -> CollisionDetector { - CollisionDetector {} + impl CollisionDetectorConfig { + pub fn new() -> CollisionDetectorConfig { + CollisionDetectorConfig { + groups: vec![] + } } - pub fn detect_collisions(&self, objs: Vec<&GameObject>) -> Box { + pub fn matches_any_group(&self, type_a: &str, type_b: &str) -> bool { + self.groups.iter().any(|g| g.matches(type_a, type_b)) + } + } + + #[derive(Debug)] + pub struct CollisionGroup(pub String, pub String); + + impl CollisionGroup { + pub fn matches(&self, type_a: &str, type_b: &str) -> bool { + self.0 == type_a && self.1 == type_b || self.0 == type_b && self.1 == type_a + } + } + + pub struct CollisionDetector { + config: CollisionDetectorConfig, + logger: Box + } + + impl CollisionDetector { + pub fn new(logger_factory: &Box) -> CollisionDetector { + let logger = logger_factory.get("collision_detector"); + CollisionDetector { + config: CollisionDetectorConfig::new(), + logger + } + } + + pub fn set_groups(&mut self, groups: Vec) { + self.config.groups = groups; + } + + pub fn detect_collisions( + &self, + objs: Vec>>>, + ) -> Box { if objs.is_empty() { return Box::new(Collisions::new(vec![])); } let mut collisions: Vec = vec![]; let mut i = 0; loop { - let obj = objs[i]; + let o = &objs[i]; + let obj = RefCell::borrow(o); i += 1; let rest = &objs[i..]; - for other in rest.iter() { + for other in rest.iter().map(|o| o.borrow()) { + if !self.config.matches_any_group(obj.obj_type(), other.obj_type()) { + // self.logger.log(&*format!("objs {} and {} do not match any group: {:?}", obj.obj_type(), other.obj_type(), self.config.groups)); + continue; + } let has_collision = obj.bounding_box().overlaps(&other.bounding_box()); if !has_collision { continue; } - collisions.push(Collision(obj.id, other.id)) + collisions.push(Collision(obj.id(), other.id())) } if i >= objs.len() { break; @@ -37,7 +87,7 @@ pub mod collision { } } - pub trait CollisionRegistry : Debug { + pub trait CollisionRegistry: Debug { fn get_collisions(&self) -> Vec<&Collision>; fn get_collisions_by_id(&self, id: u16) -> Vec<&Collision>; } @@ -65,28 +115,97 @@ pub mod collision { } } + pub struct CollisionHandlerRegistry { + handlers: HashMap<(String, String), fn(Rc>>, Rc>>)> + } + + type CollisionCallback = fn(Rc>>, Rc>>); + + impl CollisionHandlerRegistry { + pub fn new() -> CollisionHandlerRegistry { + CollisionHandlerRegistry {handlers: HashMap::new()} + } + + pub fn add(&mut self, mapping: (String, String), callback: CollisionCallback) { + if self.handlers.contains_key(&mapping) { + panic!( + "Collision handler for mapping {:?} is already registered.", + mapping + ) + } + self.handlers.insert(mapping, callback); + } + + pub fn call(&self, mapping: &(String, String), values: (Rc>>, Rc>>)) -> bool { + let regular = self.handlers.get(&mapping); + if let Some(callback) = regular { + callback(values.0, values.1); + return true; + } + let inverse = self.handlers.get(&(mapping.clone().1, mapping.clone().0)); + if let Some(callback) = inverse { + callback(values.1, values.0); + return true; + } + return false; + } + } + #[derive(Debug, Eq, PartialEq)] pub struct Collision(pub u16, pub u16); - pub struct CollisionHandler {} + pub struct CollisionHandler { + logger: Box, + handlers: CollisionHandlerRegistry + } impl CollisionHandler { - pub fn new() -> CollisionHandler { - CollisionHandler {} - } - pub fn handle(&self, obj_a: &mut GameObject, obj_b: &GameObject) { - if !obj_a.is_static { - obj_a.vel.reflect(&obj_b.orientation); - if obj_b.vel != Vector::zero() { - let mut adjusted = obj_b.vel.clone(); - adjusted.normalize(); - obj_a.vel.add(&adjusted); - } + pub fn new(logger_factory: &Box) -> CollisionHandler { + let logger = logger_factory.get("collision_handler"); + CollisionHandler { + logger, + handlers: CollisionHandlerRegistry::new(), } - let mut b_to_a = obj_a.pos.clone(); - b_to_a.sub(&obj_b.pos); - b_to_a.normalize(); - obj_a.pos.add(&b_to_a); } + + pub fn register( + &mut self, + mapping: (String, String), + callback: fn(Rc>>, Rc>>), + ) { + self.handlers.add(mapping, callback) + } + + pub fn handle( + &self, + obj_a: Rc>>, + obj_b: Rc>>, + ) -> bool { + let key = (RefCell::borrow(&obj_a).obj_type().to_string(), RefCell::borrow(&obj_b).obj_type().to_string()); + let handler_res = self.handlers.call(&key, (obj_a, obj_b)); + if !handler_res { + self.logger.log(&*format!("Found no matching collision handler: {:?}", key)); + return false; + } + return true; + } + + // pub fn new() -> CollisionHandler { + // CollisionHandler {} + // } + // pub fn handle(&self, obj_a: &mut Box, obj_b: &Box) { + // if !obj_a.is_static() { + // obj_a.vel_mut().reflect(&obj_b.orientation()); + // if *obj_b.vel() != Vector::zero() { + // let mut adjusted = obj_b.vel().clone(); + // adjusted.normalize(); + // obj_a.vel_mut().add(&adjusted); + // } + // } + // let mut b_to_a = obj_a.pos().clone(); + // b_to_a.sub(&obj_b.pos()); + // b_to_a.normalize(); + // obj_a.pos_mut().add(&b_to_a); + // } } } diff --git a/pong/src/game_field.rs b/pong/src/game_field.rs index 5bff345..6a2b5d1 100644 --- a/pong/src/game_field.rs +++ b/pong/src/game_field.rs @@ -1,8 +1,15 @@ -use std::f64::consts::{FRAC_PI_2, FRAC_PI_4}; -use crate::collision::collision::{Collision, CollisionDetector, CollisionHandler, CollisionRegistry, Collisions}; -use crate::game_object::game_object::{GameObject, Shape}; +use crate::collision::collision::{Collision, CollisionDetector, CollisionGroup, CollisionHandler, CollisionRegistry, Collisions}; +use crate::game_object::components::{DefaultGeomComp, DefaultPhysicsComp}; +use crate::game_object::game_object::{DefaultGameObject, GameObject}; use crate::geom::geom::Vector; -use crate::utils::utils::{Logger, NoopLogger}; +use crate::geom::shape::{Shape, ShapeType}; +use crate::pong::pong_collisions::{handle_ball_bounds_collision, handle_player_ball_collision, handle_player_bound_collision}; +use crate::utils::utils::{DefaultLoggerFactory, Logger, LoggerFactory, NoopLogger}; +use std::borrow::{Borrow, BorrowMut}; +use std::cell::{Cell, Ref, RefCell, RefMut}; +use std::collections::HashMap; +use std::ops::Deref; +use std::rc::Rc; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum InputType { @@ -17,239 +24,271 @@ pub struct Input { } pub struct Field { + pub logger_factory: Box, pub logger: Box, pub width: u16, pub height: u16, - pub players: Vec, - pub balls: Vec, - pub bounds: Bounds, - pub collisions: Box + pub collisions: Box, + objs: Vec>>>, + collision_detector: CollisionDetector, + collision_handler: CollisionHandler, } impl Field { - pub fn new(logger: Box) -> Field { + pub fn new(logger_factory: Box) -> Field { let width = 800; let height = 600; let mut field = Field { - logger, + logger: logger_factory.get("game_field"), width, height, - players: vec![], - balls: vec![], - bounds: Bounds::new(width, height), - collisions: Box::new(Collisions::new(vec![])) + objs: DefaultGameObject::bounds(width, height) + .into_iter() + .map(|b| Rc::new(RefCell::new(b.inner()))) + .collect(), + collisions: Box::new(Collisions::new(vec![])), + collision_detector: CollisionDetector::new(&logger_factory), + collision_handler: CollisionHandler::new(&logger_factory), + logger_factory }; field.add_player(0, 0 + width / 20, height / 2); field.add_player(1, width - width / 20, height / 2); field.add_ball(2, width / 2, height / 2); + field.collision_handler.register( + (String::from("ball"), String::from("player")), + handle_player_ball_collision, + ); + + field.collision_handler.register( + (String::from("ball"), String::from("bound")), + handle_ball_bounds_collision, + ); + + field.collision_handler.register( + (String::from("player"), String::from("bound")), + handle_player_bound_collision, + ); + + field.collision_detector.set_groups( + vec![ + CollisionGroup(String::from("player"), String::from("ball")), + CollisionGroup(String::from("player"), String::from("bound")), + CollisionGroup(String::from("ball"), String::from("bound")), + ] + ); + return field; } pub fn mock(width: u16, height: u16) -> Field { + let logger_factory = DefaultLoggerFactory::new(Box::new(NoopLogger{})); Field { - logger: Box::new(NoopLogger {}), + logger: logger_factory.get("game_field"), width, height, - players: vec![], - balls: vec![], - bounds: Bounds::new(width, height), - collisions: Box::new(Collisions::new(vec![])) + objs: DefaultGameObject::bounds(width, height) + .into_iter() + .map(|b| Rc::new(RefCell::new(b.inner()))) + .collect(), + collisions: Box::new(Collisions::new(vec![])), + collision_detector: CollisionDetector::new(&logger_factory), + collision_handler: CollisionHandler::new(&logger_factory), + logger_factory, } } pub fn add_player(&mut self, id: u16, x: u16, y: u16) { - self.players.push(Player::new(id, x, y, &self)); + let player = DefaultGameObject::player(id, x, y, &self); + self.objs.push(Rc::new(RefCell::new(player))); } pub fn add_ball(&mut self, id: u16, x: u16, y: u16) { - let ball = Ball::new(id, x, y, &self); - self.balls.push(ball); + let ball = DefaultGameObject::ball(id, x, y, &self); + self.objs.push(Rc::new(RefCell::new(ball))); } - pub fn tick(&mut self, inputs: Vec) { - for ball in self.balls.iter_mut() { - if ball.obj.vel == Vector::zero() { - ball.obj.set_vel_x(-2.) - } - } - - for player in self.players.iter_mut() { - let input_opt = inputs.iter().find(|input| player.obj.id == input.obj_id); - if let None = input_opt { - player.obj.set_vel_y(0.); + pub fn tick(&self, inputs: Vec) { + for obj in self.objs.iter() { + let mut obj_mut = RefCell::borrow_mut(obj); + if obj_mut.obj_type() != "ball" { continue; } - let input = input_opt.unwrap(); - match input.input { - InputType::UP => { - player.obj.vel.y = (player.obj.vel.y + 1.).min(5.); - } - InputType::DOWN => { - player.obj.vel.y = (player.obj.vel.y - 1.).max(-5.); - } - }; - } - - for player in self.players.iter_mut() { - player.obj.update_pos() - } - for ball in self.balls.iter_mut() { - ball.obj.update_pos() - } - - let mut objs: Vec = vec![]; - objs.extend( - self.players - .clone() - .into_iter() - .map(|p| p.obj) - .collect::>(), - ); - objs.extend( - self.balls - .clone() - .into_iter() - .map(|b| b.obj) - .collect::>(), - ); - objs.extend( - self.bounds.objs - .clone() - .into_iter() - .collect::>() - ); - let collision_detector = CollisionDetector::new(); - let collision_handler = CollisionHandler::new(); - self.collisions = collision_detector.detect_collisions(objs.iter().collect()); - - for ball in self.balls.iter_mut() { - let collisions = self.collisions.get_collisions_by_id(ball.obj.id); - if collisions.is_empty() { - continue; + if *obj_mut.vel() == Vector::zero() { + obj_mut.vel_mut().add(&Vector::new(-2.0, 0.)) } - let other = match collisions[0] { - Collision(obj_a_id, obj_b_id) if *obj_a_id == ball.obj.id => { - objs.iter().find(|o| o.id == *obj_b_id).unwrap() + } + + { + for obj in self.objs.iter() { + let mut obj_mut = RefCell::borrow_mut(obj); + if obj_mut.obj_type() != "player" { + continue; } - collision => objs.iter().find(|o| o.id == collision.0).unwrap(), - }; + let input_opt = inputs.iter().find(|i| i.obj_id == obj_mut.id()); + if let None = input_opt { + obj_mut.vel_mut().y = 0.; + continue; + } + let input = input_opt.unwrap(); + match input.input { + InputType::UP => { + let updated_vel_y = (obj_mut.vel().y + 1.).min(5.); + obj_mut.vel_mut().y = updated_vel_y; + } + InputType::DOWN => { + let updated_vel_y = (obj_mut.vel().y - 1.).max(-5.); + obj_mut.vel_mut().y = updated_vel_y; + } + }; + } + } + { + for obj in self.objs.iter() { + let mut obj_mut = RefCell::borrow_mut(obj); + obj_mut.update_pos(); + } + } - self.logger.log("### BEFORE COLLISION ###"); - self.logger.log(&*format!("{:?}", ball.obj)); - self.logger.log(&*format!("{:?}", other)); - collision_handler.handle(&mut ball.obj, other); - self.logger.log("### AFTER COLLISION ###"); - self.logger.log(&*format!("{:?}", ball.obj)); - self.logger.log(&*format!("{:?}", other)); - self.logger.log("### DONE ###"); + let collisions = self.get_collisions(); + + let collision_handler = &self.collision_handler; + let registered_collisions = collisions.get_collisions(); + self.logger.log(&*format!("Found {} collisions: {:?}", registered_collisions.len(), registered_collisions)); + for collision in registered_collisions.iter() { + let objs = &self.objs; + let obj_a = objs.iter().find(|o| RefCell::borrow(o).id() == collision.0).unwrap().clone(); + let obj_b = objs.iter().find(|o| RefCell::borrow(o).id() == collision.1).unwrap().clone(); + collision_handler.handle(obj_a, obj_b); } } - pub fn players(&self) -> Vec<&Player> { - self.players.iter().collect() + fn get_collisions(&self) -> Box { + let objs = self.objs.iter().map(|o| o.clone()).collect(); + self.collision_detector.detect_collisions(objs) } - pub fn balls(&self) -> Vec<&Ball> { - self.balls.iter().collect() + pub fn objs(&self) -> Vec<&Rc>>> { + self.objs.iter().collect() } } -#[derive(Clone, Debug, PartialEq)] -pub struct Player { - pub obj: GameObject, -} - -impl Player { - pub fn new(id: u16, x: u16, y: u16, field: &Field) -> Player { - Player { - obj: GameObject { - id, - pos: Vector {x: x as f64, y: y as f64}, - orientation: Vector::new(0., 1.), - shape: Shape::Rect, - shape_params: vec![field.width / 25, field.height / 5], - vel: Vector::zero(), - is_static: true, - }, - } +impl DefaultGameObject { + pub fn player(id: u16, x: u16, y: u16, field: &Field) -> Box { + Box::new(DefaultGameObject::new( + id, + "player".to_string(), + Box::new(DefaultGeomComp::new(Shape::rect( + Vector { + x: x as f64, + y: y as f64, + }, + Vector::new(0., 1.), + (field.width as f64) / 25., + (field.height as f64) / 5., + ))), + Box::new(DefaultPhysicsComp::new(Vector::zero(), true)), + )) } } -#[derive(Clone, Debug, PartialEq)] -pub struct Ball { - pub obj: GameObject, -} - -impl Ball { - pub fn new(id: u16, x: u16, y: u16, field: &Field) -> Ball { - Ball { - obj: GameObject { - id, - pos: Vector {x: x as f64, y: y as f64}, - orientation: Vector::zero(), - shape: Shape::Circle, - shape_params: vec![field.width / 80], - vel: Vector::zero(), - is_static: false, - }, - } +impl DefaultGameObject { + pub fn ball(id: u16, x: u16, y: u16, field: &Field) -> Box { + Box::new(DefaultGameObject::new( + id, + "ball".to_string(), + Box::new(DefaultGeomComp::new(Shape::circle( + Vector { + x: x as f64, + y: y as f64, + }, + Vector::zero(), + (field.width as f64) / 80., + ))), + Box::new(DefaultPhysicsComp::new(Vector::zero(), false)), + )) } } -#[derive(Debug)] -pub struct Bounds { - pub objs: Vec, +impl DefaultGameObject { + pub fn bounds(width: u16, height: u16) -> Vec { + let bounds = vec![ + Bounds(Bound::BOTTOM, Box::new(DefaultGameObject::new( + 90, + "bound".to_string(), + Box::new(DefaultGeomComp::new(Shape::rect( + Vector { + x: (width / 2) as f64, + y: 0 as f64, + }, + Vector::new(1., 0.), + width as f64, + 2., + ))), + Box::new(DefaultPhysicsComp::new_static()), + ))), + Bounds(Bound::TOP, Box::new(DefaultGameObject::new( + 91, + "bound".to_string(), + Box::new(DefaultGeomComp::new(Shape::rect( + Vector { + x: (width / 2) as f64, + y: height as f64, + }, + Vector::new(-1., 0.), + width as f64, + 2., + ))), + Box::new(DefaultPhysicsComp::new_static()), + ))), + Bounds(Bound::LEFT, Box::new(DefaultGameObject::new( + 92, + "bound".to_string(), + Box::new(DefaultGeomComp::new(Shape::rect( + Vector { + x: 0 as f64, + y: (height / 2) as f64, + }, + Vector::new(0., 1.), + 2., + height as f64, + ))), + Box::new(DefaultPhysicsComp::new_static()), + ))), + Bounds(Bound::RIGHT, Box::new(DefaultGameObject::new( + 93, + "bound".to_string(), + Box::new(DefaultGeomComp::new(Shape::rect( + Vector { + x: width as f64, + y: (height / 2) as f64, + }, + Vector::new(0., -1.), + 2., + height as f64, + ))), + Box::new(DefaultPhysicsComp::new_static()), + ))), + ]; + bounds + } } +#[derive(Debug, PartialEq, Eq)] +pub enum Bound { + TOP, + RIGHT, + BOTTOM, + LEFT, +} + +pub struct Bounds(pub Bound, pub Box); + impl Bounds { - pub fn new(width: u16, height: u16) -> Bounds { - Bounds { - objs: vec![ - // top - GameObject { - id: 90, - pos: Vector {x: (width / 2) as f64, y: 0 as f64}, - orientation: Vector::new(1., 0.), - shape: Shape::Rect, - shape_params: vec![width, 2], - is_static: true, - vel: Vector::zero(), - }, - // bottom - GameObject { - id: 91, - pos: Vector {x: (width / 2) as f64, y: height as f64}, - orientation: Vector::new(-1., 0.), - shape: Shape::Rect, - shape_params: vec![width, 2], - is_static: true, - vel: Vector::zero(), - }, - // left - GameObject { - id: 92, - pos: Vector {x: 0 as f64, y: (height / 2) as f64}, - orientation: Vector::new(0., 1.), - shape: Shape::Rect, - shape_params: vec![2, height], - is_static: true, - vel: Vector::zero(), - }, - // right - GameObject { - id: 93, - pos: Vector {x: width as f64, y: (height / 2) as f64}, - orientation: Vector::new(0., -1.), - shape: Shape::Rect, - shape_params: vec![2, height], - is_static: true, - vel: Vector::zero(), - }, - ], - } + pub fn inner(self) -> Box { + self.1 } } diff --git a/pong/src/game_object.rs b/pong/src/game_object.rs index abae1ec..632568a 100644 --- a/pong/src/game_object.rs +++ b/pong/src/game_object.rs @@ -1,53 +1,196 @@ pub mod game_object { + use crate::game_object::components::{GeomComp, PhysicsComp}; use crate::geom::geom::{BoundingBox, Vector}; + use crate::geom::shape::{Shape, ShapeType}; + use std::fmt::Debug; - #[derive(Clone, Debug, PartialEq)] - pub enum Shape { - Rect = 0, - Circle = 1, + pub trait GameObject: Debug { + fn id(&self) -> u16; + fn obj_type(&self) -> &str; + fn shape(&self) -> &ShapeType; + fn pos(&self) -> &Vector; + fn pos_mut(&mut self) -> &mut Vector; + fn orientation(&self) -> &Vector; + fn orientation_mut(&mut self) -> &mut Vector; + fn update_pos(&mut self); + fn bounding_box(&self) -> BoundingBox; + fn vel(&self) -> &Vector; + fn vel_mut(&mut self) -> &mut Vector; + fn is_static(&self) -> bool; } - #[derive(Clone, Debug, PartialEq)] - pub struct GameObject { + // #[derive(Clone, Debug, PartialEq)] + #[derive(Debug)] + pub struct DefaultGameObject { pub id: u16, - pub pos: Vector, - pub orientation: Vector, - pub shape: Shape, - pub shape_params: Vec, - pub vel: Vector, - pub is_static: bool, + pub obj_type: String, + geom: Box, + physics: Box, } - impl GameObject { + impl DefaultGameObject { + pub fn new( + id: u16, + obj_type: String, + geom: Box, + physics: Box, + ) -> DefaultGameObject { + DefaultGameObject { + id, + obj_type, + geom, + physics, + } + } + } - pub fn update_pos(&mut self) { - self.pos.add(&self.vel); + impl GameObject for DefaultGameObject { + fn id(&self) -> u16 { + self.id + } + + fn obj_type(&self) -> &str { + &self.obj_type + } + + fn shape(&self) -> &ShapeType { + self.geom.shape() + } + + fn pos(&self) -> &Vector { + self.geom.center() + } + + fn pos_mut(&mut self) -> &mut Vector { + self.geom.center_mut() + } + + fn orientation(&self) -> &Vector { + self.geom.orientation() + } + + fn orientation_mut(&mut self) -> &mut Vector { + self.geom.orientation_mut() + } + + fn update_pos(&mut self) { + let vel = self.vel().clone(); + let center = self.geom.center_mut(); + center.add(&vel); // Keep last orientation if vel is now zero. - if self.vel == Vector::zero() { + if vel == Vector::zero() { return; } - let mut orientation = self.vel.clone(); - orientation.normalize(); - self.orientation = orientation; + let mut updated_orientation = vel.clone(); + updated_orientation.normalize(); + let orientation = self.geom.orientation_mut(); + orientation.x = updated_orientation.x; + orientation.y = updated_orientation.y; } - pub fn set_vel_x(&mut self, x: f64) { - self.vel.x = x + fn bounding_box(&self) -> BoundingBox { + self.geom.bounding_box() } - pub fn set_vel_y(&mut self, y: f64) { - self.vel.y = y + fn vel(&self) -> &Vector { + &self.physics.vel() } - pub fn bounding_box(&self) -> BoundingBox { - match self.shape { - Shape::Rect => { - BoundingBox::create(&self.pos, self.shape_params[0], self.shape_params[1]) - } - Shape::Circle => { - BoundingBox::create(&self.pos, self.shape_params[0] * 2, self.shape_params[0] * 2) - } - } + fn vel_mut(&mut self) -> &mut Vector { + self.physics.vel_mut() + } + + fn is_static(&self) -> bool { + self.physics.is_static() + } + } +} + +pub mod components { + use crate::geom::geom::{BoundingBox, Vector}; + use crate::geom::shape::{ + get_bounding_box, get_center, get_center_mut, get_orientation, get_orientation_mut, Shape, + ShapeType, + }; + use std::fmt::Debug; + + pub trait GeomComp: Debug { + fn shape(&self) -> &ShapeType; + fn orientation(&self) -> &Vector; + fn orientation_mut(&mut self) -> &mut Vector; + fn center(&self) -> &Vector; + fn center_mut(&mut self) -> &mut Vector; + fn bounding_box(&self) -> BoundingBox; + } + + #[derive(Debug)] + pub struct DefaultGeomComp { + shape: ShapeType, + } + + impl DefaultGeomComp { + pub fn new(shape: ShapeType) -> DefaultGeomComp { + DefaultGeomComp { shape } + } + } + + impl GeomComp for DefaultGeomComp { + fn shape(&self) -> &ShapeType { + &self.shape + } + + fn orientation(&self) -> &Vector { + get_orientation(&self.shape) + } + + fn orientation_mut(&mut self) -> &mut Vector { + get_orientation_mut(&mut self.shape) + } + + fn center(&self) -> &Vector { + get_center(&self.shape) + } + + fn center_mut(&mut self) -> &mut Vector { + get_center_mut(&mut self.shape) + } + + fn bounding_box(&self) -> BoundingBox { + get_bounding_box(&self.shape) + } + } + + pub trait PhysicsComp: Debug { + fn vel(&self) -> &Vector; + fn vel_mut(&mut self) -> &mut Vector; + fn is_static(&self) -> bool; + } + + #[derive(Debug)] + pub struct DefaultPhysicsComp { + vel: Vector, + is_static: bool, + } + impl DefaultPhysicsComp { + pub fn new(vel: Vector, is_static: bool) -> DefaultPhysicsComp { + DefaultPhysicsComp { vel, is_static } + } + + pub fn new_static() -> DefaultPhysicsComp { + DefaultPhysicsComp::new(Vector::zero(), true) + } + } + impl PhysicsComp for DefaultPhysicsComp { + fn vel(&self) -> &Vector { + &self.vel + } + + fn vel_mut(&mut self) -> &mut Vector { + &mut self.vel + } + + fn is_static(&self) -> bool { + self.is_static } } } diff --git a/pong/src/geom.rs b/pong/src/geom.rs index 374f9c4..6393101 100644 --- a/pong/src/geom.rs +++ b/pong/src/geom.rs @@ -17,9 +17,7 @@ pub mod geom { } pub fn new(x: f64, y: f64) -> Vector { - Vector { - x, y - } + Vector { x, y } } pub fn normalize(&mut self) { @@ -68,7 +66,7 @@ pub mod geom { } pub fn dot(&self, other: &Vector) -> f64 { - return self.x * other.x + self.y * other.y + return self.x * other.x + self.y * other.y; } pub fn angle(&self, other: &Vector) -> f64 { @@ -110,10 +108,12 @@ pub mod geom { let mut orthogonal1 = onto.clone(); orthogonal1.orthogonal_clockwise(); if self.dot(&orthogonal1) < 0. { + // orthogonal1.normalize(); return orthogonal1; } let mut orthogonal2 = onto.clone(); orthogonal2.orthogonal_counter_clockwise(); + // orthogonal2.normalize(); return orthogonal2; } @@ -130,11 +130,12 @@ pub mod geom { impl PartialEq for Vector { fn eq(&self, other: &Self) -> bool { - (self.x * 1000.).round() == (other.x * 1000.).round() && - (self.y * 1000.).round() == (other.y * 1000.).round() + (self.x * 1000.).round() == (other.x * 1000.).round() + && (self.y * 1000.).round() == (other.y * 1000.).round() } } + #[derive(Clone, Debug)] pub struct BoundingBox { top_left: Vector, top_right: Vector, @@ -143,29 +144,24 @@ pub mod geom { } impl BoundingBox { - pub fn create_from_coords(x: f64, y: f64, width: u16, height: u16) -> BoundingBox { - let center = Vector::new(x, y); - return BoundingBox::create(¢er, width, height) - } - - pub fn create(center: &Vector, width: u16, height: u16) -> BoundingBox { + pub fn create(center: &Vector, width: f64, height: f64) -> BoundingBox { let center_x = center.x; let center_y = center.y; let top_left = Vector { - x: center_x - (width as f64 / 2.), - y: center_y + (height as f64 / 2.), + x: center_x - width / 2., + y: center_y + height / 2., }; let top_right = Vector { - x: center_x + (width as f64 / 2.), - y: center_y + (height as f64 / 2.), + x: center_x + width / 2., + y: center_y + height / 2., }; let bottom_left = Vector { - x: center_x - (width as f64 / 2.), - y: center_y - (height as f64 / 2.), + x: center_x - width / 2., + y: center_y - height / 2., }; let bottom_right = Vector { - x: center_x + (width as f64 / 2.), - y: center_y - (height as f64 / 2.), + x: center_x + width / 2., + y: center_y - height / 2., }; BoundingBox { top_left, @@ -212,13 +208,9 @@ pub mod geom { impl Range { pub fn new(a: f64, b: f64) -> Range { if a <= b { - return Range { - min: a, max: b - } - } - return Range { - min: b, max: a + return Range { min: a, max: b }; } + return Range { min: b, max: a }; } pub fn overlaps(&self, other: &Range) -> bool { @@ -242,3 +234,96 @@ pub mod geom { } } } + +pub mod shape { + use crate::geom::geom::{BoundingBox, Vector}; + use std::fmt::Debug; + + #[derive(Clone, Debug, PartialEq)] + pub enum ShapeType { + Rect(Shape, f64, f64), + Circle(Shape, f64), + } + + #[derive(Clone, Debug, PartialEq)] + pub struct Shape { + center: Vector, + orientation: Vector, + } + + impl Shape { + pub fn rect(center: Vector, orientation: Vector, width: f64, height: f64) -> ShapeType { + ShapeType::Rect( + Shape { + center, + orientation, + }, + width, + height, + ) + } + + pub fn circle(center: Vector, orientation: Vector, radius: f64) -> ShapeType { + ShapeType::Circle( + Shape { + center, + orientation, + }, + radius, + ) + } + + fn center(&self) -> &Vector { + &self.center + } + + fn center_mut(&mut self) -> &mut Vector { + &mut self.center + } + + fn orientation(&self) -> &Vector { + &self.orientation + } + + fn orientation_mut(&mut self) -> &mut Vector { + &mut self.orientation + } + } + + pub fn get_center(shape: &ShapeType) -> &Vector { + match shape { + ShapeType::Rect(ref s, _, _) => &s.center, + ShapeType::Circle(ref s, _) => &s.center, + } + } + + pub fn get_center_mut(shape: &mut ShapeType) -> &mut Vector { + match shape { + ShapeType::Rect(ref mut s, _, _) => &mut s.center, + ShapeType::Circle(ref mut s, _) => &mut s.center, + } + } + + pub fn get_orientation(shape: &ShapeType) -> &Vector { + match shape { + ShapeType::Rect(s, _, _) => &s.orientation, + ShapeType::Circle(s, _) => &s.orientation, + } + } + + pub fn get_orientation_mut(shape: &mut ShapeType) -> &mut Vector { + match shape { + ShapeType::Rect(ref mut s, _, _) => &mut s.orientation, + ShapeType::Circle(ref mut s, _) => &mut s.orientation, + } + } + + pub fn get_bounding_box(shape: &ShapeType) -> BoundingBox { + match shape { + ShapeType::Rect(s, width, height) => BoundingBox::create(&s.center, *width, *height), + ShapeType::Circle(s, radius) => { + BoundingBox::create(&s.center, *radius * 2., *radius * 2.) + } + } + } +} diff --git a/pong/src/lib.rs b/pong/src/lib.rs index 7879674..c1c5724 100644 --- a/pong/src/lib.rs +++ b/pong/src/lib.rs @@ -1,5 +1,6 @@ pub mod collision; +pub mod game_field; pub mod game_object; pub mod geom; -pub mod game_field; +pub mod pong; pub mod utils; diff --git a/pong/src/pong.rs b/pong/src/pong.rs new file mode 100644 index 0000000..4ca8472 --- /dev/null +++ b/pong/src/pong.rs @@ -0,0 +1,58 @@ +pub mod pong_collisions { + use std::cell::{RefCell, RefMut}; + use std::ops::Add; + use std::rc::Rc; + use crate::game_object::game_object::GameObject; + use crate::geom::geom::Vector; + use crate::geom::shape::ShapeType; + + pub fn handle_player_ball_collision( + ball: Rc>>, + player: Rc>>, + ) { + // reflect + let mut ball = RefCell::borrow_mut(&ball); + let player = player.borrow(); + ball.vel_mut().reflect(&player.orientation()); + // use vel of player obj + if *player.vel() != Vector::zero() { + let mut adjusted = player.vel().clone(); + adjusted.normalize(); + ball.vel_mut().add(&adjusted); + } + // move out of collision + let mut b_to_a = ball.pos().clone(); + b_to_a.sub(&player.pos()); + b_to_a.normalize(); + ball.pos_mut().add(&b_to_a); + } + + pub fn handle_ball_bounds_collision( + ball: Rc>>, + bound: Rc>>, + ) { + let mut ball = RefCell::borrow_mut(&ball); + let bound = RefCell::borrow(&bound); + ball.vel_mut().reflect(&bound.orientation()); + } + + pub fn handle_player_bound_collision( + player: Rc>>, + bound: Rc>>, + ) { + let mut player = RefCell::borrow_mut(&player); + let bound = RefCell::borrow(&bound); + let shape = player.shape().clone(); + let player_orientation = player.orientation().clone(); + let height = match shape { + ShapeType::Rect(_, _, height) => height.clone() / 2., + ShapeType::Circle(_, radius) => radius, + }; + let mut perpendicular = player_orientation.get_opposing_orthogonal(bound.orientation()); + perpendicular.y *= (height + 1.); + let mut new_pos = bound.pos().clone(); + new_pos.add(&perpendicular); + let player_pos = player.pos_mut(); + player_pos.y = new_pos.y; + } +} diff --git a/pong/src/utils.rs b/pong/src/utils.rs index f281ec5..c1f3b9a 100644 --- a/pong/src/utils.rs +++ b/pong/src/utils.rs @@ -1,12 +1,51 @@ - pub mod utils { + pub trait LoggerFactory { + fn get(&self, name: &str) -> Box; + } + + pub struct DefaultLoggerFactory { + proto: Box + } + + impl LoggerFactory for DefaultLoggerFactory { + fn get(&self, name: &str) -> Box { + let mut clone = self.proto.box_clone(); + clone.set_name(name); + clone + } + } + + impl DefaultLoggerFactory { + pub fn new(proto: Box) -> Box { + Box::new(DefaultLoggerFactory {proto}) + } + pub fn noop() -> Box { + Box::new(DefaultLoggerFactory {proto: Box::new(NoopLogger {})}) + } + } + pub trait Logger { + fn box_clone(&self) -> Box; + fn set_name(&mut self, name: &str); fn log(&self, msg: &str); } + #[derive(Clone)] pub struct NoopLogger {} + impl Logger for NoopLogger { - fn log(&self, msg: &str) { + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn set_name(&mut self, name: &str) {} + + fn log(&self, msg: &str) {} + } + + impl NoopLogger { + fn new() -> Box { + Box::new(NoopLogger {}) } } } diff --git a/pong/tests/bounding_box_tests.rs b/pong/tests/bounding_box_tests.rs index dbd5361..2a9d21f 100644 --- a/pong/tests/bounding_box_tests.rs +++ b/pong/tests/bounding_box_tests.rs @@ -1,10 +1,10 @@ -use rstest::rstest; use pong::geom::geom::{BoundingBox, Vector}; +use rstest::rstest; #[rstest] -#[case(BoundingBox::create_from_coords(10., 10., 5, 5), Vector::new(10., 10.), true)] -#[case(BoundingBox::create_from_coords(10., 10., 5, 5), Vector::new(8., 8.), true)] -#[case(BoundingBox::create_from_coords(10., 10., 5, 5), Vector::new(20., 20.), false)] +#[case(BoundingBox::create(&Vector::new(10., 10.), 5., 5.), Vector::new(10., 10.), true)] +#[case(BoundingBox::create(&Vector::new(10., 10.), 5., 5.), Vector::new(8., 8.), true)] +#[case(BoundingBox::create(&Vector::new(10., 10.), 5., 5.), Vector::new(20., 20.), false)] pub fn should_correctly_determine_if_point_is_within_box( #[case] bounding_box: BoundingBox, #[case] point: Vector, @@ -16,23 +16,23 @@ pub fn should_correctly_determine_if_point_is_within_box( #[rstest] #[case( - BoundingBox::create_from_coords(10., 10., 5, 5), - BoundingBox::create_from_coords(10., 10., 5, 5), + BoundingBox::create(&Vector::new(10., 10.), 5., 5.), + BoundingBox::create(&Vector::new(10., 10.), 5., 5.), true )] #[case( - BoundingBox::create_from_coords(10., 10., 5, 5), - BoundingBox::create_from_coords(8., 8., 5, 5), + BoundingBox::create(&Vector::new(10., 10.), 5., 5.), + BoundingBox::create(&Vector::new(8., 8.), 5., 5.), true )] #[case( - BoundingBox::create_from_coords(10., 10., 5, 5), - BoundingBox::create_from_coords(4.9, 4.9, 5, 5), + BoundingBox::create(&Vector::new(10., 10.), 5., 5.), + BoundingBox::create(&Vector::new(4.9, 4.9), 5., 5.), false )] #[case( - BoundingBox::create_from_coords(10., 10., 5, 5), - BoundingBox::create_from_coords(5., 5., 5, 5), + BoundingBox::create(&Vector::new(10., 10.), 5., 5.), + BoundingBox::create(&Vector::new(5., 5.), 5., 5.), true )] pub fn should_correctly_determine_if_overlap( diff --git a/pong/tests/collision_handler_test.rs b/pong/tests/collision_handler_test.rs index cc60e74..22c5d71 100644 --- a/pong/tests/collision_handler_test.rs +++ b/pong/tests/collision_handler_test.rs @@ -1,73 +1,103 @@ -use rstest::rstest; use pong::collision::collision::CollisionHandler; -use pong::game_object::game_object::{GameObject, Shape}; +use pong::game_object::components::{DefaultGeomComp, DefaultPhysicsComp}; +use pong::game_object::game_object::{DefaultGameObject, GameObject}; use pong::geom::geom::Vector; +use pong::geom::shape::Shape; +use rstest::rstest; +use std::borrow::{Borrow, BorrowMut}; +use std::cell::RefCell; +use std::rc::Rc; +use pong::utils::utils::DefaultLoggerFactory; #[rstest] #[case( // given - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(1., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(1., 0.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, + create_game_obj(1, Vector::new(1., 0.), Vector::new(1., 0.), true), + create_game_obj(2, Vector::new(0., 0.), Vector::new(0., 1.), true), // expected - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(1., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(1., 0.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, -)] -#[case( -// given - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(1., 0.), shape: Shape::Rect, shape_params: vec![], is_static: false, orientation: Vector::new(1., 0.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, - // expected - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(-1., 0.), shape: Shape::Rect, shape_params: vec![], is_static: false, orientation: Vector::new(1., 0.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, + create_game_obj(1, Vector::new(1., 0.), Vector::new(1., 0.), true), + create_game_obj(2, Vector::new(0., 0.), Vector::new(0., 1.), true), )] #[case( // given - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(-1., 0.), shape: Shape::Rect, shape_params: vec![], is_static: false, orientation: Vector::new(-1., 0.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, + create_game_obj(1, Vector::new(1., 0.), Vector::new(1., 0.), false), + create_game_obj(2, Vector::new(0., 0.), Vector::new(0., 1.), true), // expected - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(1., 0.), shape: Shape::Rect, shape_params: vec![], is_static: false, orientation: Vector::new(-1., 0.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, -)] -#[case( -// given - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(1., 1.), shape: Shape::Rect, shape_params: vec![], is_static: false, orientation: Vector::new(1., 1.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, - // expected - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(-1., 1.), shape: Shape::Rect, shape_params: vec![], is_static: false, orientation: Vector::new(1., 1.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, + create_game_obj(1, Vector::new(-1., 0.), Vector::new(1., 0.), false), + create_game_obj(2, Vector::new(0., 0.), Vector::new(0., 1.), true), )] #[case( // given - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(-2., 0.), shape: Shape::Rect, shape_params: vec![], is_static: false, orientation: Vector::new(-1., 0.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, + create_game_obj(1, Vector::new(-1., 0.), Vector::new(-1., 0.), false), + create_game_obj(2, Vector::new(0., 0.), Vector::new(0., 1.), true), // expected - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(2., 0.), shape: Shape::Rect, shape_params: vec![], is_static: false, orientation: Vector::new(-1., 0.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, + create_game_obj(1, Vector::new(1., 0.), Vector::new(-1., 0.), false), + create_game_obj(2, Vector::new(0., 0.), Vector::new(0., 1.), true), )] #[case( // given - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(1., 0.), shape: Shape::Rect, shape_params: vec![], is_static: false, orientation: Vector::new(1., 0.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 1.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, + create_game_obj(1, Vector::new(1., 1.), Vector::new(1., 1.), false), + create_game_obj(2, Vector::new(0., 0.), Vector::new(0., 1.), true), // expected - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(-1., 1.), shape: Shape::Rect, shape_params: vec![], is_static: false, orientation: Vector::new(1., 0.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 1.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, + create_game_obj(1, Vector::new(-1., 1.), Vector::new(1., 1.), false), + create_game_obj(2, Vector::new(0., 0.), Vector::new(0., 1.), true), )] #[case( // given - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(-2., 1.), shape: Shape::Rect, shape_params: vec![], is_static: false, orientation: Vector::new(-1., 0.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, + create_game_obj(1, Vector::new(-2., 1.), Vector::new(-1., 0.), false), + create_game_obj(2, Vector::new(0., 0.), Vector::new(0., 1.), true), // expected - GameObject {id: 1, pos: Vector::zero(), vel: Vector::new(2., 1.), shape: Shape::Rect, shape_params: vec![], is_static: false, orientation: Vector::new(-1., 0.)}, - GameObject {id: 2, pos: Vector::zero(), vel: Vector::new(0., 0.), shape: Shape::Rect, shape_params: vec![], is_static: true, orientation: Vector::new(0., 1.)}, + create_game_obj(1, Vector::new(2., 1.), Vector::new(-1., 0.), false), + create_game_obj(2, Vector::new(0., 0.), Vector::new(0., 1.), true), +)] +#[case( + // given + create_game_obj(1, Vector::new(1., 0.), Vector::new(1., 0.), false), + create_game_obj(2, Vector::new(0., 1.), Vector::new(0., 1.), true), + // expected + create_game_obj(1, Vector::new(-1., 1.), Vector::new(1., 0.), false), + create_game_obj(2, Vector::new(0., 1.), Vector::new(0., 1.), true), +)] +#[case( + // given + create_game_obj(1, Vector::new(-2., 1.), Vector::new(-1., 0.), false), + create_game_obj(2, Vector::new(0., 0.), Vector::new(0., 1.), true), + // expected + create_game_obj(1, Vector::new(2., 1.), Vector::new(-1., 0.), false), + create_game_obj(2, Vector::new(0., 0.), Vector::new(0., 1.), true), )] pub fn should_handle_collision( - #[case] mut obj_a: GameObject, - #[case] obj_b: GameObject, - #[case] expected_a: GameObject, - #[case] expected_b: GameObject, + #[case] mut obj_a: Rc>>, + #[case] mut obj_b: Rc>>, + #[case] expected_a: Rc>>, + #[case] expected_b: Rc>>, ) { - let handler = CollisionHandler {}; - handler.handle(&mut obj_a, &obj_b); - assert_eq!(obj_a, expected_a); - assert_eq!(obj_b, expected_b); + let logger = DefaultLoggerFactory::noop(); + let mut handler = CollisionHandler::new(&logger); + handler.register((String::from("obj"), String::from("obj")), |a, b| {}); + let res = handler.handle(obj_a, obj_b); + assert_eq!(true, res) + // assert_eq!(obj_a.pos(), expected_a.pos()); + // assert_eq!(obj_a.vel(), expected_a.vel()); + // assert_eq!(obj_b.pos(), expected_b.pos()); + // assert_eq!(obj_b.vel(), expected_b.vel()); +} + +fn create_game_obj( + id: u16, + vel: Vector, + orientation: Vector, + is_static: bool, +) -> Rc>> { + Rc::new(RefCell::new(Box::new(DefaultGameObject::new( + id, + "obj".to_string(), + Box::new(DefaultGeomComp::new(Shape::rect( + Vector::zero(), + orientation, + 20., + 20., + ))), + Box::new(DefaultPhysicsComp::new(vel, is_static)), + )))) } diff --git a/pong/tests/collision_tests.rs b/pong/tests/collision_tests.rs index 8cd9c3c..8098d95 100644 --- a/pong/tests/collision_tests.rs +++ b/pong/tests/collision_tests.rs @@ -1,46 +1,128 @@ +use pong::collision::collision::{Collision, CollisionDetector, CollisionGroup}; +use pong::game_object::game_object::GameObject; +use pong::geom::geom::{BoundingBox, Vector}; +use pong::geom::shape::ShapeType; use rstest::rstest; -use pong::collision::collision::{Collision, CollisionDetector}; -use pong::game_object::game_object::{GameObject, Shape}; -use pong::geom::geom::{Vector}; +use std::cell::{Ref, RefCell}; +use std::rc::Rc; +use pong::utils::utils::DefaultLoggerFactory; #[rstest] #[case(vec![], vec![])] #[case( vec![ - GameObject{id: 1, pos: Vector{x: 50., y: 50.}, shape: Shape::Rect, shape_params: vec![20, 20], vel: Vector::zero(), is_static: false, orientation: Vector::new(1., 1.)}, - GameObject{id: 2, pos: Vector{x: 50., y: 50.}, shape: Shape::Rect, shape_params: vec![20, 20], vel: Vector::zero(), is_static: false, orientation: Vector::new(1., 1.)} + MockGameObject::new(1, "a", BoundingBox::create(&Vector{x: 50., y: 50.}, 20., 20.)), + MockGameObject::new(2, "b", BoundingBox::create(&Vector{x: 50., y: 50.}, 20., 20.)) ], vec![Collision(1, 2)] )] #[case( vec![ - GameObject{id: 1, pos: Vector{x: 60., y: 65.}, shape: Shape::Rect, shape_params: vec![20, 20], vel: Vector::zero(), is_static: false, orientation: Vector::new(1., 1.)}, - GameObject{id: 2, pos: Vector{x: 50., y: 50.}, shape: Shape::Rect, shape_params: vec![20, 20], vel: Vector::zero(), is_static: false, orientation: Vector::new(1., 1.)} + MockGameObject::new(1, "a", BoundingBox::create(&Vector{x: 60., y: 65.}, 20., 20.)), + MockGameObject::new(2, "b", BoundingBox::create(&Vector{x: 50., y: 50.}, 20., 20.)), ], vec![Collision(1, 2)] )] #[case( vec![ - GameObject{id: 1, pos: Vector{x: 50., y: 50.}, shape: Shape::Rect, shape_params: vec![20, 20], vel: Vector::zero(), is_static: false, orientation: Vector::new(1., 1.)}, - GameObject{id: 2, pos: Vector{x: 80., y: 80.}, shape: Shape::Rect, shape_params: vec![20, 20], vel: Vector::zero(), is_static: false, orientation: Vector::new(1., 1.)} + MockGameObject::new(1, "a", BoundingBox::create(&Vector{x: 50., y: 50.}, 20., 20.)), + MockGameObject::new(2, "b", BoundingBox::create(&Vector{x: 80., y: 80.}, 20., 20.)), ], vec![] )] #[case( vec![ - GameObject{id: 1, pos: Vector{x: 50., y: 50.}, shape: Shape::Rect, shape_params: vec![50, 50], vel: Vector::zero(), is_static: false, orientation: Vector::new(1., 1.)}, - GameObject{id: 2, pos: Vector{x: 500., y: 50.}, shape: Shape::Rect, shape_params: vec![50, 50], vel: Vector::zero(), is_static: false, orientation: Vector::new(1., 1.)} + MockGameObject::new(1, "a", BoundingBox::create(&Vector{x: 50., y: 50.}, 50., 50.)), + MockGameObject::new(2, "b", BoundingBox::create(&Vector{x: 500., y: 50.}, 50., 50.)), + ], + vec![] +)] +#[case( + vec![ + MockGameObject::new(1, "a", BoundingBox::create(&Vector{x: 60., y: 65.}, 20., 20.)), + MockGameObject::new(2, "c", BoundingBox::create(&Vector{x: 50., y: 50.}, 20., 20.)), ], vec![] )] pub fn should_detect_collisions( - #[case] objs: Vec, + #[case] objs: Vec>>>, #[case] expected_collisions: Vec, ) { - let detector = CollisionDetector::new(); - let res = detector.detect_collisions(objs.iter().collect()); + let logger = DefaultLoggerFactory::noop(); + let mut detector = CollisionDetector::new(&logger); + detector.set_groups(vec![CollisionGroup(String::from("a"), String::from("b"))]); + let res = detector.detect_collisions(objs); assert_eq!( res.get_collisions(), expected_collisions.iter().collect::>() ); } + +#[derive(Debug)] +pub struct MockGameObject { + id: u16, + obj_type: String, + bounding_box: BoundingBox, + zero_vec: Vector, +} + +impl MockGameObject { + pub fn new(id: u16, obj_type: &str, bounding_box: BoundingBox) -> Rc>> { + Rc::new(RefCell::new(Box::new(MockGameObject { + id, + obj_type: String::from(obj_type), + bounding_box, + zero_vec: Vector::zero(), + }))) + } +} + +impl GameObject for MockGameObject { + fn id(&self) -> u16 { + self.id + } + + fn obj_type(&self) -> &str { + &*self.obj_type + } + + fn shape(&self) -> &ShapeType { + todo!() + } + + fn pos(&self) -> &Vector { + todo!() + } + + fn pos_mut(&mut self) -> &mut Vector { + todo!() + } + + fn orientation(&self) -> &Vector { + todo!() + } + + fn orientation_mut(&mut self) -> &mut Vector { + todo!() + } + + fn update_pos(&mut self) { + todo!() + } + + fn bounding_box(&self) -> BoundingBox { + self.bounding_box.clone() + } + + fn vel(&self) -> &Vector { + todo!() + } + + fn vel_mut(&mut self) -> &mut Vector { + todo!() + } + + fn is_static(&self) -> bool { + todo!() + } +} diff --git a/pong/tests/game_field_test.rs b/pong/tests/game_field_test.rs index 54ce890..81bd4a7 100644 --- a/pong/tests/game_field_test.rs +++ b/pong/tests/game_field_test.rs @@ -1,6 +1,8 @@ #[cfg(test)] mod game_field_tests { use pong::game_field::{Field, Input, InputType}; + use std::borrow::Borrow; + use std::cell::RefCell; #[test] fn player_input_update_pos__up() { @@ -12,9 +14,14 @@ mod game_field_tests { obj_id: 1, }]; field.tick(inputs); - let players = field.players(); - let player = players.first().unwrap(); - assert_eq!(player.obj.pos.y, height as f64 / 2. + 1.); + let player = RefCell::borrow( + field + .objs() + .iter() + .find(|o| RefCell::borrow(o).obj_type() == "player") + .unwrap(), + ); + assert_eq!(player.pos().y, height as f64 / 2. + 1.); } #[test] @@ -27,38 +34,11 @@ mod game_field_tests { obj_id: 1, }]; field.tick(inputs); - let players = field.players(); - let player = players.first().unwrap(); - assert_eq!(player.obj.pos.y, height as f64 / 2. - 1.); - } - - #[test] - fn player_input_update_out_of_bounds__up() { - let height = 1000; - let mut field = Field::mock(1000, height); - field.add_player(1, 50, height - height / 5 / 2); - let inputs = vec![Input { - input: InputType::UP, - obj_id: 1, - }]; - field.tick(inputs); - let players = field.players(); - let player = players.first().unwrap(); - assert_eq!(player.obj.pos.y, height as f64 - height as f64 / 5. / 2.); - } - - #[test] - fn player_input_update_out_of_bounds__down() { - let height = 1000; - let mut field = Field::mock(1000, height); - field.add_player(1, 50, height / 5 / 2); - let inputs = vec![Input { - input: InputType::DOWN, - obj_id: 1, - }]; - field.tick(inputs); - let players = field.players(); - let player = players.first().unwrap(); - assert_eq!(player.obj.pos.y, height as f64 / 5. / 2.); + let objs = field.objs(); + let player = objs + .iter() + .find(|o| RefCell::borrow(o).obj_type() == "player") + .unwrap(); + assert_eq!(RefCell::borrow(player).pos().y, height as f64 / 2. - 1.); } } diff --git a/pong/tests/game_object_tests.rs b/pong/tests/game_object_tests.rs index 077c4f3..784baef 100644 --- a/pong/tests/game_object_tests.rs +++ b/pong/tests/game_object_tests.rs @@ -1,19 +1,27 @@ +use pong::game_object::components::{DefaultGeomComp, DefaultPhysicsComp}; +use pong::game_object::game_object::{DefaultGameObject, GameObject}; +use pong::geom::geom::Vector; +use pong::geom::shape::Shape; use rstest::rstest; -use pong::game_object::game_object::{GameObject, Shape}; -use pong::geom::geom::{Vector}; #[rstest] #[case(Vector::new(100., 100.), Vector::new(-1., 1.), Vector::new(99., 101.))] -pub fn should_update_pos(#[case] start_pos: Vector, #[case] vel: Vector, #[case] expected_pos: Vector) { - let mut obj = GameObject { - id: 1, - pos: Vector::new(start_pos.x as f64, start_pos.y as f64), - vel, - shape: Shape::Rect, - shape_params: vec![], - is_static: false, - orientation: Vector::new(1., 0.) - }; +pub fn should_update_pos( + #[case] start_pos: Vector, + #[case] vel: Vector, + #[case] expected_pos: Vector, +) { + let mut obj = DefaultGameObject::new( + 1, + "obj".to_string(), + Box::new(DefaultGeomComp::new(Shape::rect( + Vector::new(start_pos.x as f64, start_pos.y as f64), + Vector::new(1., 0.), + 0., + 0., + ))), + Box::new(DefaultPhysicsComp::new(vel, false)), + ); obj.update_pos(); - assert_eq!(obj.pos, expected_pos); + assert_eq!(*obj.pos(), expected_pos); } diff --git a/pong/tests/pong_tests.rs b/pong/tests/pong_tests.rs new file mode 100644 index 0000000..b6bb2e4 --- /dev/null +++ b/pong/tests/pong_tests.rs @@ -0,0 +1,71 @@ +use std::cell::RefCell; +use std::rc::Rc; +use rstest::rstest; +use pong::game_field::{Bound, Field}; +use pong::game_object::game_object::{DefaultGameObject, GameObject}; +use pong::geom::geom::Vector; +use pong::pong::pong_collisions::handle_player_bound_collision; +use pong::utils::utils::{DefaultLoggerFactory, NoopLogger}; + +#[rstest] +#[case( + // given + create_player(1, 10, 0, Vector::new(0., -1.)), + get_bound(Bound::BOTTOM), + // expected + create_player(1, 10, 61, Vector::new(0., -1.)), + get_bound(Bound::BOTTOM) +)] +#[case( + // given + create_player(1, 10, 1, Vector::new(0., -1.)), + get_bound(Bound::BOTTOM), + // expected + create_player(1, 10, 61, Vector::new(0., -1.)), + get_bound(Bound::BOTTOM) +)] +#[case( + // given + create_player(1, 10, 601, Vector::new(0., 1.)), + get_bound(Bound::TOP), + // expected + create_player(1, 10, 539, Vector::new(0., 1.)), + get_bound(Bound::TOP) +)] +#[case( + // given + create_player(1, 10, 599, Vector::new(0., 1.)), + get_bound(Bound::TOP), + // expected + create_player(1, 10, 539, Vector::new(0., 1.)), + get_bound(Bound::TOP) +)] +pub fn should_correctly_handle_player_bounds_collision( + #[case] mut player: Rc>>, + #[case] mut bounds: Rc>>, + #[case] mut player_expected: Rc>>, + #[case] mut bounds_expected: Rc>> +) { + handle_player_bound_collision(player.clone(), bounds.clone()); + assert_eq!(player_expected.borrow().pos(), player.borrow().pos()); + assert_eq!(bounds_expected.borrow().pos(), bounds.borrow().pos()); +} + +fn create_player(id: u16, x: u16, y: u16, orientation: Vector) -> Rc>> { + let logger = DefaultLoggerFactory::noop(); + let field = Field::new(logger); + let mut player = DefaultGameObject::player(id, x, y, &field); + let player_orientation = player.orientation_mut(); + player_orientation.x = orientation.x; + player_orientation.y = orientation.y; + Rc::new(RefCell::new(player)) +} + +fn get_bound(bound: Bound) -> Rc>> { + let logger = DefaultLoggerFactory::noop(); + let field = Field::new(logger); + let bounds = DefaultGameObject::bounds(field.width, field.height); + return Rc::new(RefCell::new(bounds.into_iter().find(|b| { + b.0 == bound + }).unwrap().inner())); +} diff --git a/pong/tests/vector_tests.rs b/pong/tests/vector_tests.rs index 691eb72..e8fc097 100644 --- a/pong/tests/vector_tests.rs +++ b/pong/tests/vector_tests.rs @@ -1,8 +1,8 @@ -use rstest::rstest; use pong::geom::geom::Vector; -use std::f64::consts::PI; +use rstest::rstest; use std::f64::consts::FRAC_PI_2; use std::f64::consts::FRAC_PI_4; +use std::f64::consts::PI; #[rstest] #[case(1., 0., 1.)] @@ -39,7 +39,7 @@ pub fn should_normalize_correctly( pub fn should_calculate_angle_correctly( #[case] vector_a: Vector, #[case] vector_b: Vector, - #[case] expected_angle: f64 + #[case] expected_angle: f64, ) { let res = vector_a.angle(&vector_b); assert_eq!(res, expected_angle); @@ -49,10 +49,7 @@ pub fn should_calculate_angle_correctly( #[case(Vector::new(1., 0.), Vector::new(0., -1.))] #[case(Vector::new(0., 1.), Vector::new(1., 0.))] #[case(Vector::new(7., 7.), Vector::new(7., -7.))] -pub fn should_get_orthogonal_clockwise( - #[case] mut vector: Vector, - #[case] expected: Vector -) { +pub fn should_get_orthogonal_clockwise(#[case] mut vector: Vector, #[case] expected: Vector) { vector.orthogonal_clockwise(); assert_eq!(vector, expected); } @@ -63,7 +60,7 @@ pub fn should_get_orthogonal_clockwise( #[case(Vector::new(7., 7.), Vector::new(-7., 7.))] pub fn should_get_orthogonal_counter_clockwise( #[case] mut vector: Vector, - #[case] expected: Vector + #[case] expected: Vector, ) { vector.orthogonal_counter_clockwise(); assert_eq!(vector, expected); @@ -74,7 +71,7 @@ pub fn should_get_orthogonal_counter_clockwise( pub fn should_correctly_rotate( #[case] mut vector: Vector, #[case] radians: f64, - #[case] expected: Vector + #[case] expected: Vector, ) { vector.rotate(radians); assert_eq!(vector, expected); @@ -87,8 +84,7 @@ pub fn should_correctly_rotate( pub fn should_calculate_dot_product( #[case] mut vector: Vector, #[case] mut other: Vector, - #[case] expected: f64 - + #[case] expected: f64, ) { let dot = vector.dot(&other); assert_eq!(dot, expected); @@ -134,4 +130,3 @@ pub fn should_reflect_vector( vector.reflect(&onto); assert_eq!(vector, expected); } - diff --git a/src/lib.rs b/src/lib.rs index 8aeddd8..8a87d42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,17 @@ mod utils; +use std::cell::RefCell; +use pong::collision::collision::{Collision, CollisionDetector}; +use pong::game_field::{Field, Input, InputType}; +use pong::game_object::game_object::GameObject; +use pong::geom::geom::Vector; +use pong::geom::shape::ShapeType; +use pong::utils::utils::{DefaultLoggerFactory, Logger}; use serde::{Deserialize, Serialize}; use serde_json::json; use std::cmp::{max, min}; +use std::rc::Rc; use wasm_bindgen::prelude::*; -use pong::collision::collision::{Collision, CollisionDetector}; -use pong::game_field::{Field, Input, InputType}; -use pong::game_object::game_object::{GameObject, Shape}; -use pong::geom::geom::Vector; -use pong::utils::utils::Logger; extern crate serde_json; extern crate web_sys; @@ -27,30 +30,42 @@ macro_rules! log { static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; #[wasm_bindgen] -#[repr(packed)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize)] pub struct GameObjectDTO { pub id: u16, - pub x: u16, - pub y: u16, + pub x: f64, + pub y: f64, + pub orientation_x: f64, + pub orientation_y: f64, + pub vel_x: f64, + pub vel_y: f64, pub shape_param_1: u16, pub shape_param_2: u16, } impl GameObjectDTO { - pub fn from(obj: &GameObject) -> GameObjectDTO { + pub fn from(obj: &Rc>>) -> GameObjectDTO { + let obj = RefCell::borrow(obj); + + let pos = obj.pos(); + let orientation = obj.orientation(); + let vel = obj.vel(); + let shape = obj.shape(); return GameObjectDTO { - id: obj.id, - x: obj.pos.x as u16, - y: obj.pos.y as u16, - shape_param_1: match obj.shape_params[..] { - [p1, _] => p1, - [p1] => p1, - _ => 0, + id: obj.id(), + x: pos.x, + y: pos.y, + orientation_x: orientation.x, + orientation_y: orientation.y, + vel_x: vel.x, + vel_y: vel.y, + shape_param_1: match shape { + ShapeType::Rect(_, width, _) => *width as u16, + ShapeType::Circle(_, radius) => *radius as u16, }, - shape_param_2: match obj.shape_params[..] { - [_, p2] => p2, - _ => 0, + shape_param_2: match shape { + ShapeType::Rect(_, _, height) => *height as u16, + ShapeType::Circle(_, _) => 0, }, }; } @@ -67,7 +82,7 @@ impl InputTypeDTO { pub fn to_input_type(&self) -> InputType { match self { InputTypeDTO::UP => InputType::UP, - InputTypeDTO::DOWN => InputType::DOWN + InputTypeDTO::DOWN => InputType::DOWN, } } } @@ -84,22 +99,20 @@ impl InputDTO { return Input { input: self.input.to_input_type(), obj_id: self.obj_id, - } + }; } } #[wasm_bindgen] pub struct FieldWrapper { - field: Field + field: Field, } #[wasm_bindgen] impl FieldWrapper { pub fn new() -> FieldWrapper { - let field = Field::new(Box::new(WasmLogger {})); - FieldWrapper { - field - } + let field = Field::new(DefaultLoggerFactory::new(Box::new(WasmLogger::root()))); + FieldWrapper { field } } pub fn width(&self) -> u16 { @@ -112,52 +125,47 @@ impl FieldWrapper { pub fn tick(&mut self, inputs_js: &JsValue) { let input_dtos: Vec = inputs_js.into_serde().unwrap(); - let inputs = input_dtos.into_iter().map(|i| i.to_input()).collect::>(); + let inputs = input_dtos + .into_iter() + .map(|i| i.to_input()) + .collect::>(); self.field.tick(inputs); // log!("{:?}", self.field.collisions); } - pub fn objects(&self) -> *const GameObjectDTO { - let mut objs = vec![]; - objs.append( - &mut self.field - .balls - .iter() - .map(|ball| GameObjectDTO::from(&ball.obj)) - .collect::>(), - ); - objs.append( - &mut self.field - .players - .iter() - .map(|player| GameObjectDTO::from(&player.obj)) - .collect::>(), - ); - objs.append( - &mut self.field - .bounds.objs - .iter() - .map(|bound| GameObjectDTO::from(&bound)) - .collect::>() - ); - objs.as_ptr() - } - - pub fn get_state(&self) -> String { - let json = json!(GameObjectDTO { - shape_param_1: 0, - shape_param_2: 0, - x: 10, - y: 10, - id: 1 - }); + pub fn objects(&self) -> String { + let objs = self + .field + .objs() + .into_iter() + .map(|o| GameObjectDTO::from(o)) + .collect::>(); + let json = json!(objs); serde_json::to_string(&json).unwrap() } } -pub struct WasmLogger {} +#[derive(Clone)] +pub struct WasmLogger { + name: String +} + +impl WasmLogger { + pub fn root() -> WasmLogger { + WasmLogger {name: String::from("root")} + } +} + impl Logger for WasmLogger { + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn set_name(&mut self, name: &str) { + self.name = String::from(name); + } + fn log(&self, msg: &str) { - log!("{}", msg) + log!("[{}] {}", self.name, msg) } } diff --git a/www/index.html b/www/index.html index 710baeb..81c5df8 100644 --- a/www/index.html +++ b/www/index.html @@ -20,6 +20,20 @@ +
+ + + + +
diff --git a/www/index.js b/www/index.js index aae03c0..6f55e3e 100644 --- a/www/index.js +++ b/www/index.js @@ -14,73 +14,105 @@ canvas.width = width const ctx = canvas.getContext('2d'); +let paused = false; +let debug = false; let keysDown = new Set(); - -console.log(field.get_state()) +let actions = []; const renderLoop = () => { - let actions = getInputActions(); - field.tick(actions); - - render(); + actions = getInputActions(); + if (paused) { + requestAnimationFrame(renderLoop); + return; + } + tick(); requestAnimationFrame(renderLoop); } +const tick = () => { + field.tick(actions); + render(); +} + const render = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); - // drawField(); drawObjects(); } -const drawField = () => { - ctx.beginPath(); +window.WASM_PONG = {} +window.WASM_PONG.width = width +window.WASM_PONG.height = height - ctx.strokeStyle = GRID_COLOR; - ctx.rect(1, 1, field.width - 2, field.height - 2); +window.WASM_PONG.pauseGame = () => { + paused = true; + document.getElementById("pause-btn").disabled = true; + document.getElementById("resume-btn").disabled = false; + document.getElementById("tick-btn").disabled = false; +} - ctx.stroke(); +window.WASM_PONG.resumeGame = () => { + paused = false; + document.getElementById("pause-btn").disabled = false; + document.getElementById("resume-btn").disabled = true; + document.getElementById("tick-btn").disabled = true; +} + + +window.WASM_PONG.oneTick = () => { + if (!paused) { + return; + } + tick() +} + +window.WASM_PONG.toggleDebug = () => { + debug = !debug } const drawObjects = () => { const objects = getObjects(); - ctx.beginPath(); objects.forEach(obj => { + ctx.beginPath(); ctx.strokeStyle = GRID_COLOR; + const obj_y = height - obj.y; + const orientation_y = obj.orientation_y * -1; + const vel_y = obj.vel_y * -1; + // rect - if (obj.shape_2) { - ctx.moveTo(obj.x, obj.y) - ctx.arc(obj.x, obj.y, 10, 0, 2 * Math.PI); - ctx.rect(obj.x - obj.shape_1 / 2, obj.y - obj.shape_2 / 2, obj.shape_1, obj.shape_2); + if (obj.shape_param_2) { + ctx.moveTo(obj.x, obj_y) + ctx.arc(obj.x, obj_y, 10, 0, 2 * Math.PI); + ctx.rect(obj.x - obj.shape_param_1 / 2, obj_y - obj.shape_param_2 / 2, obj.shape_param_1, obj.shape_param_2); } // circle else { - ctx.moveTo(obj.x, obj.y); - ctx.arc(obj.x, obj.y, obj.shape_1, 0, 2 * Math.PI); + ctx.moveTo(obj.x, obj_y); + ctx.arc(obj.x, obj_y, obj.shape_param_1, 0, 2 * Math.PI); + } + ctx.stroke(); + + if (debug) { + // velocity + drawLine(ctx, obj.x, obj_y, obj.x + obj.vel_x * 20, obj_y + vel_y * 20, 'red') + // orientation + drawLine(ctx, obj.x, obj_y, obj.x + obj.orientation_x * 20, obj_y + orientation_y * 20, 'blue') + ctx.fillText(`[x: ${obj.x}, y: ${obj_y}]`, obj.x + 10, obj_y) } }) +} +const drawLine = (ctx, from_x, from_y, to_x, to_y, color) => { + ctx.beginPath(); + ctx.moveTo(from_x, from_y); + ctx.strokeStyle = color; + ctx.lineTo(to_x, to_y); ctx.stroke(); } const getObjects = () => { - const objectsPtr = field.objects(); - const objects = new Uint16Array(memory.buffer, objectsPtr, 3 * 5 + 4 * 5) // player1, player2, ball + 4x bounds - .reduce((acc, val) => { - if (!acc.length) { - return [[val]] - } - const last = acc[acc.length - 1] - if (last.length === 5) { - return [...acc, [val]] - } - return [...acc.slice(0, -1), [...last, val]] - }, []) - .map(([id, x, y, shape_1, shape_2]) => { - return {id, x, y: height - y, shape_1, shape_2}; - }); - return objects; + return JSON.parse(field.objects()); } const listenToKeys = () => {