SportsBallEngine/Sources/PhysicsEngine/PhysicsWorld.swift
2025-12-15 16:03:37 -08:00

317 lines
10 KiB
Swift

/// PhysicsEngine - Sports-optimized physics simulation
/// NO PLATFORM-SPECIFIC IMPORTS ALLOWED IN THIS MODULE
import Foundation
// MARK: - Physics Types
/// A physics body in the simulation
public struct PhysicsBody: Sendable {
public var id: UUID
public var position: SIMD3<Float>
public var velocity: SIMD3<Float>
public var acceleration: SIMD3<Float>
public var mass: Float
public var restitution: Float // Bounciness (0-1)
public var friction: Float
public var isStatic: Bool
public init(id: UUID = UUID(), position: SIMD3<Float> = .zero, velocity: SIMD3<Float> = .zero,
mass: Float = 1.0, restitution: Float = 0.5, friction: Float = 0.5, isStatic: Bool = false) {
self.id = id
self.position = position
self.velocity = velocity
self.acceleration = .zero
self.mass = mass
self.restitution = restitution
self.friction = friction
self.isStatic = isStatic
}
}
/// Collision shapes
public enum CollisionShape: Sendable {
case sphere(radius: Float)
case box(halfExtents: SIMD3<Float>)
case capsule(radius: Float, height: Float)
case mesh(vertices: [SIMD3<Float>], indices: [UInt32])
}
/// Collision data
public struct Collision: Sendable {
public var bodyA: UUID
public var bodyB: UUID
public var contactPoint: SIMD3<Float>
public var normal: SIMD3<Float>
public var penetrationDepth: Float
public init(bodyA: UUID, bodyB: UUID, contactPoint: SIMD3<Float>,
normal: SIMD3<Float>, penetrationDepth: Float) {
self.bodyA = bodyA
self.bodyB = bodyB
self.contactPoint = contactPoint
self.normal = normal
self.penetrationDepth = penetrationDepth
}
}
// MARK: - Physics World
/// Main physics simulation world - optimized for sports game scenarios
public actor PhysicsWorld {
private var bodies: [UUID: PhysicsBody] = [:]
private var shapes: [UUID: CollisionShape] = [:]
private var gravity: SIMD3<Float> = SIMD3<Float>(0, -9.81, 0)
private var collisions: [Collision] = []
// Sports-specific optimizations
private var ballBodies: Set<UUID> = [] // Track ball/puck for special handling
private var playerBodies: Set<UUID> = [] // Track player bodies
public init() {}
// MARK: - Lifecycle
public func initialize() {
print(" ✓ Physics engine initialized (gravity: \(gravity))")
}
public func shutdown() {
bodies.removeAll()
shapes.removeAll()
collisions.removeAll()
}
// MARK: - Body Management
public func addBody(_ body: PhysicsBody, shape: CollisionShape, isBall: Bool = false, isPlayer: Bool = false) {
bodies[body.id] = body
shapes[body.id] = shape
if isBall {
ballBodies.insert(body.id)
}
if isPlayer {
playerBodies.insert(body.id)
}
}
public func removeBody(_ id: UUID) {
bodies.removeValue(forKey: id)
shapes.removeValue(forKey: id)
ballBodies.remove(id)
playerBodies.remove(id)
}
public func getBody(_ id: UUID) -> PhysicsBody? {
return bodies[id]
}
public func updateBody(_ id: UUID, transform: (inout PhysicsBody) -> Void) {
guard var body = bodies[id] else { return }
transform(&body)
bodies[id] = body
}
// MARK: - Simulation
/// Step the physics simulation forward by deltaTime
public func step(deltaTime: Float) {
// Apply forces
applyForces(deltaTime: deltaTime)
// Integrate velocities
integrateVelocities(deltaTime: deltaTime)
// Detect collisions
detectCollisions()
// Resolve collisions
resolveCollisions()
// Integrate positions
integratePositions(deltaTime: deltaTime)
// Apply sports-specific constraints (e.g., keep ball in bounds)
applySportsConstraints()
}
private func applyForces(deltaTime: Float) {
for (id, var body) in bodies {
guard !body.isStatic else { continue }
// Apply gravity
body.acceleration = gravity
// Apply custom forces (wind, drag, etc.)
// For sports games: apply spin to balls, player momentum, etc.
if ballBodies.contains(id) {
// Apply air drag to ball
let dragCoefficient: Float = 0.1
let drag = -body.velocity * dragCoefficient
body.acceleration += drag / body.mass
}
bodies[id] = body
}
}
private func integrateVelocities(deltaTime: Float) {
for (id, var body) in bodies {
guard !body.isStatic else { continue }
// Semi-implicit Euler integration
body.velocity += body.acceleration * deltaTime
bodies[id] = body
}
}
private func integratePositions(deltaTime: Float) {
for (id, var body) in bodies {
guard !body.isStatic else { continue }
body.position += body.velocity * deltaTime
bodies[id] = body
}
}
private func detectCollisions() {
collisions.removeAll()
let bodyArray = Array(bodies.values)
// Broad phase - simple N^2 check (would use spatial partitioning in production)
for i in 0..<bodyArray.count {
for j in (i+1)..<bodyArray.count {
let bodyA = bodyArray[i]
let bodyB = bodyArray[j]
// Skip if both are static
if bodyA.isStatic && bodyB.isStatic { continue }
// Narrow phase collision detection
if let collision = checkCollision(bodyA: bodyA, bodyB: bodyB) {
collisions.append(collision)
}
}
}
}
private func checkCollision(bodyA: PhysicsBody, bodyB: PhysicsBody) -> Collision? {
guard let shapeA = shapes[bodyA.id], let shapeB = shapes[bodyB.id] else {
return nil
}
// Simplified collision detection (sphere-sphere for now)
switch (shapeA, shapeB) {
case (.sphere(let radiusA), .sphere(let radiusB)):
return checkSphereSphere(bodyA: bodyA, radiusA: radiusA, bodyB: bodyB, radiusB: radiusB)
default:
return nil // Other collision shapes not implemented yet
}
}
private func checkSphereSphere(bodyA: PhysicsBody, radiusA: Float,
bodyB: PhysicsBody, radiusB: Float) -> Collision? {
let delta = bodyB.position - bodyA.position
let distanceSquared = simd_length_squared(delta)
let radiusSum = radiusA + radiusB
if distanceSquared < radiusSum * radiusSum {
let distance = sqrt(distanceSquared)
let normal = distance > 0.001 ? delta / distance : SIMD3<Float>(0, 1, 0)
let penetration = radiusSum - distance
let contactPoint = bodyA.position + normal * radiusA
return Collision(
bodyA: bodyA.id,
bodyB: bodyB.id,
contactPoint: contactPoint,
normal: normal,
penetrationDepth: penetration
)
}
return nil
}
private func resolveCollisions() {
for collision in collisions {
guard var bodyA = bodies[collision.bodyA],
var bodyB = bodies[collision.bodyB] else {
continue
}
// Position correction (separate bodies)
let correctionPercent: Float = 0.8
let correction = collision.normal * collision.penetrationDepth * correctionPercent
if !bodyA.isStatic && !bodyB.isStatic {
let totalMass = bodyA.mass + bodyB.mass
bodyA.position -= correction * (bodyB.mass / totalMass)
bodyB.position += correction * (bodyA.mass / totalMass)
} else if !bodyA.isStatic {
bodyA.position -= correction
} else if !bodyB.isStatic {
bodyB.position += correction
}
// Velocity resolution (impulse-based)
let relativeVelocity = bodyB.velocity - bodyA.velocity
let velocityAlongNormal = simd_dot(relativeVelocity, collision.normal)
// Don't resolve if bodies are separating
if velocityAlongNormal > 0 {
continue
}
// Calculate restitution (bounciness)
let restitution = min(bodyA.restitution, bodyB.restitution)
// Calculate impulse scalar
let j = -(1 + restitution) * velocityAlongNormal
let impulseScalar = bodyA.isStatic ? j / bodyB.mass :
bodyB.isStatic ? j / bodyA.mass :
j / (1/bodyA.mass + 1/bodyB.mass)
// Apply impulse
let impulse = collision.normal * impulseScalar
if !bodyA.isStatic {
bodyA.velocity -= impulse / bodyA.mass
}
if !bodyB.isStatic {
bodyB.velocity += impulse / bodyB.mass
}
bodies[collision.bodyA] = bodyA
bodies[collision.bodyB] = bodyB
}
}
private func applySportsConstraints() {
// Keep ball in play bounds, apply field friction, etc.
// This would contain sports-specific rules
}
// MARK: - Public API
public func setGravity(_ gravity: SIMD3<Float>) {
self.gravity = gravity
}
public func getGravity() -> SIMD3<Float> {
return gravity
}
public func getAllBodies() -> [PhysicsBody] {
return Array(bodies.values)
}
public func getCollisions() -> [Collision] {
return collisions
}
}