mirror of
https://github.com/thilo-behnke/wasm-pong.git
synced 2026-02-14 14:39:51 +00:00
initial game prototype
This commit is contained in:
38
.github/workflows/rust.yml
vendored
Normal file
38
.github/workflows/rust.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Rust
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build-pong-lib:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build
|
||||
run: |
|
||||
cd pong
|
||||
cargo build --verbose
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd pong
|
||||
cargo test --verbose
|
||||
build-wasm-wrapper:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Prepare
|
||||
run: |
|
||||
(test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
|
||||
(test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
|
||||
cargo install-update -a
|
||||
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f
|
||||
- name: Build
|
||||
run: wasm-pack build
|
||||
- name: Run tests
|
||||
run: wasm-pack test
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -4,14 +4,26 @@ version = "0.1.0"
|
||||
authors = ["Thilo Behnke <thilo.behnke@gmx.net>"]
|
||||
edition = "2018"
|
||||
|
||||
[workspace]
|
||||
members = ["pong"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"console",
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2.63"
|
||||
wasm-bindgen = {version = "0.2.63", features = ["serde-serialize"]}
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0.79" }
|
||||
pong = { path = "pong", version = "0.1.0" }
|
||||
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
@@ -28,6 +40,7 @@ wee_alloc = { version = "0.4.5", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.13"
|
||||
rstest = "0.12.0"
|
||||
|
||||
[profile.release]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
|
||||
1
pong/.gitignore
vendored
Normal file
1
pong/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/.idea
|
||||
11
pong/Cargo.toml
Normal file
11
pong/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "pong"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.12.0"
|
||||
92
pong/src/collision.rs
Normal file
92
pong/src/collision.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
pub mod collision {
|
||||
use std::fmt::Debug;
|
||||
use crate::game_object::game_object::GameObject;
|
||||
use crate::geom::geom::Vector;
|
||||
|
||||
pub struct CollisionDetector {}
|
||||
|
||||
impl CollisionDetector {
|
||||
pub fn new() -> CollisionDetector {
|
||||
CollisionDetector {}
|
||||
}
|
||||
|
||||
pub fn detect_collisions(&self, objs: Vec<&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];
|
||||
i += 1;
|
||||
|
||||
let rest = &objs[i..];
|
||||
for other in rest.iter() {
|
||||
let has_collision = obj.bounding_box().overlaps(&other.bounding_box());
|
||||
if !has_collision {
|
||||
continue;
|
||||
}
|
||||
collisions.push(Collision(obj.id, other.id))
|
||||
}
|
||||
if i >= objs.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let registry = Collisions::new(collisions);
|
||||
return Box::new(registry);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CollisionRegistry : Debug {
|
||||
fn get_collisions(&self) -> Vec<&Collision>;
|
||||
fn get_collisions_by_id(&self, id: u16) -> Vec<&Collision>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Collisions {
|
||||
pub state: Vec<Collision>,
|
||||
}
|
||||
|
||||
impl Collisions {
|
||||
pub fn new(collisions: Vec<Collision>) -> Collisions {
|
||||
Collisions { state: collisions }
|
||||
}
|
||||
}
|
||||
|
||||
impl CollisionRegistry for Collisions {
|
||||
fn get_collisions(&self) -> Vec<&Collision> {
|
||||
self.state.iter().collect()
|
||||
}
|
||||
fn get_collisions_by_id(&self, id: u16) -> Vec<&Collision> {
|
||||
self.state
|
||||
.iter()
|
||||
.filter(|c| c.0 == id || c.1 == id)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct Collision(pub u16, pub u16);
|
||||
|
||||
pub struct CollisionHandler {}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
255
pong/src/game_field.rs
Normal file
255
pong/src/game_field.rs
Normal file
@@ -0,0 +1,255 @@
|
||||
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::geom::geom::Vector;
|
||||
use crate::utils::utils::{Logger, NoopLogger};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum InputType {
|
||||
UP,
|
||||
DOWN,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Input {
|
||||
pub input: InputType,
|
||||
pub obj_id: u16,
|
||||
}
|
||||
|
||||
pub struct Field {
|
||||
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>
|
||||
}
|
||||
|
||||
impl Field {
|
||||
pub fn new(logger: Box<dyn Logger>) -> Field {
|
||||
let width = 800;
|
||||
let height = 600;
|
||||
|
||||
let mut field = Field {
|
||||
logger,
|
||||
width,
|
||||
height,
|
||||
players: vec![],
|
||||
balls: vec![],
|
||||
bounds: Bounds::new(width, height),
|
||||
collisions: Box::new(Collisions::new(vec![]))
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
pub fn mock(width: u16, height: u16) -> Field {
|
||||
Field {
|
||||
logger: Box::new(NoopLogger {}),
|
||||
width,
|
||||
height,
|
||||
players: vec![],
|
||||
balls: vec![],
|
||||
bounds: Bounds::new(width, height),
|
||||
collisions: Box::new(Collisions::new(vec![]))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_player(&mut self, id: u16, x: u16, y: u16) {
|
||||
self.players.push(Player::new(id, x, y, &self));
|
||||
}
|
||||
|
||||
pub fn add_ball(&mut self, id: u16, x: u16, y: u16) {
|
||||
let ball = Ball::new(id, x, y, &self);
|
||||
self.balls.push(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.);
|
||||
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;
|
||||
}
|
||||
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()
|
||||
}
|
||||
collision => objs.iter().find(|o| o.id == collision.0).unwrap(),
|
||||
};
|
||||
|
||||
|
||||
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 ###");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn players(&self) -> Vec<&Player> {
|
||||
self.players.iter().collect()
|
||||
}
|
||||
|
||||
pub fn balls(&self) -> Vec<&Ball> {
|
||||
self.balls.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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Bounds {
|
||||
pub objs: Vec<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(),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
53
pong/src/game_object.rs
Normal file
53
pong/src/game_object.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
pub mod game_object {
|
||||
use crate::geom::geom::{BoundingBox, Vector};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Shape {
|
||||
Rect = 0,
|
||||
Circle = 1,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct GameObject {
|
||||
pub id: u16,
|
||||
pub pos: Vector,
|
||||
pub orientation: Vector,
|
||||
pub shape: Shape,
|
||||
pub shape_params: Vec<u16>,
|
||||
pub vel: Vector,
|
||||
pub is_static: bool,
|
||||
}
|
||||
|
||||
impl GameObject {
|
||||
|
||||
pub fn update_pos(&mut self) {
|
||||
self.pos.add(&self.vel);
|
||||
// Keep last orientation if vel is now zero.
|
||||
if self.vel == Vector::zero() {
|
||||
return;
|
||||
}
|
||||
let mut orientation = self.vel.clone();
|
||||
orientation.normalize();
|
||||
self.orientation = orientation;
|
||||
}
|
||||
|
||||
pub fn set_vel_x(&mut self, x: f64) {
|
||||
self.vel.x = x
|
||||
}
|
||||
|
||||
pub fn set_vel_y(&mut self, y: f64) {
|
||||
self.vel.y = y
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
244
pong/src/geom.rs
Normal file
244
pong/src/geom.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
pub mod geom {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Vector {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
impl Vector {
|
||||
pub fn zero() -> Vector {
|
||||
Vector { x: 0., y: 0. }
|
||||
}
|
||||
|
||||
pub fn unit() -> Vector {
|
||||
let mut vector = Vector { x: 1., y: 1. };
|
||||
vector.normalize();
|
||||
vector
|
||||
}
|
||||
|
||||
pub fn new(x: f64, y: f64) -> Vector {
|
||||
Vector {
|
||||
x, y
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize(&mut self) {
|
||||
if self == &Vector::zero() {
|
||||
return;
|
||||
}
|
||||
let length = self.len();
|
||||
self.x /= length;
|
||||
self.y /= length;
|
||||
}
|
||||
|
||||
pub fn orthogonal_clockwise(&mut self) {
|
||||
let updated_x = self.y;
|
||||
let updated_y = -self.x;
|
||||
self.x = updated_x;
|
||||
self.y = updated_y;
|
||||
}
|
||||
|
||||
pub fn orthogonal_counter_clockwise(&mut self) {
|
||||
let updated_x = -self.y;
|
||||
let updated_y = self.x;
|
||||
self.x = updated_x;
|
||||
self.y = updated_y;
|
||||
}
|
||||
|
||||
pub fn rotate(&mut self, radians: f64) {
|
||||
let updated_x = self.x * radians.cos() - self.y * radians.sin();
|
||||
let updated_y = self.x * radians.sin() + self.y * radians.cos();
|
||||
self.x = updated_x;
|
||||
self.y = updated_y;
|
||||
}
|
||||
|
||||
pub fn add(&mut self, other: &Vector) {
|
||||
self.x += other.x;
|
||||
self.y += other.y;
|
||||
}
|
||||
|
||||
pub fn sub(&mut self, other: &Vector) {
|
||||
self.x -= other.x;
|
||||
self.y -= other.y;
|
||||
}
|
||||
|
||||
pub fn invert(&mut self) {
|
||||
self.x = self.x * -1.;
|
||||
self.y = self.y * -1.;
|
||||
}
|
||||
|
||||
pub fn dot(&self, other: &Vector) -> f64 {
|
||||
return self.x * other.x + self.y * other.y
|
||||
}
|
||||
|
||||
pub fn angle(&self, other: &Vector) -> f64 {
|
||||
let mut self_clone = self.clone();
|
||||
self_clone.normalize();
|
||||
let mut other_clone = other.clone();
|
||||
other_clone.normalize();
|
||||
|
||||
let dot = self_clone.dot(&other_clone);
|
||||
let dot_float = dot as f64;
|
||||
let acos_res = dot_float.acos();
|
||||
(acos_res * 100.0).round() / 100.0
|
||||
}
|
||||
|
||||
// r = d - 2 * (d * n) * n
|
||||
pub fn reflect(&mut self, onto: &Vector) {
|
||||
let dot = self.dot(onto);
|
||||
if dot == 0. {
|
||||
self.invert();
|
||||
return;
|
||||
}
|
||||
let mut orthogonal = self.get_opposing_orthogonal(onto);
|
||||
let d_dot_n = orthogonal.dot(self);
|
||||
orthogonal.scalar_multiplication(d_dot_n);
|
||||
orthogonal.scalar_multiplication(2.);
|
||||
self.sub(&orthogonal);
|
||||
}
|
||||
|
||||
pub fn get_projection(&self, onto: &Vector) -> Vector {
|
||||
let mut onto_normalized = onto.clone();
|
||||
onto_normalized.normalize();
|
||||
let dot = self.dot(&onto_normalized);
|
||||
let mut projected = onto_normalized.clone();
|
||||
projected.scalar_multiplication(dot);
|
||||
projected
|
||||
}
|
||||
|
||||
pub fn get_opposing_orthogonal(&self, onto: &Vector) -> Vector {
|
||||
let mut orthogonal1 = onto.clone();
|
||||
orthogonal1.orthogonal_clockwise();
|
||||
if self.dot(&orthogonal1) < 0. {
|
||||
return orthogonal1;
|
||||
}
|
||||
let mut orthogonal2 = onto.clone();
|
||||
orthogonal2.orthogonal_counter_clockwise();
|
||||
return orthogonal2;
|
||||
}
|
||||
|
||||
pub fn scalar_multiplication(&mut self, n: f64) {
|
||||
self.x *= n;
|
||||
self.y *= n;
|
||||
}
|
||||
|
||||
pub fn len(&self) -> f64 {
|
||||
let distance = self.x.powi(2) + self.y.powi(2);
|
||||
return (distance as f64).sqrt();
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BoundingBox {
|
||||
top_left: Vector,
|
||||
top_right: Vector,
|
||||
bottom_left: Vector,
|
||||
bottom_right: Vector,
|
||||
}
|
||||
|
||||
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 {
|
||||
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.),
|
||||
};
|
||||
let top_right = Vector {
|
||||
x: center_x + (width as f64 / 2.),
|
||||
y: center_y + (height as f64 / 2.),
|
||||
};
|
||||
let bottom_left = Vector {
|
||||
x: center_x - (width as f64 / 2.),
|
||||
y: center_y - (height as f64 / 2.),
|
||||
};
|
||||
let bottom_right = Vector {
|
||||
x: center_x + (width as f64 / 2.),
|
||||
y: center_y - (height as f64 / 2.),
|
||||
};
|
||||
BoundingBox {
|
||||
top_left,
|
||||
top_right,
|
||||
bottom_left,
|
||||
bottom_right,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn points(&self) -> Vec<&Vector> {
|
||||
return vec![
|
||||
&self.top_left,
|
||||
&self.top_right,
|
||||
&self.bottom_left,
|
||||
&self.bottom_right,
|
||||
];
|
||||
}
|
||||
|
||||
pub fn vert(&self) -> Range {
|
||||
Range::new(self.bottom_left.y, self.top_left.y)
|
||||
}
|
||||
|
||||
pub fn hor(&self) -> Range {
|
||||
Range::new(self.top_left.x, self.top_right.x)
|
||||
}
|
||||
|
||||
pub fn overlaps(&self, other: &BoundingBox) -> bool {
|
||||
self.vert().overlaps(&other.vert()) && self.hor().overlaps(&other.hor())
|
||||
}
|
||||
|
||||
pub fn is_point_within(&self, point: &Vector) -> bool {
|
||||
return point.x >= self.top_left.x
|
||||
&& point.x <= self.top_right.x
|
||||
&& point.y <= self.top_left.y
|
||||
&& point.y >= self.bottom_left.y;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Range {
|
||||
min: f64,
|
||||
max: f64,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlaps(&self, other: &Range) -> bool {
|
||||
if self.min >= other.min && self.max <= other.max {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.max >= other.min && self.max <= other.max {
|
||||
return true;
|
||||
}
|
||||
|
||||
if other.min >= self.min && other.max <= self.max {
|
||||
return true;
|
||||
}
|
||||
|
||||
if other.max >= self.min && other.max <= self.max {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
5
pong/src/lib.rs
Normal file
5
pong/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod collision;
|
||||
pub mod game_object;
|
||||
pub mod geom;
|
||||
pub mod game_field;
|
||||
pub mod utils;
|
||||
12
pong/src/utils.rs
Normal file
12
pong/src/utils.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
pub mod utils {
|
||||
pub trait Logger {
|
||||
fn log(&self, msg: &str);
|
||||
}
|
||||
|
||||
pub struct NoopLogger {}
|
||||
impl Logger for NoopLogger {
|
||||
fn log(&self, msg: &str) {
|
||||
}
|
||||
}
|
||||
}
|
||||
45
pong/tests/bounding_box_tests.rs
Normal file
45
pong/tests/bounding_box_tests.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use rstest::rstest;
|
||||
use pong::geom::geom::{BoundingBox, Vector};
|
||||
|
||||
#[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)]
|
||||
pub fn should_correctly_determine_if_point_is_within_box(
|
||||
#[case] bounding_box: BoundingBox,
|
||||
#[case] point: Vector,
|
||||
#[case] expected: bool,
|
||||
) {
|
||||
let res = bounding_box.is_point_within(&point);
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
BoundingBox::create_from_coords(10., 10., 5, 5),
|
||||
BoundingBox::create_from_coords(10., 10., 5, 5),
|
||||
true
|
||||
)]
|
||||
#[case(
|
||||
BoundingBox::create_from_coords(10., 10., 5, 5),
|
||||
BoundingBox::create_from_coords(8., 8., 5, 5),
|
||||
true
|
||||
)]
|
||||
#[case(
|
||||
BoundingBox::create_from_coords(10., 10., 5, 5),
|
||||
BoundingBox::create_from_coords(4.9, 4.9, 5, 5),
|
||||
false
|
||||
)]
|
||||
#[case(
|
||||
BoundingBox::create_from_coords(10., 10., 5, 5),
|
||||
BoundingBox::create_from_coords(5., 5., 5, 5),
|
||||
true
|
||||
)]
|
||||
pub fn should_correctly_determine_if_overlap(
|
||||
#[case] bounding_box_a: BoundingBox,
|
||||
#[case] bounding_box_b: BoundingBox,
|
||||
#[case] expected: bool,
|
||||
) {
|
||||
let res = bounding_box_a.overlaps(&bounding_box_b);
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
73
pong/tests/collision_handler_test.rs
Normal file
73
pong/tests/collision_handler_test.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use rstest::rstest;
|
||||
use pong::collision::collision::CollisionHandler;
|
||||
use pong::game_object::game_object::{GameObject, Shape};
|
||||
use pong::geom::geom::Vector;
|
||||
|
||||
#[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.)},
|
||||
// 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.)},
|
||||
)]
|
||||
#[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.)},
|
||||
)]
|
||||
#[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.)},
|
||||
)]
|
||||
#[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.)},
|
||||
// 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.)},
|
||||
)]
|
||||
#[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.)},
|
||||
// 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.)},
|
||||
)]
|
||||
#[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.)},
|
||||
// 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.)},
|
||||
)]
|
||||
pub fn should_handle_collision(
|
||||
#[case] mut obj_a: GameObject,
|
||||
#[case] obj_b: GameObject,
|
||||
#[case] expected_a: GameObject,
|
||||
#[case] expected_b: GameObject,
|
||||
) {
|
||||
let handler = CollisionHandler {};
|
||||
handler.handle(&mut obj_a, &obj_b);
|
||||
assert_eq!(obj_a, expected_a);
|
||||
assert_eq!(obj_b, expected_b);
|
||||
}
|
||||
46
pong/tests/collision_tests.rs
Normal file
46
pong/tests/collision_tests.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use rstest::rstest;
|
||||
use pong::collision::collision::{Collision, CollisionDetector};
|
||||
use pong::game_object::game_object::{GameObject, Shape};
|
||||
use pong::geom::geom::{Vector};
|
||||
|
||||
#[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.)}
|
||||
],
|
||||
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.)}
|
||||
],
|
||||
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.)}
|
||||
],
|
||||
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.)}
|
||||
],
|
||||
vec![]
|
||||
)]
|
||||
pub fn should_detect_collisions(
|
||||
#[case] objs: Vec<GameObject>,
|
||||
#[case] expected_collisions: Vec<Collision>,
|
||||
) {
|
||||
let detector = CollisionDetector::new();
|
||||
let res = detector.detect_collisions(objs.iter().collect());
|
||||
assert_eq!(
|
||||
res.get_collisions(),
|
||||
expected_collisions.iter().collect::<Vec<&Collision>>()
|
||||
);
|
||||
}
|
||||
64
pong/tests/game_field_test.rs
Normal file
64
pong/tests/game_field_test.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
#[cfg(test)]
|
||||
mod game_field_tests {
|
||||
use pong::game_field::{Field, Input, InputType};
|
||||
|
||||
#[test]
|
||||
fn player_input_update_pos__up() {
|
||||
let height = 1000;
|
||||
let mut field = Field::mock(1000, height);
|
||||
field.add_player(1, 50, height / 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 / 2. + 1.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_input_update_pos__down() {
|
||||
let height = 1000;
|
||||
let mut field = Field::mock(1000, height);
|
||||
field.add_player(1, 50, height / 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 / 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.);
|
||||
}
|
||||
}
|
||||
19
pong/tests/game_object_tests.rs
Normal file
19
pong/tests/game_object_tests.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
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.)
|
||||
};
|
||||
obj.update_pos();
|
||||
assert_eq!(obj.pos, expected_pos);
|
||||
}
|
||||
137
pong/tests/vector_tests.rs
Normal file
137
pong/tests/vector_tests.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use rstest::rstest;
|
||||
use pong::geom::geom::Vector;
|
||||
use std::f64::consts::PI;
|
||||
use std::f64::consts::FRAC_PI_2;
|
||||
use std::f64::consts::FRAC_PI_4;
|
||||
|
||||
#[rstest]
|
||||
#[case(1., 0., 1.)]
|
||||
#[case(0., 1., 1.)]
|
||||
#[case(3., 4., 5.)]
|
||||
pub fn should_get_correct_length(#[case] x: f64, #[case] y: f64, #[case] expected: f64) {
|
||||
let vector = Vector { x, y };
|
||||
let len = vector.len();
|
||||
assert_eq!(len, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(1., 0., 1., 0.)]
|
||||
#[case(3., 0., 1., 0.)]
|
||||
#[case(3., 4., 3. / 5., 4. / 5.)]
|
||||
pub fn should_normalize_correctly(
|
||||
#[case] x: f64,
|
||||
#[case] y: f64,
|
||||
#[case] expected_x: f64,
|
||||
#[case] expected_y: f64,
|
||||
) {
|
||||
let mut vector = Vector { x, y };
|
||||
let expected = Vector {
|
||||
x: expected_x,
|
||||
y: expected_y,
|
||||
};
|
||||
vector.normalize();
|
||||
assert_eq!(vector, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Vector::new(1., 1.), Vector::new(2., 2.), 0.)]
|
||||
#[case(Vector::new(1., 0.), Vector::new(1., 1.), 0.79)]
|
||||
pub fn should_calculate_angle_correctly(
|
||||
#[case] vector_a: Vector,
|
||||
#[case] vector_b: Vector,
|
||||
#[case] expected_angle: f64
|
||||
) {
|
||||
let res = vector_a.angle(&vector_b);
|
||||
assert_eq!(res, expected_angle);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[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
|
||||
) {
|
||||
vector.orthogonal_clockwise();
|
||||
assert_eq!(vector, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Vector::new(0., -1.), Vector::new(1., 0.))]
|
||||
#[case(Vector::new(1., 0.), Vector::new(0., 1.))]
|
||||
#[case(Vector::new(7., 7.), Vector::new(-7., 7.))]
|
||||
pub fn should_get_orthogonal_counter_clockwise(
|
||||
#[case] mut vector: Vector,
|
||||
#[case] expected: Vector
|
||||
) {
|
||||
vector.orthogonal_counter_clockwise();
|
||||
assert_eq!(vector, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Vector::new(1., 0.), FRAC_PI_4, Vector::unit())]
|
||||
pub fn should_correctly_rotate(
|
||||
#[case] mut vector: Vector,
|
||||
#[case] radians: f64,
|
||||
#[case] expected: Vector
|
||||
) {
|
||||
vector.rotate(radians);
|
||||
assert_eq!(vector, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Vector::new(1., 0.), Vector::new(1., 0.), 1.)]
|
||||
#[case(Vector::new(1., 0.), Vector::new(0., 1.), 0.)]
|
||||
#[case(Vector::new(1., 0.), Vector::new(-1., 0.), -1.)]
|
||||
pub fn should_calculate_dot_product(
|
||||
#[case] mut vector: Vector,
|
||||
#[case] mut other: Vector,
|
||||
#[case] expected: f64
|
||||
|
||||
) {
|
||||
let dot = vector.dot(&other);
|
||||
assert_eq!(dot, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Vector::new(0., 1.), Vector::new(1., 0.), Vector::new(0., 0.))]
|
||||
#[case(Vector::new(1., 0.), Vector::new(1., 0.), Vector::new(1., 0.))]
|
||||
#[case(Vector::new(-1., 0.), Vector::new(1., 0.), Vector::new(-1., 0.))]
|
||||
#[case(Vector::new(1., 1.), Vector::new(1., 0.), Vector::new(1., 0.))]
|
||||
#[case(Vector::new(2., 1.), Vector::new(1., 0.), Vector::new(2., 0.))]
|
||||
pub fn should_get_projection(
|
||||
#[case] vector: Vector,
|
||||
#[case] other: Vector,
|
||||
#[case] expected: Vector,
|
||||
) {
|
||||
let projected = vector.get_projection(&other);
|
||||
assert_eq!(projected, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Vector::new(1., 1.), Vector::new(1., 0.), Vector::new(0., -1.))]
|
||||
#[case(Vector::new(-1., -1.), Vector::new(1., 0.), Vector::new(0., 1.))]
|
||||
pub fn should_get_opposing_orthogonal(
|
||||
#[case] vector: Vector,
|
||||
#[case] onto: Vector,
|
||||
#[case] expected: Vector,
|
||||
) {
|
||||
let orthogonal = vector.get_opposing_orthogonal(&onto);
|
||||
assert_eq!(orthogonal, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Vector::new(1., 1.), Vector::new(1., 0.), Vector::new(1., -1.))]
|
||||
#[case(Vector::new(-1., 1.), Vector::new(1., 0.), Vector::new(-1., -1.))]
|
||||
#[case(Vector::new(1., -1.), Vector::new(0., 1.), Vector::new(-1., -1.))]
|
||||
#[case(Vector::new(-1., -1.), Vector::new(0., 1.), Vector::new(1., -1.))]
|
||||
pub fn should_reflect_vector(
|
||||
#[case] mut vector: Vector,
|
||||
#[case] onto: Vector,
|
||||
#[case] expected: Vector,
|
||||
) {
|
||||
vector.reflect(&onto);
|
||||
assert_eq!(vector, expected);
|
||||
}
|
||||
|
||||
152
src/lib.rs
152
src/lib.rs
@@ -1,6 +1,24 @@
|
||||
mod utils;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::cmp::{max, min};
|
||||
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;
|
||||
|
||||
// A macro to provide `println!(..)`-style syntax for `console.log` logging.
|
||||
macro_rules! log {
|
||||
( $( $t:tt )* ) => {
|
||||
web_sys::console::log_1(&format!( $( $t )* ).into());
|
||||
}
|
||||
}
|
||||
|
||||
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
|
||||
// allocator.
|
||||
@@ -9,11 +27,137 @@ use wasm_bindgen::prelude::*;
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern {
|
||||
fn alert(s: &str);
|
||||
#[repr(packed)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct GameObjectDTO {
|
||||
pub id: u16,
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
pub shape_param_1: u16,
|
||||
pub shape_param_2: u16,
|
||||
}
|
||||
|
||||
impl GameObjectDTO {
|
||||
pub fn from(obj: &GameObject) -> GameObjectDTO {
|
||||
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,
|
||||
},
|
||||
shape_param_2: match obj.shape_params[..] {
|
||||
[_, p2] => p2,
|
||||
_ => 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn greet() {
|
||||
alert("Hello, rust-wasm!");
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InputTypeDTO {
|
||||
UP,
|
||||
DOWN,
|
||||
}
|
||||
|
||||
impl InputTypeDTO {
|
||||
pub fn to_input_type(&self) -> InputType {
|
||||
match self {
|
||||
InputTypeDTO::UP => InputType::UP,
|
||||
InputTypeDTO::DOWN => InputType::DOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InputDTO {
|
||||
pub input: InputTypeDTO,
|
||||
pub obj_id: u16,
|
||||
}
|
||||
|
||||
impl InputDTO {
|
||||
pub fn to_input(&self) -> Input {
|
||||
return Input {
|
||||
input: self.input.to_input_type(),
|
||||
obj_id: self.obj_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct FieldWrapper {
|
||||
field: Field
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl FieldWrapper {
|
||||
pub fn new() -> FieldWrapper {
|
||||
let field = Field::new(Box::new(WasmLogger {}));
|
||||
FieldWrapper {
|
||||
field
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> u16 {
|
||||
self.field.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u16 {
|
||||
self.field.height
|
||||
}
|
||||
|
||||
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>>();
|
||||
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
|
||||
});
|
||||
serde_json::to_string(&json).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WasmLogger {}
|
||||
impl Logger for WasmLogger {
|
||||
fn log(&self, msg: &str) {
|
||||
log!("{}", msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,24 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hello wasm-pack!</title>
|
||||
<style>
|
||||
body {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
|
||||
<canvas id="wasm-app-canvas"></canvas>
|
||||
<script src="./bootstrap.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
120
www/index.js
120
www/index.js
@@ -1,3 +1,121 @@
|
||||
import * as wasm from "wasm-app";
|
||||
import { FieldWrapper, GameObject } from "wasm-app";
|
||||
import { memory } from "wasm-app/rust_wasm_bg";
|
||||
|
||||
wasm.greet();
|
||||
const GRID_COLOR = "#CCCCCC";
|
||||
|
||||
const field = FieldWrapper.new();
|
||||
const width = field.width();
|
||||
const height = field.height();
|
||||
|
||||
const canvas = document.getElementById('wasm-app-canvas');
|
||||
canvas.height = height
|
||||
canvas.width = width
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let keysDown = new Set();
|
||||
|
||||
console.log(field.get_state())
|
||||
|
||||
const renderLoop = () => {
|
||||
let actions = getInputActions();
|
||||
field.tick(actions);
|
||||
|
||||
render();
|
||||
requestAnimationFrame(renderLoop);
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
// drawField();
|
||||
drawObjects();
|
||||
}
|
||||
|
||||
const drawField = () => {
|
||||
ctx.beginPath();
|
||||
|
||||
ctx.strokeStyle = GRID_COLOR;
|
||||
ctx.rect(1, 1, field.width - 2, field.height - 2);
|
||||
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
const drawObjects = () => {
|
||||
const objects = getObjects();
|
||||
ctx.beginPath();
|
||||
|
||||
objects.forEach(obj => {
|
||||
ctx.strokeStyle = GRID_COLOR;
|
||||
|
||||
// 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);
|
||||
}
|
||||
// circle
|
||||
else {
|
||||
ctx.moveTo(obj.x, obj.y);
|
||||
ctx.arc(obj.x, obj.y, obj.shape_1, 0, 2 * Math.PI);
|
||||
}
|
||||
})
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const listenToKeys = () => {
|
||||
const relevantKeys = ['ArrowUp', 'ArrowDown', 'KeyW', 'KeyS']
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!relevantKeys.includes(e.code)) {
|
||||
return;
|
||||
}
|
||||
keysDown.add(e.code)
|
||||
})
|
||||
document.addEventListener('keyup', (e) => {
|
||||
if (!relevantKeys.includes(e.code)) {
|
||||
return;
|
||||
}
|
||||
keysDown.delete(e.code);
|
||||
})
|
||||
}
|
||||
|
||||
const getInputActions = () => {
|
||||
return [...keysDown].map(key => {
|
||||
switch(key) {
|
||||
case 'KeyW':
|
||||
return {input: 'UP', obj_id: 0}
|
||||
case 'KeyS':
|
||||
return {input: 'DOWN', obj_id: 0}
|
||||
case 'ArrowUp':
|
||||
return {input: 'UP', obj_id: 1}
|
||||
case 'ArrowDown':
|
||||
return {input: 'DOWN', obj_id: 1}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}).filter(it => !!it);
|
||||
}
|
||||
|
||||
listenToKeys();
|
||||
render();
|
||||
requestAnimationFrame(renderLoop);
|
||||
|
||||
Reference in New Issue
Block a user