317 lines
10 KiB
Swift
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
|
|
}
|
|
}
|
|
|