refactoring/game-obj-components

This commit is contained in:
Thilo Behnke
2022-07-10 23:38:12 +02:00
parent 046fcded31
commit 3ec9b37eac
17 changed files with 1204 additions and 500 deletions
+145 -26
View File
@@ -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<CollisionGroup>
}
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<dyn CollisionRegistry> {
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<dyn Logger>
}
impl CollisionDetector {
pub fn new(logger_factory: &Box<dyn LoggerFactory>) -> CollisionDetector {
let logger = logger_factory.get("collision_detector");
CollisionDetector {
config: CollisionDetectorConfig::new(),
logger
}
}
pub fn set_groups(&mut self, groups: Vec<CollisionGroup>) {
self.config.groups = groups;
}
pub fn detect_collisions(
&self,
objs: Vec<Rc<RefCell<Box<dyn GameObject>>>>,
) -> Box<dyn CollisionRegistry> {
if objs.is_empty() {
return Box::new(Collisions::new(vec![]));
}
let mut collisions: Vec<Collision> = 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<RefCell<Box<dyn GameObject>>>, Rc<RefCell<Box<dyn GameObject>>>)>
}
type CollisionCallback = fn(Rc<RefCell<Box<dyn GameObject>>>, Rc<RefCell<Box<dyn GameObject>>>);
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<RefCell<Box<dyn GameObject>>>, Rc<RefCell<Box<dyn GameObject>>>)) -> 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<dyn Logger>,
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<dyn LoggerFactory>) -> 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<RefCell<Box<dyn GameObject>>>, Rc<RefCell<Box<dyn GameObject>>>),
) {
self.handlers.add(mapping, callback)
}
pub fn handle(
&self,
obj_a: Rc<RefCell<Box<dyn GameObject>>>,
obj_b: Rc<RefCell<Box<dyn GameObject>>>,
) -> 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<dyn GameObject>, obj_b: &Box<dyn GameObject>) {
// 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);
// }
}
}
+220 -181
View File
@@ -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<dyn LoggerFactory>,
pub logger: Box<dyn Logger>,
pub width: u16,
pub height: u16,
pub players: Vec<Player>,
pub balls: Vec<Ball>,
pub bounds: Bounds,
pub collisions: Box<dyn CollisionRegistry>
pub collisions: Box<dyn CollisionRegistry>,
objs: Vec<Rc<RefCell<Box<dyn GameObject>>>>,
collision_detector: CollisionDetector,
collision_handler: CollisionHandler,
}
impl Field {
pub fn new(logger: Box<dyn Logger>) -> Field {
pub fn new(logger_factory: Box<dyn LoggerFactory>) -> 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<Input>) {
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<Input>) {
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<GameObject> = vec![];
objs.extend(
self.players
.clone()
.into_iter()
.map(|p| p.obj)
.collect::<Vec<GameObject>>(),
);
objs.extend(
self.balls
.clone()
.into_iter()
.map(|b| b.obj)
.collect::<Vec<GameObject>>(),
);
objs.extend(
self.bounds.objs
.clone()
.into_iter()
.collect::<Vec<GameObject>>()
);
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<dyn CollisionRegistry> {
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<RefCell<Box<dyn GameObject>>>> {
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<dyn GameObject> {
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<dyn GameObject> {
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<GameObject>,
impl DefaultGameObject {
pub fn bounds(width: u16, height: u16) -> Vec<Bounds> {
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<dyn GameObject>);
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<dyn GameObject> {
self.1
}
}
+175 -32
View File
@@ -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<u16>,
pub vel: Vector,
pub is_static: bool,
pub obj_type: String,
geom: Box<dyn GeomComp>,
physics: Box<dyn PhysicsComp>,
}
impl GameObject {
impl DefaultGameObject {
pub fn new(
id: u16,
obj_type: String,
geom: Box<dyn GeomComp>,
physics: Box<dyn PhysicsComp>,
) -> 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
}
}
}
+111 -26
View File
@@ -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(&center, 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.)
}
}
}
}
+2 -1
View File
@@ -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;
+58
View File
@@ -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<RefCell<Box<dyn GameObject>>>,
player: Rc<RefCell<Box<dyn GameObject>>>,
) {
// 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<RefCell<Box<dyn GameObject>>>,
bound: Rc<RefCell<Box<dyn GameObject>>>,
) {
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<RefCell<Box<dyn GameObject>>>,
bound: Rc<RefCell<Box<dyn GameObject>>>,
) {
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;
}
}
+41 -2
View File
@@ -1,12 +1,51 @@
pub mod utils {
pub trait LoggerFactory {
fn get(&self, name: &str) -> Box<dyn Logger>;
}
pub struct DefaultLoggerFactory {
proto: Box<dyn Logger>
}
impl LoggerFactory for DefaultLoggerFactory {
fn get(&self, name: &str) -> Box<dyn Logger> {
let mut clone = self.proto.box_clone();
clone.set_name(name);
clone
}
}
impl DefaultLoggerFactory {
pub fn new(proto: Box<dyn Logger>) -> Box<dyn LoggerFactory> {
Box::new(DefaultLoggerFactory {proto})
}
pub fn noop() -> Box<dyn LoggerFactory> {
Box::new(DefaultLoggerFactory {proto: Box::new(NoopLogger {})})
}
}
pub trait Logger {
fn box_clone(&self) -> Box<dyn Logger>;
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<dyn Logger> {
Box::new(self.clone())
}
fn set_name(&mut self, name: &str) {}
fn log(&self, msg: &str) {}
}
impl NoopLogger {
fn new() -> Box<dyn Logger> {
Box::new(NoopLogger {})
}
}
}
+12 -12
View File
@@ -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(
+76 -46
View File
@@ -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<RefCell<Box<dyn GameObject>>>,
#[case] mut obj_b: Rc<RefCell<Box<dyn GameObject>>>,
#[case] expected_a: Rc<RefCell<Box<dyn GameObject>>>,
#[case] expected_b: Rc<RefCell<Box<dyn GameObject>>>,
) {
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<RefCell<Box<dyn GameObject>>> {
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)),
))))
}
+96 -14
View File
@@ -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<GameObject>,
#[case] objs: Vec<Rc<RefCell<Box<dyn GameObject>>>>,
#[case] expected_collisions: Vec<Collision>,
) {
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::<Vec<&Collision>>()
);
}
#[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<RefCell<Box<dyn GameObject>>> {
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!()
}
}
+16 -36
View File
@@ -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.);
}
}
+21 -13
View File
@@ -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);
}
+71
View File
@@ -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<RefCell<Box<dyn GameObject>>>,
#[case] mut bounds: Rc<RefCell<Box<dyn GameObject>>>,
#[case] mut player_expected: Rc<RefCell<Box<dyn GameObject>>>,
#[case] mut bounds_expected: Rc<RefCell<Box<dyn GameObject>>>
) {
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<RefCell<Box<dyn GameObject>>> {
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<RefCell<Box<dyn GameObject>>> {
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()));
}
+7 -12
View File
@@ -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);
}
+72 -64
View File
@@ -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<RefCell<Box<dyn GameObject>>>) -> 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<InputDTO> = inputs_js.into_serde().unwrap();
let inputs = input_dtos.into_iter().map(|i| i.to_input()).collect::<Vec<Input>>();
let inputs = input_dtos
.into_iter()
.map(|i| i.to_input())
.collect::<Vec<Input>>();
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::<Vec<GameObjectDTO>>(),
);
objs.append(
&mut self.field
.players
.iter()
.map(|player| GameObjectDTO::from(&player.obj))
.collect::<Vec<GameObjectDTO>>(),
);
objs.append(
&mut self.field
.bounds.objs
.iter()
.map(|bound| GameObjectDTO::from(&bound))
.collect::<Vec<GameObjectDTO>>()
);
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::<Vec<GameObjectDTO>>();
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<dyn Logger> {
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)
}
}
+14
View File
@@ -20,6 +20,20 @@
</head>
<body>
<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
<div>
<button id="pause-btn" onclick="WASM_PONG.pauseGame()">
Pause
</button>
<button id="resume-btn" onclick="WASM_PONG.resumeGame()" disabled>
Resume
</button>
<button id="tick-btn" onclick="WASM_PONG.oneTick()" disabled>
Tick
</button>
<button id="debug-btn" onclick="WASM_PONG.toggleDebug()">
Debug
</button>
</div>
<canvas id="wasm-app-canvas"></canvas>
<script src="./bootstrap.js"></script>
</body>
+67 -35
View File
@@ -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 = () => {