Archived
1
0
Fork 0

Implement colliders

This commit is contained in:
Sebastian Benjamin 2025-02-12 22:59:48 -08:00
parent d5cf0528f8
commit 5d11565a4d
12 changed files with 177 additions and 40 deletions

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ target/
*.tmp *.tmp
lib/ lib/
*.vscode/

View file

@ -39,6 +39,7 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData):
match p_state.op_code: match p_state.op_code:
2: 2:
var data = JSON.parse_string(p_state.data) var data = JSON.parse_string(p_state.data)
print(data)
# Set player position given server bounds-checking # Set player position given server bounds-checking
if data["forcePlayerPos"]: if data["forcePlayerPos"]:
@ -52,6 +53,7 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData):
int(b["tick"]), int(b["tick"]),
b["x"], b["x"],
b["y"], b["y"],
b["radius"],
b["vel_x"], b["vel_x"],
b["vel_y"]) b["vel_y"])
bullet.texture = load("res://test-bullet.png") bullet.texture = load("res://test-bullet.png")

View file

@ -6,7 +6,7 @@ var velocity := Vector2.ZERO
func get_input(): func get_input():
if Input.is_action_pressed("Slow Mode"): if Input.is_action_pressed("Slow Mode"):
speed = 40 speed = 30
else: else:
speed = 80 speed = 80

View file

@ -1,14 +1,17 @@
use shared::bullet::Bullet; use shared::bullet::Bullet;
use shared::collision::Circle;
// Bullet routines
#[no_mangle] #[no_mangle]
pub extern "C" fn new_bullet( 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 { ) -> *mut Bullet {
let bullet = Bullet::new( let bullet = Bullet::new(
class, class,
spawn_time, spawn_time,
spawn_x, spawn_x,
spawn_y, spawn_y,
radius,
[param_x, param_y], [param_x, param_y],
); );
Box::into_raw(Box::new(bullet)) Box::into_raw(Box::new(bullet))
@ -42,3 +45,39 @@ pub extern "C" fn destroy_bullet(bullet: *mut 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) }
}

View file

@ -1,3 +1,4 @@
mod ffi; mod ffi;
pub use shared::bullet::Bullet; pub use shared::bullet::Bullet;
pub use shared::collision::Circle;

View file

@ -1,12 +1,12 @@
use godot::prelude::*; use godot::prelude::*;
use shared::bullet::Bullet; use shared::bullet::Bullet;
use godot::classes::{ Sprite2D, ISprite2D }; use godot::classes::{ Sprite2D, ISprite2D };
use crate::collision::DanmakuCircle;
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=Sprite2D)] #[class(base=Sprite2D)]
struct DanmakuBullet { struct DanmakuBullet {
bullet_state: Bullet, bullet_state: Bullet,
base: Base<Sprite2D>, base: Base<Sprite2D>,
} }
@ -14,7 +14,7 @@ struct DanmakuBullet {
impl ISprite2D for DanmakuBullet { impl ISprite2D for DanmakuBullet {
fn init(base: Base<Sprite2D>) -> Self { fn init(base: Base<Sprite2D>) -> Self {
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, base,
} }
} }
@ -23,8 +23,8 @@ impl ISprite2D for DanmakuBullet {
#[godot_api] #[godot_api]
impl DanmakuBullet { impl DanmakuBullet {
#[func] #[func]
fn setup_bullet(&mut self, class: u8, spawn_time: i64, spawn_x: f64, spawn_y: f64, param_x: f64, param_y: f64) { 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, [param_x, param_y]); self.bullet_state = Bullet::new(class, spawn_time, spawn_x, spawn_y, radius, [param_x, param_y]);
} }
#[func] #[func]
@ -38,4 +38,8 @@ impl DanmakuBullet {
self.bullet_state.beyond_kill_boundary(tick) self.bullet_state.beyond_kill_boundary(tick)
} }
#[func]
fn collides_with_circle(&self, tick: i64, circle: Gd<DanmakuCircle>) -> bool {
self.bullet_state.collides_with(tick, &circle.bind().circle_state)
}
} }

View file

@ -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<Node2D>,
}
#[godot_api]
impl INode2D for DanmakuCircle {
fn init(base: Base<Node2D>) -> 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<DanmakuCircle>) -> bool {
self.circle_state.collides_with(&other.bind().circle_state)
}
}

View file

@ -1,4 +1,5 @@
mod bullet; mod bullet;
mod collision;
use godot::prelude::*; use godot::prelude::*;
struct Danmaku; struct Danmaku;

View file

@ -40,20 +40,22 @@ const (
// Interface for registering match handlers // Interface for registering match handlers
type BattleRoyaleMatch struct{} 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"` X float64 `json:"x"`
Y float64 `json:"y"` Y float64 `json:"y"`
} }
type PlayerStageState struct {
pos Position
bullets []*C.Bullet
updatePlayerPos bool
}
type GameTickUpdate struct { type GameTickUpdate struct {
Tick int64 `json:"tick"` Tick int64 `json:"tick"`
PlayerPos Position `json:"playerPos"` PlayerPos map[string]interface{} `json:"playerPos"`
NewBullets []map[string]interface{} `json:"newBullets"` NewBullets []map[string]interface{} `json:"newBullets"`
ForcePlayerPos bool `json:"forcePlayerPos"` ForcePlayerPos bool `json:"forcePlayerPos"`
} }
@ -109,12 +111,10 @@ func (m *BattleRoyaleMatch) MatchJoin(ctx context.Context, logger runtime.Logger
lobbyState.presences[presences[i].GetSessionId()] = &PresenceState{ lobbyState.presences[presences[i].GetSessionId()] = &PresenceState{
presence: presences[i], presence: presences[i],
stageState: PlayerStageState{ stageState: PlayerStageState{
pos: Position{ col: C.new_circle(C.double(STAGE_WIDTH*0.5), C.double(STAGE_HEIGHT-STAGE_HEIGHT*0.1), C.double(STAGE_WIDTH*0.09)),
X: STAGE_WIDTH * 0.5,
Y: STAGE_HEIGHT - STAGE_HEIGHT*0.1,
},
bullets: []*C.Bullet{}, bullets: []*C.Bullet{},
updatePlayerPos: true, updatePlayerPos: true,
health: 3,
}, },
} }
} }
@ -138,7 +138,7 @@ func (m *BattleRoyaleMatch) MatchLeave(ctx context.Context, logger runtime.Logge
for _, bullet := range playerState.stageState.bullets { for _, bullet := range playerState.stageState.bullets {
C.destroy_bullet(bullet) C.destroy_bullet(bullet)
} }
C.destroy_circle(playerState.stageState.col)
delete(lobbyState.presences, sessionID) delete(lobbyState.presences, sessionID)
} }
} }
@ -197,21 +197,21 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
} }
// Parse player message // Parse player message
var pos Position var update PlayerUpdate
if err := json.Unmarshal(msg.GetData(), &pos); err != nil { if err := json.Unmarshal(msg.GetData(), &update); err != nil {
logger.Warn("Failed to parse input: %v", err) logger.Warn("Failed to parse input: %v", err)
continue continue
} }
// Player movement bounds detection // Player movement bounds detection
clampedX := pos.X < 0 || pos.X > STAGE_WIDTH clampedX := update.X < 0 || update.X > STAGE_WIDTH
clampedY := pos.Y < 0 || pos.Y > STAGE_HEIGHT clampedY := update.Y < 0 || update.Y > STAGE_HEIGHT
pos.X = math.Max(0, math.Min(pos.X, STAGE_WIDTH)) update.X = math.Max(0, math.Min(update.X, STAGE_WIDTH))
pos.Y = math.Max(0, math.Min(pos.Y, STAGE_HEIGHT)) 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.col.x = C.double(update.X)
lobbyState.presences[msg.GetSessionId()].stageState.pos.Y = pos.Y lobbyState.presences[msg.GetSessionId()].stageState.col.y = C.double(update.Y)
if clampedX || clampedY { if clampedX || clampedY {
lobbyState.presences[msg.GetSessionId()].stageState.updatePlayerPos = true 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 { if tick%10 == 0 {
velx := (rand.Float64() * STAGE_WIDTH) / float64(lobbyState.tickRate) velx := (rand.Float64() * STAGE_WIDTH) / float64(lobbyState.tickRate)
vely := (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_x_sign := 2*rand.Intn(2) - 1
vel_y_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.int64_t(tick),
C.double(STAGE_WIDTH*rand.Float64()), C.double(STAGE_WIDTH*rand.Float64()),
C.double(STAGE_HEIGHT*rand.Float64()), C.double(STAGE_HEIGHT*rand.Float64()),
C.double(radius),
C.double(float64(vel_x_sign)*velx), C.double(float64(vel_x_sign)*velx),
C.double(float64(vel_y_sign)*vely), 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) C.bullet_get_current_pos(bullet, C.int64_t(tick), &x, &y)
bulletData := map[string]interface{}{ bulletData := map[string]interface{}{
"class": BULLET_LINEAR, "class": BULLET_LINEAR,
"tick": tick, "tick": tick,
"x": float64(x), "x": float64(x),
"y": float64(y), "y": float64(y),
"vel_x": float64(vel_x_sign) * velx, "radius": float64(radius),
"vel_y": float64(vel_y_sign) * vely, "vel_x": float64(vel_x_sign) * velx,
"vel_y": float64(vel_y_sign) * vely,
} }
newBulletsToBroadcast = append(newBulletsToBroadcast, bulletData) newBulletsToBroadcast = append(newBulletsToBroadcast, bulletData)
@ -266,9 +269,9 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
var tickData = GameTickUpdate{ var tickData = GameTickUpdate{
Tick: tick, Tick: tick,
PlayerPos: Position{ PlayerPos: map[string]interface{}{
X: v.stageState.pos.X, "x": v.stageState.col.x,
Y: v.stageState.pos.Y, "y": v.stageState.col.y,
}, },
NewBullets: newBulletsToBroadcast, NewBullets: newBulletsToBroadcast,
ForcePlayerPos: v.stageState.updatePlayerPos, ForcePlayerPos: v.stageState.updatePlayerPos,

View file

@ -1,3 +1,5 @@
use crate::collision;
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Bullet { pub struct Bullet {
@ -5,18 +7,20 @@ pub struct Bullet {
pub spawn_time: i64, pub spawn_time: i64,
pub spawn_x: f64, pub spawn_x: f64,
pub spawn_y: f64, pub spawn_y: f64,
pub radius: f64,
pub parameters: [f64; 2], pub parameters: [f64; 2],
} }
impl Bullet { impl Bullet {
pub const BULLET_LINEAR: u8 = 0; 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 { Self {
class, class,
spawn_time, spawn_time,
spawn_x, spawn_x,
spawn_y, spawn_y,
radius,
parameters, parameters,
} }
} }
@ -43,4 +47,9 @@ impl Bullet {
|| y < -BULLET_KILL_BUFFER_WIDTH || y < -BULLET_KILL_BUFFER_WIDTH
|| y > STAGE_HEIGHT + 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))
}
} }

22
shared/src/collision.rs Normal file
View file

@ -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
}
}

View file

@ -1 +1,2 @@
pub mod bullet; pub mod bullet;
pub mod collision;