summaryrefslogtreecommitdiff
path: root/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-10-26 21:38:22 -0700
committerElizabeth Hunt <me@liz.coffee>2025-10-26 21:39:58 -0700
commita8e5e723b7e1891c9b352261a3ee4c3d3563e8cf (patch)
tree853df79c877d37d7e5d25f52b301aedcc3d5db55 /composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics
parent395aa7d1c312e495517701be11c21425d9a5838e (diff)
downloadabstraction-engine-kt-a8e5e723b7e1891c9b352261a3ee4c3d3563e8cf.tar.gz
abstraction-engine-kt-a8e5e723b7e1891c9b352261a3ee4c3d3563e8cf.zip
Checkpoint twoHEADmain
Diffstat (limited to 'composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics')
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/CollisionTick.kt95
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/Components.kt25
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/IntegrationTick.kt14
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsSystem.kt25
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsTick.kt10
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>)
+}