From 5d11565a4d00abfa2598023c5408eb68142fe08d Mon Sep 17 00:00:00 2001 From: Sebastian Benjamin Date: Wed, 12 Feb 2025 22:59:48 -0800 Subject: [PATCH] Implement colliders --- .gitignore | 3 +- client/danmaku!/network_manager.gd | 2 + client/danmaku!/player.gd | 2 +- ffi-wrapper/src/ffi.rs | 41 ++++++++++++++++++- ffi-wrapper/src/lib.rs | 3 +- godot-extension/src/bullet.rs | 12 ++++-- godot-extension/src/collision.rs | 54 +++++++++++++++++++++++++ godot-extension/src/lib.rs | 1 + server/main.go | 63 ++++++++++++++++-------------- shared/src/bullet.rs | 11 +++++- shared/src/collision.rs | 22 +++++++++++ shared/src/lib.rs | 3 +- 12 files changed, 177 insertions(+), 40 deletions(-) create mode 100644 godot-extension/src/collision.rs create mode 100644 shared/src/collision.rs diff --git a/.gitignore b/.gitignore index 0203e78..5641b27 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ release/ target/ *.tmp -lib/ \ No newline at end of file +lib/ +*.vscode/ \ No newline at end of file diff --git a/client/danmaku!/network_manager.gd b/client/danmaku!/network_manager.gd index e6ed3b8..caa9214 100644 --- a/client/danmaku!/network_manager.gd +++ b/client/danmaku!/network_manager.gd @@ -39,6 +39,7 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData): match p_state.op_code: 2: var data = JSON.parse_string(p_state.data) + print(data) # Set player position given server bounds-checking if data["forcePlayerPos"]: @@ -52,6 +53,7 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData): int(b["tick"]), b["x"], b["y"], + b["radius"], b["vel_x"], b["vel_y"]) bullet.texture = load("res://test-bullet.png") diff --git a/client/danmaku!/player.gd b/client/danmaku!/player.gd index 86da5c5..e78d73d 100644 --- a/client/danmaku!/player.gd +++ b/client/danmaku!/player.gd @@ -6,7 +6,7 @@ var velocity := Vector2.ZERO func get_input(): if Input.is_action_pressed("Slow Mode"): - speed = 40 + speed = 30 else: speed = 80 diff --git a/ffi-wrapper/src/ffi.rs b/ffi-wrapper/src/ffi.rs index b80357f..96773e7 100644 --- a/ffi-wrapper/src/ffi.rs +++ b/ffi-wrapper/src/ffi.rs @@ -1,14 +1,17 @@ use shared::bullet::Bullet; +use shared::collision::Circle; +// Bullet routines #[no_mangle] pub extern "C" fn new_bullet( - class: u8, spawn_time: i64, spawn_x: f64, spawn_y: f64, param_x: f64, param_y: f64 + class: u8, spawn_time: i64, spawn_x: f64, spawn_y: f64, radius: f64, param_x: f64, param_y: f64 ) -> *mut Bullet { let bullet = Bullet::new( class, spawn_time, spawn_x, spawn_y, + radius, [param_x, param_y], ); Box::into_raw(Box::new(bullet)) @@ -41,4 +44,40 @@ pub extern "C" fn destroy_bullet(bullet: *mut Bullet) { drop(Box::from_raw(bullet)); } } +} + +#[no_mangle] +pub extern "C" fn bullet_collides_with(tick: i64, bullet: *mut Bullet, c: *const Circle) -> bool { + if bullet.is_null() || c.is_null() { + return false; + } + + unsafe { (*bullet).collides_with(tick, &*c) } +} + + + +// Collision +#[no_mangle] +pub extern "C" fn new_circle(x: f64, y: f64, radius: f64) -> *mut Circle { + let circle = Circle::new(x, y, radius); + Box::into_raw(Box::new(circle)) +} + +#[no_mangle] +pub extern "C" fn destroy_circle(circle: *mut Circle) { + if !circle.is_null() { + unsafe { + drop(Box::from_raw(circle)); + } + } +} + +#[no_mangle] +pub extern "C" fn circle_collides_with(c1: *const Circle, c2: *const Circle) -> bool { + if c1.is_null() || c2.is_null() { + return false; + } + + unsafe { (*c1).collides_with(&*c2) } } \ No newline at end of file diff --git a/ffi-wrapper/src/lib.rs b/ffi-wrapper/src/lib.rs index 7445988..5e714d3 100644 --- a/ffi-wrapper/src/lib.rs +++ b/ffi-wrapper/src/lib.rs @@ -1,3 +1,4 @@ mod ffi; -pub use shared::bullet::Bullet; \ No newline at end of file +pub use shared::bullet::Bullet; +pub use shared::collision::Circle; \ No newline at end of file diff --git a/godot-extension/src/bullet.rs b/godot-extension/src/bullet.rs index 659135c..dbddfb1 100644 --- a/godot-extension/src/bullet.rs +++ b/godot-extension/src/bullet.rs @@ -1,12 +1,12 @@ use godot::prelude::*; use shared::bullet::Bullet; use godot::classes::{ Sprite2D, ISprite2D }; +use crate::collision::DanmakuCircle; #[derive(GodotClass)] #[class(base=Sprite2D)] struct DanmakuBullet { bullet_state: Bullet, - base: Base, } @@ -14,7 +14,7 @@ struct DanmakuBullet { impl ISprite2D for DanmakuBullet { fn init(base: Base) -> Self { Self { - bullet_state: Bullet::new(0, 0, 0.0, 0.0, [0.0, 0.0]), + bullet_state: Bullet::new(0, 0, 0.0, 0.0, 0.0, [0.0, 0.0]), base, } } @@ -23,8 +23,8 @@ impl ISprite2D for DanmakuBullet { #[godot_api] impl DanmakuBullet { #[func] - fn setup_bullet(&mut self, class: u8, spawn_time: i64, spawn_x: f64, spawn_y: f64, param_x: f64, param_y: f64) { - self.bullet_state = Bullet::new(class, spawn_time, spawn_x, spawn_y, [param_x, param_y]); + fn setup_bullet(&mut self, class: u8, spawn_time: i64, spawn_x: f64, spawn_y: f64, radius: f64, param_x: f64, param_y: f64) { + self.bullet_state = Bullet::new(class, spawn_time, spawn_x, spawn_y, radius, [param_x, param_y]); } #[func] @@ -38,4 +38,8 @@ impl DanmakuBullet { self.bullet_state.beyond_kill_boundary(tick) } + #[func] + fn collides_with_circle(&self, tick: i64, circle: Gd) -> bool { + self.bullet_state.collides_with(tick, &circle.bind().circle_state) + } } \ No newline at end of file diff --git a/godot-extension/src/collision.rs b/godot-extension/src/collision.rs new file mode 100644 index 0000000..1703ec6 --- /dev/null +++ b/godot-extension/src/collision.rs @@ -0,0 +1,54 @@ +use godot::prelude::*; +use shared::collision::Circle; +use godot::classes::{ Node2D, INode2D }; + +#[derive(GodotClass)] +#[class(base=Node2D)] +pub struct DanmakuCircle { + pub circle_state: Circle, + base: Base, +} + +#[godot_api] +impl INode2D for DanmakuCircle { + fn init(base: Base) -> Self { + Self { + circle_state: Circle::new(0.0, 0.0, 0.0), + base, + } + } +} + +#[godot_api] +impl DanmakuCircle { + #[func] + fn setup_circle(&mut self, x: f64, y: f64, radius: f64) { + self.circle_state = Circle::new(x, y, radius); + } + + #[func] + fn get_position(&self) -> Vector2 { + Vector2::new(self.circle_state.x as f32, self.circle_state.y as f32) + } + + #[func] + fn set_position(&mut self, new_x: f64, new_y: f64) { + self.circle_state.x = new_x; + self.circle_state.y = new_y; + } + + #[func] + fn get_radius(&self) -> f64 { + self.circle_state.radius + } + + #[func] + fn set_radius(&mut self, new_radius: f64) { + self.circle_state.radius = new_radius; + } + + #[func] + fn collides_with(&self, other: Gd) -> bool { + self.circle_state.collides_with(&other.bind().circle_state) + } +} \ No newline at end of file diff --git a/godot-extension/src/lib.rs b/godot-extension/src/lib.rs index 129f005..d38b2d7 100644 --- a/godot-extension/src/lib.rs +++ b/godot-extension/src/lib.rs @@ -1,4 +1,5 @@ mod bullet; +mod collision; use godot::prelude::*; struct Danmaku; diff --git a/server/main.go b/server/main.go index f573548..dc172db 100644 --- a/server/main.go +++ b/server/main.go @@ -40,20 +40,22 @@ const ( // Interface for registering match handlers type BattleRoyaleMatch struct{} -type Position struct { +type PlayerStageState struct { + col *C.Circle + bullets []*C.Bullet + updatePlayerPos bool + health int + graze int +} + +type PlayerUpdate struct { X float64 `json:"x"` Y float64 `json:"y"` } -type PlayerStageState struct { - pos Position - bullets []*C.Bullet - updatePlayerPos bool -} - type GameTickUpdate struct { Tick int64 `json:"tick"` - PlayerPos Position `json:"playerPos"` + PlayerPos map[string]interface{} `json:"playerPos"` NewBullets []map[string]interface{} `json:"newBullets"` ForcePlayerPos bool `json:"forcePlayerPos"` } @@ -109,12 +111,10 @@ func (m *BattleRoyaleMatch) MatchJoin(ctx context.Context, logger runtime.Logger lobbyState.presences[presences[i].GetSessionId()] = &PresenceState{ presence: presences[i], stageState: PlayerStageState{ - pos: Position{ - X: STAGE_WIDTH * 0.5, - Y: STAGE_HEIGHT - STAGE_HEIGHT*0.1, - }, + col: C.new_circle(C.double(STAGE_WIDTH*0.5), C.double(STAGE_HEIGHT-STAGE_HEIGHT*0.1), C.double(STAGE_WIDTH*0.09)), bullets: []*C.Bullet{}, updatePlayerPos: true, + health: 3, }, } } @@ -138,7 +138,7 @@ func (m *BattleRoyaleMatch) MatchLeave(ctx context.Context, logger runtime.Logge for _, bullet := range playerState.stageState.bullets { C.destroy_bullet(bullet) } - + C.destroy_circle(playerState.stageState.col) delete(lobbyState.presences, sessionID) } } @@ -197,21 +197,21 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger } // Parse player message - var pos Position - if err := json.Unmarshal(msg.GetData(), &pos); err != nil { + var update PlayerUpdate + if err := json.Unmarshal(msg.GetData(), &update); err != nil { logger.Warn("Failed to parse input: %v", err) continue } // Player movement bounds detection - clampedX := pos.X < 0 || pos.X > STAGE_WIDTH - clampedY := pos.Y < 0 || pos.Y > STAGE_HEIGHT + clampedX := update.X < 0 || update.X > STAGE_WIDTH + clampedY := update.Y < 0 || update.Y > STAGE_HEIGHT - pos.X = math.Max(0, math.Min(pos.X, STAGE_WIDTH)) - pos.Y = math.Max(0, math.Min(pos.Y, STAGE_HEIGHT)) + update.X = math.Max(0, math.Min(update.X, STAGE_WIDTH)) + update.Y = math.Max(0, math.Min(update.Y, STAGE_HEIGHT)) - lobbyState.presences[msg.GetSessionId()].stageState.pos.X = pos.X - lobbyState.presences[msg.GetSessionId()].stageState.pos.Y = pos.Y + lobbyState.presences[msg.GetSessionId()].stageState.col.x = C.double(update.X) + lobbyState.presences[msg.GetSessionId()].stageState.col.y = C.double(update.Y) if clampedX || clampedY { lobbyState.presences[msg.GetSessionId()].stageState.updatePlayerPos = true @@ -235,6 +235,7 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger if tick%10 == 0 { velx := (rand.Float64() * STAGE_WIDTH) / float64(lobbyState.tickRate) vely := (rand.Float64() * STAGE_WIDTH) / float64(lobbyState.tickRate) + radius := 0.03 vel_x_sign := 2*rand.Intn(2) - 1 vel_y_sign := 2*rand.Intn(2) - 1 @@ -243,6 +244,7 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger C.int64_t(tick), C.double(STAGE_WIDTH*rand.Float64()), C.double(STAGE_HEIGHT*rand.Float64()), + C.double(radius), C.double(float64(vel_x_sign)*velx), C.double(float64(vel_y_sign)*vely), ) @@ -253,12 +255,13 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger C.bullet_get_current_pos(bullet, C.int64_t(tick), &x, &y) bulletData := map[string]interface{}{ - "class": BULLET_LINEAR, - "tick": tick, - "x": float64(x), - "y": float64(y), - "vel_x": float64(vel_x_sign) * velx, - "vel_y": float64(vel_y_sign) * vely, + "class": BULLET_LINEAR, + "tick": tick, + "x": float64(x), + "y": float64(y), + "radius": float64(radius), + "vel_x": float64(vel_x_sign) * velx, + "vel_y": float64(vel_y_sign) * vely, } newBulletsToBroadcast = append(newBulletsToBroadcast, bulletData) @@ -266,9 +269,9 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger var tickData = GameTickUpdate{ Tick: tick, - PlayerPos: Position{ - X: v.stageState.pos.X, - Y: v.stageState.pos.Y, + PlayerPos: map[string]interface{}{ + "x": v.stageState.col.x, + "y": v.stageState.col.y, }, NewBullets: newBulletsToBroadcast, ForcePlayerPos: v.stageState.updatePlayerPos, diff --git a/shared/src/bullet.rs b/shared/src/bullet.rs index f60baef..a4867df 100644 --- a/shared/src/bullet.rs +++ b/shared/src/bullet.rs @@ -1,3 +1,5 @@ +use crate::collision; + #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct Bullet { @@ -5,18 +7,20 @@ pub struct Bullet { pub spawn_time: i64, pub spawn_x: f64, pub spawn_y: f64, + pub radius: f64, pub parameters: [f64; 2], } impl Bullet { pub const BULLET_LINEAR: u8 = 0; - pub fn new(class: u8, spawn_time: i64, spawn_x: f64, spawn_y: f64, parameters: [f64; 2]) -> Self { + pub fn new(class: u8, spawn_time: i64, spawn_x: f64, spawn_y: f64, radius: f64, parameters: [f64; 2]) -> Self { Self { class, spawn_time, spawn_x, spawn_y, + radius, parameters, } } @@ -43,4 +47,9 @@ impl Bullet { || y < -BULLET_KILL_BUFFER_WIDTH || y > STAGE_HEIGHT + BULLET_KILL_BUFFER_WIDTH } + + pub fn collides_with(&self, tick: i64, circle: &collision::Circle) -> bool { + let (current_x, current_y) = self.get_current_pos(tick); + circle.collides_with(&collision::Circle::new(current_x, current_y, self.radius)) + } } \ No newline at end of file diff --git a/shared/src/collision.rs b/shared/src/collision.rs new file mode 100644 index 0000000..2f5c9f6 --- /dev/null +++ b/shared/src/collision.rs @@ -0,0 +1,22 @@ +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct Circle { + pub x: f64, + pub y: f64, + pub radius: f64, +} + +impl Circle { + pub fn new(x: f64, y: f64, radius: f64) -> Self { + Circle { x, y, radius } + } + + pub fn collides_with(&self, other: &Circle) -> bool { + let dx = self.x - other.x; + let dy = self.y - other.y; + let distance_squared = dx * dx + dy * dy; + let radius_sum = self.radius + other.radius; + + distance_squared <= radius_sum * radius_sum + } +} \ No newline at end of file diff --git a/shared/src/lib.rs b/shared/src/lib.rs index a04085d..bf6a41e 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -1 +1,2 @@ -pub mod bullet; \ No newline at end of file +pub mod bullet; +pub mod collision; \ No newline at end of file