-
Notifications
You must be signed in to change notification settings - Fork 2
Description
π Is your feature request related to a specific issue?
The current battle box system only supports axis-aligned rectangles for collision. Complex battle scenarios require non-rectangular collision regions (e.g., circular arenas, L-shaped areas, boxes with holes).
Background: souprune already has a full SDF rendering pipeline (bevy_alight_motion/src/sdf.rs, sdf_material.rs, sdf_view_shape.rs) but the collision system only uses axis-aligned rectangles (BattleBoxBoundary with half_size: Vec2). This creates an inconsistency: rendering can show arbitrary SDF shapes, but collision is limited to rectangles.
π‘ Feature Description
SDF composite shape enum
Use SDF composition operations to build complex collision shapes. This replaces the polygon CSG approach used in the predecessor project UCT, where BoxService.FindIntersections + ProcessPolygons + GetUnion/GetDifference required ~400 lines of polygon math. The SDF approach achieves the same results in ~50 lines:
/// SDF composite battle box shape
#[derive(Component, Debug, Clone, Deserialize, Serialize)]
pub enum BattleBoxShape {
/// Rectangle (current default)
Rect { half_size: Vec2 },
/// Circle
Circle { radius: f32 },
/// Rounded rectangle
RoundedRect { half_size: Vec2, radius: f32 },
/// Union of two shapes: SDF = min(a, b)
Union(Box<BattleBoxShape>, Box<BattleBoxShape>),
/// Subtraction: A minus B: SDF = max(a, -b)
Subtract(Box<BattleBoxShape>, Box<BattleBoxShape>),
/// Smooth union: SDF = smooth_min(a, b, k)
SmoothUnion {
a: Box<BattleBoxShape>,
b: Box<BattleBoxShape>,
smoothness: f32,
},
}
impl BattleBoxShape {
pub fn sdf_distance(&self, point: Vec2) -> f32 {
match self {
Self::Rect { half_size } => {
// Inigo Quilez box SDF (already exists in systems.rs:184-195)
let d = point.abs() - *half_size;
d.max(Vec2::ZERO).length() + d.x.max(d.y).min(0.0)
}
Self::Circle { radius } => point.length() - radius,
Self::RoundedRect { half_size, radius } => {
let d = point.abs() - *half_size + Vec2::splat(*radius);
d.max(Vec2::ZERO).length() + d.x.max(d.y).min(0.0) - radius
}
Self::Union(a, b) => {
a.sdf_distance(point).min(b.sdf_distance(point))
}
Self::Subtract(a, b) => {
a.sdf_distance(point).max(-b.sdf_distance(point))
}
Self::SmoothUnion { a, b, smoothness: k } => {
let da = a.sdf_distance(point);
let db = b.sdf_distance(point);
let h = (0.5 + 0.5 * (db - da) / k).clamp(0.0, 1.0);
db * (1.0 - h) + da * h - k * h * (1.0 - h)
}
}
}
}Collision constraint via SDF gradient
For non-rectangular shapes, the current axis-aligned separation (BattleBoxBoundary::constrain_with_collider()) won't work. Instead, use the SDF gradient to find the surface normal:
impl BattleBoxShape {
/// Numerical SDF gradient (surface normal direction)
pub fn gradient(&self, point: Vec2) -> Vec2 {
const EPS: f32 = 0.01;
let dx = self.sdf_distance(point + Vec2::X * EPS)
- self.sdf_distance(point - Vec2::X * EPS);
let dy = self.sdf_distance(point + Vec2::Y * EPS)
- self.sdf_distance(point - Vec2::Y * EPS);
Vec2::new(dx, dy).normalize_or_zero()
}
/// Constrain a point to stay inside the shape with a given margin
pub fn constrain_point(&self, point: Vec2, margin: f32) -> Vec2 {
let dist = self.sdf_distance(point);
if dist + margin < 0.0 {
return point; // Safely inside
}
let normal = self.gradient(point);
point - normal * (dist + margin)
}
}RON data-driven configuration
// A rounded rectangle battle box
(
shape: RoundedRect(
half_size: (283.0, 65.0),
radius: 8.0,
),
)
// An L-shaped battle box (rect minus rect)
(
shape: Subtract(
Rect(half_size: (200.0, 100.0)),
Rect(half_size: (100.0, 50.0)),
),
)
// Two circles smoothly merged
(
shape: SmoothUnion(
a: Circle(radius: 80.0),
b: Circle(radius: 60.0),
smoothness: 20.0,
),
)Shared SDF for rendering and collision
The same BattleBoxShape should drive both:
- Collision: CPU-side
sdf_distance()+constrain_point() - Rendering: Generate matching shader parameters for the SDF material pipeline
This eliminates the inconsistency where rendering shows one shape but collision uses another.
β Alternatives Considered
-
UCT's BΓ©zier curve deformation: The predecessor project UCT uses n-degree Bernstein polynomials in
BoxDrawer.cs(760 lines) to deform battle box edges into smooth curves. This requires: vertex generation β triangulation (via LibTessDotNet) β edge collider β polygon collision testing. The SDF approach is significantly simpler and more performant:Aspect UCT Polygon CSG souprune SDF Composition Code size ~400 lines (BoxService) ~50 lines Collision Triangulation + point-in-polygon Direct distance function Render consistency Separate data paths Same SDF for both Composition ops FindIntersections + ProcessPolygons min(a,b)/max(a,-b)GPU compatibility Must transfer polygon vertices SDF computed natively in shader Shape extensibility Requires rewriting triangulation Add a new SDF primitive -
Polygon colliders (general purpose): More universally applicable but far more complex to implement, and inconsistent with souprune's existing SDF rendering architecture.
π Additional Information
Depends on: Issue #84 (multi battle box β BattleBoxBoundary needs to be extended/replaced by BattleBoxShape)
Files to modify:
crates/souprune/src/core/collision/battle_collision.rsβ AddBattleBoxShapeenum, gradient, and constrain methodscrates/souprune/src/core/collision/systems.rsβ Existingsdf_box()(lines 184-195) can be reused as theRectvariantcrates/souprune/src/app_state/battle/collision.rsβ Updateconstrain_player_to_battle_box_systemto useBattleBoxShape- SDF shader files β Optional: sync rendering side with new shapes
Existing SDF infrastructure in souprune:
bevy_alight_motion/src/sdf.rs: SDF rendering module, BASE_HALF_EXTENT=50.0bevy_alight_motion/src/sdf_material.rs: SDF Material + Shader systemcrates/souprune/src/core/view/sdf_shape.rs: View system SDF integrationcrates/souprune/src/core/collision/systems.rs:184-195: Existingsdf_box()implementation (Inigo Quilez formula)