From a8e5e723b7e1891c9b352261a3ee4c3d3563e8cf Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sun, 26 Oct 2025 21:38:22 -0700 Subject: Checkpoint two --- .../kotlin/coffee/liz/ecs/physics/CollisionTick.kt | 95 ++++++++++++++++++---- .../kotlin/coffee/liz/ecs/physics/Components.kt | 25 +++++- .../coffee/liz/ecs/physics/IntegrationTick.kt | 14 ++-- .../kotlin/coffee/liz/ecs/physics/PhysicsSystem.kt | 25 ++++-- .../kotlin/coffee/liz/ecs/physics/PhysicsTick.kt | 10 +++ 5 files changed, 135 insertions(+), 34 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsTick.kt (limited to 'composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics') diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/CollisionTick.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/CollisionTick.kt index bc34ac1..d7d4dda 100644 --- a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/CollisionTick.kt +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/CollisionTick.kt @@ -1,28 +1,89 @@ package coffee.liz.ecs.physics +import coffee.liz.ecs.Entity import coffee.liz.ecs.Rect +import coffee.liz.ecs.Vec2 import coffee.liz.ecs.World +import kotlin.math.min internal class CollisionTick( - private val collisionResolver: CollisionResolver -) { - fun runTick(world: World) { - // Eh, fast enough for now. Don't need to do any fancy collision detection. There's always later to improve if - // it's that bad. - world.query(Collidable::class, Position::class).forEach { a -> - world.query(Collidable::class, Position::class).forEach { b -> - val aHitBoxes = a.get(Position::class).let { pos -> a.get(Collidable::class).hitboxes.map { - Rect(pos.vec2, it.dimensions) - }} - val bHitBoxes = b.get(Position::class).let { pos -> b.get(Collidable::class).hitboxes.map { - Rect(pos.vec2, it.dimensions) - }} - - val collisionDetected = aHitBoxes.any { a -> bHitBoxes.any { b -> a.overlaps(b) } } - if (collisionDetected) { - collisionResolver.resolveCollision(world, a, b) + private val collisionResolver: CollisionResolver, + private val subgridDimension: Vec2 = Vec2(50f, 50f) +) : PhysicsTick { + override fun runTick(world: World) { + val subgridEntities = world.collidingBounds().split(subgridDimension).map { subgrid -> + subgrid.map { cell -> + world.query(Colliding::class, Position::class).filter { entity -> + entity.positionalHitBoxes().any { cell.overlaps(it) } } } } + + for (y in subgridEntities.indices) { + for (x in subgridEntities[y].indices) { + resolveCollisions(world, subgridEntities[y][x]) + } + } + } + + private fun resolveCollisions(world: World, potentialCollisions: Collection) { + potentialCollisions.forEach { from -> + potentialCollisions.filter { to -> + val fromColliding = from.get(Colliding::class) + val toColliding = to.get(Colliding::class) + + val canCollide = fromColliding.group.collideInto.contains(toColliding.group.collisionGroup) + if (!canCollide) { + return@filter false + } + + val fromHitBoxes = from.positionalHitBoxes() + val toHitBoxes = to.positionalHitBoxes() + + fromHitBoxes.any { a -> toHitBoxes.any { b -> a.overlaps(b) } } + }.forEach { collision -> propagateCollision(world, from, collision)} + } + } + + private fun propagateCollision(world: World, from: Entity, to: Entity) { + collisionResolver.resolveCollision(world, from, to) + + val fromVelocity = from.takeIf { it.has(Velocity::class) }?.get(Velocity::class) ?: return + + val toVelocity = to.takeIf { it.has(Velocity::class) }?.get(Velocity::class) + ?: Velocity(Vec2(0f, 0f)) + val toColliding = to.get(Colliding::class) + + if (toColliding.propagateMovement) { + to.add(Velocity(toVelocity.velocity.plus(fromVelocity.velocity))) + } + } +} + +private fun Entity.positionalHitBoxes(): Collection { + val pos = get(Position::class) + val hitboxes = get(Colliding::class).hitboxes + return hitboxes.map { Rect(pos.position.plus(it.topLeft), it.dimensions) } +} + +fun World<*>.collidingBounds(): Rect { + return query(Position::class, Colliding::class).map { entity -> + val topLeft = entity.get(Position::class).position + val dimensions = entity.get(Colliding::class).hitboxes.merge().dimensions + Rect(topLeft, dimensions) + }.merge() +} + +fun Collection.merge(): Rect { + return reduce { a, b -> + val newTopLeft = Vec2( + min(a.topLeft.x, b.topLeft.x), + min(a.topLeft.y, b.topLeft.y) + ) + val dimensions = Vec2( + maxOf(a.topLeft.x + a.dimensions.x, b.topLeft.x + b.dimensions.x) - newTopLeft.x, + maxOf(a.topLeft.y + a.dimensions.y, b.topLeft.y + b.dimensions.y) - newTopLeft.y + ) + Rect(newTopLeft, dimensions) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/Components.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/Components.kt index ffae10b..0b4818d 100644 --- a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/Components.kt +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/Components.kt @@ -4,12 +4,29 @@ import coffee.liz.ecs.Component import coffee.liz.ecs.Rect import coffee.liz.ecs.Vec2 -data class Position(val vec2: Vec2): Component -data class Velocity(val vec2: Vec2) : Component -data class Acceleration(val vec2: Vec2) : Component +data class Position( + val position: Vec2, +) : Component + +data class Velocity( + val velocity: Vec2, +) : Component + +data class Acceleration( + val acceleration: Vec2, +) : Component + +interface CollisionGroup { + val collisionGroup: TGroup + val collideInto: Set +} /** * @param hitboxes a collection of hitboxes to check collisions against relative to [Rect.topLeft] as the top left of * [Position]. */ -data class Collidable(val hitboxes: Collection): Component +data class Colliding>( + val hitboxes: Collection, + val group: CollisionGroup, + val propagateMovement: Boolean = true, +) : Component diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/IntegrationTick.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/IntegrationTick.kt index 03af835..c3484ae 100644 --- a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/IntegrationTick.kt +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/IntegrationTick.kt @@ -2,15 +2,15 @@ package coffee.liz.ecs.physics import coffee.liz.ecs.World -internal class IntegrationTick { - fun runTick(world: World) { +internal class IntegrationTick : PhysicsTick { + override fun runTick(world: World) { world.query(Velocity::class, Acceleration::class).forEach { entity -> val velocity = entity.get(Velocity::class) val acceleration = entity.get(Acceleration::class) entity.add( Velocity( - vec2 = velocity.vec2.plus(acceleration.vec2) - ) + vec2 = velocity.vec2.plus(acceleration.vec2), + ), ) } @@ -19,9 +19,9 @@ internal class IntegrationTick { val velocity = entity.get(Acceleration::class) entity.add( Position( - vec2 = position.vec2.plus(velocity.vec2) - ) + vec2 = position.vec2.plus(velocity.vec2), + ), ) } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsSystem.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsSystem.kt index 703e614..ec9c71b 100644 --- a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsSystem.kt +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsSystem.kt @@ -6,21 +6,34 @@ import coffee.liz.ecs.World import kotlin.time.Duration internal fun interface CollisionResolver { - fun resolveCollision(world: World, a: Entity, b: Entity) + fun resolveCollision( + world: World, + a: Entity, + b: Entity, + ) } abstract class PhysicsSystem( - physicsTickRate: Duration -): TickedSystem(physicsTickRate), CollisionResolver { + physicsTickRate: Duration, +) : TickedSystem(physicsTickRate), + CollisionResolver { private val integrationTick = IntegrationTick() private val collisionTick = CollisionTick(this) - abstract override fun resolveCollision(world: World, a: Entity, b: Entity) + abstract override fun resolveCollision( + world: World, + a: Entity, + b: Entity, + ) - override fun update(world: World, state: Outside, ticks: Int) { + override fun update( + world: World, + state: Outside, + ticks: Int, + ) { for (tick in 1..ticks) { collisionTick.runTick(world) integrationTick.runTick(world) } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsTick.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsTick.kt new file mode 100644 index 0000000..79a95c0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsTick.kt @@ -0,0 +1,10 @@ +package coffee.liz.ecs.physics + +import coffee.liz.ecs.World + +/** + * Runs a physics tick in [World]. + */ +fun interface PhysicsTick { + fun runTick(world: World) +} -- cgit v1.2.3-70-g09d2