diff options
| author | Elizabeth Hunt <me@liz.coffee> | 2025-10-26 21:38:22 -0700 |
|---|---|---|
| committer | Elizabeth Hunt <me@liz.coffee> | 2025-10-26 21:39:58 -0700 |
| commit | a8e5e723b7e1891c9b352261a3ee4c3d3563e8cf (patch) | |
| tree | 853df79c877d37d7e5d25f52b301aedcc3d5db55 /composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics | |
| parent | 395aa7d1c312e495517701be11c21425d9a5838e (diff) | |
| download | abstraction-engine-kt-a8e5e723b7e1891c9b352261a3ee4c3d3563e8cf.tar.gz abstraction-engine-kt-a8e5e723b7e1891c9b352261a3ee4c3d3563e8cf.zip | |
Diffstat (limited to 'composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics')
5 files changed, 135 insertions, 34 deletions
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<Outside>( - private val collisionResolver: CollisionResolver<Outside> -) { - fun runTick(world: World<Outside>) { - // 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<Outside>, + private val subgridDimension: Vec2 = Vec2(50f, 50f) +) : PhysicsTick<Outside> { + override fun runTick(world: World<Outside>) { + 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<Outside>, potentialCollisions: Collection<Entity>) { + 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<Outside>, 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<Rect> { + 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<Rect>.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<TGroup> { + val collisionGroup: TGroup + val collideInto: Set<TGroup> +} /** * @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<Rect>): Component +data class Colliding<T : Enum<*>>( + val hitboxes: Collection<Rect>, + val group: CollisionGroup<T>, + 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<Outside> { - fun runTick(world: World<Outside>) { +internal class IntegrationTick<Outside> : PhysicsTick<Outside> { + override fun runTick(world: World<Outside>) { 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<Outside> { 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<Outside> { - fun resolveCollision(world: World<Outside>, a: Entity, b: Entity) + fun resolveCollision( + world: World<Outside>, + a: Entity, + b: Entity, + ) } abstract class PhysicsSystem<Outside>( - physicsTickRate: Duration -): TickedSystem<Outside>(physicsTickRate), CollisionResolver<Outside> { + physicsTickRate: Duration, +) : TickedSystem<Outside>(physicsTickRate), + CollisionResolver<Outside> { private val integrationTick = IntegrationTick<Outside>() private val collisionTick = CollisionTick(this) - abstract override fun resolveCollision(world: World<Outside>, a: Entity, b: Entity) + abstract override fun resolveCollision( + world: World<Outside>, + a: Entity, + b: Entity, + ) - override fun update(world: World<Outside>, state: Outside, ticks: Int) { + override fun update( + world: World<Outside>, + 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<Outside> { + fun runTick(world: World<Outside>) +} |
