physics: reduce memory allocations a lot

This should improve performance of physics when exploding millions of tnt. Memory is being reused with the temporary allocator. Shapes are extracted initially, which is still bad, because most of them are empty.
This commit is contained in:
Moritz Zwerger 2025-04-11 14:01:28 +02:00
parent 2340fc1a3d
commit 728ca1ce2f
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
14 changed files with 249 additions and 176 deletions

View File

@ -82,4 +82,32 @@ class CollisionIT {
player.assertPosition(0.0, 1.5, 0.0)
player.assertVelocity(0.0, -0.0784000015258789, 0.0)
}
fun `not crashing when already exceeding negative y`() {
val player = createPlayer(createSession(2))
player.forceTeleport(Vec3d(0.0, -3000, 0.0))
player.physics.velocity = Vec3d(0.0, -150.0, 0.0)
player.runTicks(10)
}
fun `not crashing when exceeding negative y`() {
val player = createPlayer(createSession(2))
player.forceTeleport(Vec3d(0.0, -2040, 0.0))
player.physics.velocity = Vec3d(0.0, -150.0, 0.0)
player.runTicks(10)
}
fun `not crashing when already exceeding positive y`() {
val player = createPlayer(createSession(2))
player.forceTeleport(Vec3d(0.0, 3000, 0.0))
player.physics.velocity = Vec3d(0.0, 150.0, 0.0)
player.runTicks(10)
}
fun `not crashing when exceeding positive y`() {
val player = createPlayer(createSession(2))
player.forceTeleport(Vec3d(0.0, 2040, 0.0))
player.physics.velocity = Vec3d(0.0, 150.0, 0.0)
player.runTicks(10)
}
}

View File

@ -17,7 +17,6 @@ import de.bixilon.kotlinglm.func.common.clamp
import de.bixilon.kotlinglm.vec3.Vec3
import de.bixilon.kotlinglm.vec3.Vec3d
import de.bixilon.kotlinglm.vec3.Vec3i
import de.bixilon.kotlinglm.vec3.Vec3t
import de.bixilon.kutil.math.simple.DoubleMath.ceil
import de.bixilon.kutil.math.simple.DoubleMath.floor
import de.bixilon.minosoft.data.Axes
@ -59,12 +58,13 @@ class AABB {
this.max = max
}
fun intersect(other: AABB): Boolean {
fun intersects(other: AABB): Boolean {
return (min.x < other.max.x && max.x > other.min.x) && (min.y < other.max.y && max.y > other.min.y) && (min.z < other.max.z && max.z > other.min.z)
}
operator fun plus(other: Vec3t<out Number>): AABB = offset(other)
fun offset(other: Vec3t<out Number>) = AABB(true, min + other, max + other)
fun intersects(other: AABB, offset: BlockPosition): Boolean {
return (min.x < (other.max.x + offset.x) && max.x > (other.min.x + offset.x)) && (min.y < (other.max.y + offset.y) && max.y > (other.min.y + offset.y)) && (min.z < (other.max.z + offset.z) && max.z > (other.min.z + offset.z))
}
operator fun plus(other: Vec3d): AABB = offset(other)
fun offset(other: Vec3d) = AABB(true, min + other, max + other)
@ -128,15 +128,10 @@ class AABB {
return AABB(true, newMin, newMax)
}
fun extend(vec3i: Vec3i): AABB {
return this.extend(Vec3d(vec3i))
}
fun extend(direction: Directions): AABB {
return this.extend(direction.vectord)
}
fun grow(size: Double = 1.0E-7): AABB {
return AABB(min - size, max + size)
}
@ -149,37 +144,38 @@ class AABB {
return this > min && this < max
}
private fun intersects(axis: Axes, other: AABB): Boolean {
private fun intersects(axis: Axes, other: AABB, offset: BlockPosition): Boolean {
val min = min[axis]
val max = max[axis]
val otherMin = other.min[axis]
val otherMax = other.max[axis]
val otherMin = other.min[axis] + offset[axis]
val otherMax = other.max[axis] + offset[axis]
return min.isIn(otherMin, otherMax)
|| max.isIn(otherMin, otherMax)
|| otherMin.isIn(min, max)
|| otherMax.isIn(min, max)
|| (min == otherMin && max == otherMax)
|| max.isIn(otherMin, otherMax)
|| otherMin.isIn(min, max)
|| otherMax.isIn(min, max)
|| (min == otherMin && max == otherMax)
}
fun calculateMaxOffset(other: AABB, offset: Double, axis: Axes): Double {
if (!intersects(axis.next(), other) || !intersects(axis.previous(), other)) {
return offset
fun calculateMaxDistance(other: AABB, maxDistance: Double, axis: Axes) = calculateMaxDistance(other, BlockPosition(), maxDistance, axis)
fun calculateMaxDistance(other: AABB, offset: BlockPosition, maxDistance: Double, axis: Axes): Double {
if (!intersects(axis.next(), other, offset) || !intersects(axis.previous(), other, offset)) {
return maxDistance
}
val min = min[axis]
val max = max[axis]
val otherMin = other.min[axis]
val otherMax = other.max[axis]
val otherMin = other.min[axis] + offset[axis]
val otherMax = other.max[axis] + offset[axis]
if (offset > 0 && otherMax <= min && otherMax + offset > min) {
return (min - otherMax).clamp(0.0, offset)
if (maxDistance > 0 && otherMax <= min && otherMax + maxDistance > min) {
return (min - otherMax).clamp(0.0, maxDistance)
}
if (offset < 0 && max <= otherMin && otherMin + offset < max) {
return (max - otherMin).clamp(offset, 0.0)
if (maxDistance < 0 && max <= otherMin && otherMin + maxDistance < max) {
return (max - otherMin).clamp(maxDistance, 0.0)
}
return offset
return maxDistance
}
@Deprecated("mutable")
@ -188,12 +184,10 @@ class AABB {
max.array[axis.ordinal] += value
}
fun offset(axis: Axes, offset: Double): AABB {
return when (axis) {
Axes.X -> this + Vec3d(-offset, 0.0, 0.0)
Axes.Y -> this + Vec3d(0.0, -offset, 0.0)
Axes.Z -> this + Vec3d(0.0, 0.0, -offset)
}
fun offset(axis: Axes, offset: Double) = when (axis) {
Axes.X -> this + Vec3d(-offset, 0.0, 0.0)
Axes.Y -> this + Vec3d(0.0, -offset, 0.0)
Axes.Z -> this + Vec3d(0.0, 0.0, -offset)
}
/**
@ -296,19 +290,19 @@ class AABB {
fun checkSide(x: Double): Boolean {
return (this.min == Vec3d(x, min.y, min.z) && this.max == Vec3d(x, max.y, min.z))
|| (this.min == Vec3d(x, min.y, min.z) && this.max == Vec3d(x, min.y, max.z))
|| (this.min == Vec3d(x, max.y, min.z) && this.max == Vec3d(x, max.y, max.z))
|| (this.min == Vec3d(x, min.y, max.z) && this.max == Vec3d(x, max.y, max.z))
|| (this.min == Vec3d(x, min.y, min.z) && this.max == Vec3d(x, min.y, max.z))
|| (this.min == Vec3d(x, max.y, min.z) && this.max == Vec3d(x, max.y, max.z))
|| (this.min == Vec3d(x, min.y, max.z) && this.max == Vec3d(x, max.y, max.z))
}
return checkSide(min.x) // left quad
|| checkSide(max.x) // right quad
// connections between 2 quads
|| (this.min == min && this.max == Vec3d(max.x, min.y, min.z))
|| (this.min == Vec3d(min.x, max.y, min.z) && this.max == Vec3d(max.x, max.y, min.z))
|| (this.min == Vec3d(min.x, max.y, max.z) && this.max == max)
|| (this.min == Vec3d(min.x, min.y, max.z) && this.max == Vec3d(max.x, min.y, max.z))
|| checkSide(max.x) // right quad
// connections between 2 quads
|| (this.min == min && this.max == Vec3d(max.x, min.y, min.z))
|| (this.min == Vec3d(min.x, max.y, min.z) && this.max == Vec3d(max.x, max.y, min.z))
|| (this.min == Vec3d(min.x, max.y, max.z) && this.max == max)
|| (this.min == Vec3d(min.x, min.y, max.z) && this.max == Vec3d(max.x, min.y, max.z))
}
override fun toString(): String {

View File

@ -0,0 +1,125 @@
/*
* Minosoft
* Copyright (C) 2020-2025 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.registries.shapes.collision
import de.bixilon.kotlinglm.vec3.Vec3d
import de.bixilon.kutil.array.ArrayUtil.cast
import de.bixilon.kutil.cast.CastUtil.unsafeCast
import de.bixilon.minosoft.data.Axes
import de.bixilon.minosoft.data.registries.blocks.shapes.collision.CollisionPredicate
import de.bixilon.minosoft.data.registries.blocks.shapes.collision.context.CollisionContext
import de.bixilon.minosoft.data.registries.blocks.types.entity.BlockWithEntity
import de.bixilon.minosoft.data.registries.blocks.types.properties.shape.collision.CollidableBlock
import de.bixilon.minosoft.data.registries.blocks.types.properties.shape.collision.fixed.FixedCollidable
import de.bixilon.minosoft.data.registries.shapes.aabb.AABB
import de.bixilon.minosoft.data.registries.shapes.voxel.AbstractVoxelShape
import de.bixilon.minosoft.data.world.World
import de.bixilon.minosoft.data.world.chunk.chunk.Chunk
import de.bixilon.minosoft.data.world.iterator.WorldIterator
import de.bixilon.minosoft.data.world.positions.BlockPosition
import de.bixilon.minosoft.gui.rendering.util.allocator.LongAllocator
import de.bixilon.minosoft.gui.rendering.util.allocator.TemporaryAllocator
class CollisionShape(
val world: World,
context: CollisionContext,
aabb: AABB,
movement: Vec3d,
chunk: Chunk?,
predicate: CollisionPredicate? = null,
) : AbstractVoxelShape {
override val aabbs: Int
private var total: Int
private val positions: LongArray
private val shapes: Array<AbstractVoxelShape>
init {
val aabbs = aabb.extend(movement).grow(1.0).positions()
val positions = POSITIONS.allocate(aabbs.size)
val shapes: Array<AbstractVoxelShape?> = SHAPES.allocate(aabbs.size)
var index = 0
var total = 0
// TODO: add entity collisions (boat, shulker)
// TODO: add world border collision shape
for ((position, state, chunk) in WorldIterator(aabbs, world, chunk)) {
if (state.block !is CollidableBlock) continue
if (predicate != null && !predicate.invoke(state)) continue
// TODO: filter blocks (e.g. moving piston), whatever that means
val shape = when (state.block) {
is FixedCollidable -> state.block.getCollisionShape(state)
is BlockWithEntity<*> -> state.block.getCollisionShape(world.session, context, position, state, chunk.getBlockEntity(position.inChunkPosition))
else -> state.block.getCollisionShape(world.session, context, position, state, null)
} ?: continue
if (position in aabb && shape.intersects(aabb, -position)) {
continue
}
positions[index] = position.raw
shapes[index] = shape
index++
total += shape.aabbs
}
this.shapes = shapes.cast()
this.total = index
this.positions = positions
this.aabbs = total
}
override fun iterator(): Iterator<AABB> {
TODO("Not yet implemented")
}
override fun intersects(other: AABB): Boolean {
for (index in 0 until total) {
val position = BlockPosition(this.positions[index])
val shape = this.shapes[index]
if (shape.intersects(other, -position)) return true
}
return false
}
override fun calculateMaxDistance(other: AABB, maxDistance: Double, axis: Axes): Double {
var distance = maxDistance
for (index in 0 until total) {
val position = BlockPosition(this.positions[index])
val shape = this.shapes[index]
distance = shape.calculateMaxDistance(other, -position, distance, axis)
}
return distance
}
fun free() {
POSITIONS.free(this.positions)
SHAPES.free(this.shapes.unsafeCast())
}
companion object {
private val POSITIONS = LongAllocator()
private val SHAPES = object : TemporaryAllocator<Array<AbstractVoxelShape?>>() {
override fun getSize(value: Array<AbstractVoxelShape?>) = value.size
override fun create(size: Int): Array<AbstractVoxelShape?> = arrayOfNulls(size)
}
}
}

View File

@ -16,7 +16,6 @@ package de.bixilon.minosoft.data.registries.shapes.voxel
import de.bixilon.kotlinglm.vec3.Vec3
import de.bixilon.kotlinglm.vec3.Vec3d
import de.bixilon.kotlinglm.vec3.Vec3i
import de.bixilon.kotlinglm.vec3.Vec3t
import de.bixilon.kutil.primitive.IntUtil.toInt
import de.bixilon.minosoft.data.Axes
import de.bixilon.minosoft.data.registries.shapes.ShapeRegistry
@ -30,14 +29,20 @@ import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3dUtil.max
import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3dUtil.min
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
abstract class AbstractVoxelShape : Iterable<AABB> {
abstract val aabbs: Int
interface AbstractVoxelShape : Iterable<AABB> {
val aabbs: Int
fun intersect(other: AABB): Boolean {
fun intersects(other: AABB): Boolean {
for (aabb in this) {
if (!aabb.intersect(other)) {
continue
}
if (!aabb.intersects(other)) continue
return true
}
return false
}
fun intersects(other: AABB, offset: BlockPosition): Boolean {
for (aabb in this) {
if (!aabb.intersects(other, offset)) continue
return true
}
return false
@ -51,18 +56,14 @@ abstract class AbstractVoxelShape : Iterable<AABB> {
return VoxelShape(result)
}
operator fun plus(offset: Vec3t<out Number>) = modify { it + offset }
operator fun plus(offset: Vec3d) = modify { it + offset }
operator fun plus(offset: Vec3) = modify { it + offset }
operator fun plus(offset: Vec3i) = modify { it + offset }
@JvmName("plusBlockPosition")
operator fun plus(offset: BlockPosition) = modify { it + offset }
@JvmName("plusInChunkPosition")
operator fun plus(offset: InChunkPosition) = modify { it + offset }
@JvmName("plusInSectionPosition")
operator fun plus(offset: InSectionPosition) = modify { it + offset }
fun add(other: AbstractVoxelShape): AbstractVoxelShape {
@ -75,7 +76,15 @@ abstract class AbstractVoxelShape : Iterable<AABB> {
fun calculateMaxDistance(other: AABB, maxDistance: Double, axis: Axes): Double {
var distance = maxDistance
for (aabb in this) {
distance = aabb.calculateMaxOffset(other, distance, axis)
distance = aabb.calculateMaxDistance(other, distance, axis)
}
return distance
}
fun calculateMaxDistance(other: AABB, offset: BlockPosition, maxDistance: Double, axis: Axes): Double {
var distance = maxDistance
for (aabb in this) {
distance = aabb.calculateMaxDistance(other, offset, distance, axis)
}
return distance
}
@ -110,7 +119,6 @@ abstract class AbstractVoxelShape : Iterable<AABB> {
return shouldDrawLine(start.toVec3d, end.toVec3d)
}
fun getMax(axis: Axes): Double {
if (aabbs == 0) return Double.NaN

View File

@ -1,57 +0,0 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.registries.shapes.voxel
import de.bixilon.minosoft.data.registries.shapes.aabb.AABB
class MutableVoxelShape(
val aabb: MutableSet<AABB>,
) : AbstractVoxelShape() {
override val aabbs: Int = aabb.size
constructor(vararg aabbs: AABB) : this(aabbs.toMutableSet())
constructor(shape: AbstractVoxelShape) : this(shape.toMutableSet())
operator fun plusAssign(aabb: AABB) {
this.aabb += aabb
}
operator fun plusAssign(shape: AbstractVoxelShape) {
this.aabb += shape
}
override fun iterator(): Iterator<AABB> {
return aabb.iterator()
}
override fun toString(): String {
if (aabbs == 0) {
return "VoxelShape{EMPTY}"
}
return "VoxelShape{$aabb}"
}
override fun hashCode(): Int {
return aabb.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null && aabbs == 0) return true
if (other !is AbstractVoxelShape) return false
if (other is VoxelShape) return aabb == other.aabb
if (other is MutableVoxelShape) return aabb == other.aabb
TODO("Can not compare $this with $other")
}
}

View File

@ -1,6 +1,6 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
* Copyright (C) 2020-2025 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
@ -19,7 +19,7 @@ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
class VoxelShape(
val aabb: Collection<AABB>,
) : AbstractVoxelShape() {
) : AbstractVoxelShape {
override val aabbs: Int = aabb.size
constructor(vararg aabbs: AABB) : this(ObjectOpenHashSet(aabbs))
@ -47,7 +47,6 @@ class VoxelShape(
if (other == null && aabbs == 0) return true
if (other !is AbstractVoxelShape) return false
if (other is VoxelShape) return aabb == other.aabb
if (other is MutableVoxelShape) return aabb == other.aabb
TODO("Can not compare $this with $other")
}
}

View File

@ -177,7 +177,7 @@ class WorldEntities : Iterable<Entity> {
}
val aabb = entity.physics.aabb
if (shape.intersect(aabb)) {
if (shape.intersects(aabb)) {
return true
}
}

View File

@ -60,6 +60,7 @@ class WorldIterator(
if (this.chunk !== chunk) {
this.chunk = chunk
}
// TODO: some fast skip? (if section is empty, can not be in section or chunk is null)
val state = chunk[position.inChunkPosition] ?: continue
@ -116,7 +117,7 @@ class WorldIterator(
if (predicate != null && !predicate.invoke(state)) continue
val shape = state.block.getCollisionShape(world.session, context, position, state, null) ?: continue
if ((shape + position).intersect(aabb)) {
if ((shape + position).intersects(aabb)) {
return true
}
}

View File

@ -13,6 +13,7 @@
package de.bixilon.minosoft.data.world.positions
import de.bixilon.minosoft.data.Axes
import de.bixilon.minosoft.data.direction.Directions
import de.bixilon.minosoft.data.text.formatting.TextFormattable
import de.bixilon.minosoft.data.world.positions.BlockPositionUtil.assertPosition
@ -102,6 +103,12 @@ value class BlockPosition(
return modifyZ(-Z * 1)
}
operator fun get(axis: Axes) = when (axis) {
Axes.X -> x
Axes.Y -> y
Axes.Z -> z
}
inline fun with(x: Int = this.x, y: Int = this.y, z: Int = this.z) = BlockPosition(x, y, z)
inline operator fun plus(value: Int) = BlockPosition(this.x + value, this.y + value, this.z + value)

View File

@ -44,7 +44,7 @@ class WallOverlay(context: RenderContext) : SimpleOverlay(context) {
}
val camera = context.session.camera.entity
val shape = blockState.block.getCollisionShape(context.session, EntityCollisionContext(camera), position, blockState, null) ?: return false // TODO: block entity
if (!shape.intersect(player.physics.aabb)) {
if (!shape.intersects(player.physics.aabb)) {
return false
}
return true

View File

@ -20,6 +20,7 @@ import de.bixilon.minosoft.data.physics.PhysicsEntity
import de.bixilon.minosoft.data.registries.blocks.shapes.collision.context.ParticleCollisionContext
import de.bixilon.minosoft.data.registries.particle.data.ParticleData
import de.bixilon.minosoft.data.registries.shapes.aabb.AABB
import de.bixilon.minosoft.data.registries.shapes.collision.CollisionShape
import de.bixilon.minosoft.data.world.chunk.chunk.Chunk
import de.bixilon.minosoft.gui.rendering.particle.ParticleFactory
import de.bixilon.minosoft.gui.rendering.particle.ParticleMesh
@ -28,7 +29,6 @@ import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3Util.EMPTY
import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3dUtil.EMPTY
import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3dUtil.blockPosition
import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3dUtil.interpolateLinear
import de.bixilon.minosoft.physics.parts.CollisionMovementPhysics.collectCollisions
import de.bixilon.minosoft.physics.parts.CollisionMovementPhysics.collide
import de.bixilon.minosoft.protocol.network.session.play.PlaySession
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
@ -138,7 +138,7 @@ abstract class Particle(
private fun collide(movement: Vec3d): Vec3d {
val aabb = aabb + movement
val context = ParticleCollisionContext(this)
val collisions = session.world.collectCollisions(context, movement, aabb, getChunk())
val collisions = CollisionShape(session.world, context, aabb, movement, getChunk())
val adjusted = collide(movement, aabb, collisions)
if (adjusted.y != movement.y) {
onGround = true

View File

@ -108,7 +108,9 @@ class LocalPlayerPhysics(entity: LocalPlayerEntity) : PlayerPhysics<LocalPlayerE
val aabb = aabb
val offset = AABB(position.x + 0.0, aabb.min.y, position.z + 0.0, position.x + 1.0, aabb.max.y, position.z + 1.0).shrink(1.0E-7)
val collisions = collectCollisions(Vec3d.EMPTY, offset, predicate = predicate)
return collisions.intersect(aabb)
val intersects = collisions.intersects(aabb)
collisions.free()
return intersects
}
private fun shouldSprint(): Boolean {

View File

@ -17,59 +17,19 @@ import de.bixilon.kotlinglm.vec3.Vec3d
import de.bixilon.kotlinglm.vec3.swizzle.xz
import de.bixilon.minosoft.data.Axes
import de.bixilon.minosoft.data.registries.blocks.shapes.collision.CollisionPredicate
import de.bixilon.minosoft.data.registries.blocks.shapes.collision.context.CollisionContext
import de.bixilon.minosoft.data.registries.blocks.shapes.collision.context.EntityCollisionContext
import de.bixilon.minosoft.data.registries.blocks.types.entity.BlockWithEntity
import de.bixilon.minosoft.data.registries.blocks.types.properties.shape.collision.CollidableBlock
import de.bixilon.minosoft.data.registries.blocks.types.properties.shape.collision.fixed.FixedCollidable
import de.bixilon.minosoft.data.registries.shapes.aabb.AABB
import de.bixilon.minosoft.data.registries.shapes.collision.CollisionShape
import de.bixilon.minosoft.data.registries.shapes.voxel.AbstractVoxelShape
import de.bixilon.minosoft.data.registries.shapes.voxel.VoxelShape
import de.bixilon.minosoft.data.world.World
import de.bixilon.minosoft.data.world.chunk.chunk.Chunk
import de.bixilon.minosoft.data.world.iterator.WorldIterator
import de.bixilon.minosoft.data.world.positions.BlockPosition
import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3dUtil.EMPTY
import de.bixilon.minosoft.physics.entities.EntityPhysics
import kotlin.math.abs
object CollisionMovementPhysics {
fun World.collectCollisions(context: CollisionContext, movement: Vec3d, aabb: AABB, chunk: Chunk?, predicate: CollisionPredicate? = null): VoxelShape {
val shapes: ArrayList<AABB> = ArrayList()
// TODO: add entity collisions (boat, shulker)
// TODO: add world border collision shape
for ((position, state, chunk) in WorldIterator(aabb.extend(movement).grow(1.0).positions(), this, chunk)) {
if (state.block !is CollidableBlock) continue
if (predicate != null && !predicate.invoke(state)) continue
// TODO: filter blocks (e.g. moving piston), whatever that means
var shape = when (state.block) {
is FixedCollidable -> state.block.getCollisionShape(state)
is BlockWithEntity<*> -> {
state.block.getCollisionShape(session, context, position, state, chunk.getBlockEntity(position.inChunkPosition))
}
else -> {
state.block.getCollisionShape(session, context, position, state, null)
}
} ?: continue
shape += position
if (position in aabb && shape.intersect(aabb)) {
continue
}
shapes += shape
}
return VoxelShape(shapes)
}
fun EntityPhysics<*>.collectCollisions(movement: Vec3d, aabb: AABB, predicate: CollisionPredicate? = null): VoxelShape {
return this.entity.session.world.collectCollisions(EntityCollisionContext(entity, this, aabb), movement, aabb, positionInfo.chunk, predicate)
fun EntityPhysics<*>.collectCollisions(movement: Vec3d, aabb: AABB, predicate: CollisionPredicate? = null): CollisionShape {
return CollisionShape(this.entity.session.world, EntityCollisionContext(entity, this, aabb), aabb, movement, positionInfo.chunk, predicate)
}
private fun checkMovement(axis: Axes, originalValue: Double, offsetAABB: Boolean, aabb: AABB, collisions: AbstractVoxelShape): Double {
@ -114,13 +74,19 @@ object CollisionMovementPhysics {
fun EntityPhysics<*>.collide(movement: Vec3d): Vec3d {
val aabb = aabb
val collisions = collectCollisions(movement, aabb)
val collision = collide(movement, aabb, collisions)
if (stepHeight <= 0.0) {
return collision
}
if (aabb.min.y <= BlockPosition.MIN_Y || aabb.max.y >= BlockPosition.MAX_Y) return movement // TODO: also check movement
return collideStepping(movement, collision, collisions)
val collisions = collectCollisions(movement, aabb)
try {
val collision = collide(movement, aabb, collisions)
if (stepHeight <= 0.0) {
return collision
}
return collideStepping(movement, collision, collisions)
} finally {
collisions.free()
}
}
private fun EntityPhysics<*>.collideStepping(movement: Vec3d, collision: Vec3d, collisions: AbstractVoxelShape): Vec3d {

View File

@ -1,6 +1,6 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
* Copyright (C) 2020-2025 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
@ -33,7 +33,7 @@ internal class AABBTest {
val a = AABB(Vec3d(5.0, 0.0, 7.0), Vec3d(6.0, 1.0, 8.0))
val b = AABB(Vec3d(5.7, 1.0, 6.3), Vec3d(6.3, 3, 6.9))
assertEquals(1239312.0, a.calculateMaxOffset(b, 1239312.0, Axes.Z))
assertEquals(1239312.0, a.calculateMaxDistance(b, 1239312.0, Axes.Z))
}
@Test
@ -41,7 +41,7 @@ internal class AABBTest {
val a = AABB(Vec3d(5.0, 0.0, 7.0), Vec3d(6.0, 1.0, 8.0))
val b = AABB(Vec3d(5.699999988079071, 0.5358406250445555, 6.373910529638632), Vec3d(6.300000011920929, 2.3358405773608397, 6.97391055348049))
assertNotEquals(0.1, a.calculateMaxOffset(b, 0.1, Axes.Z))
assertNotEquals(0.1, a.calculateMaxDistance(b, 0.1, Axes.Z))
}
@Test
@ -49,7 +49,7 @@ internal class AABBTest {
val a = AABB(Vec3d(5.0, 0.0, 6.0), Vec3d(5.8, 1.0, 7.0))
val b = AABB(Vec3d(5.7, 1.0, 6.0), Vec3d(6.3, 2.8, 6.6))
assertEquals(0.0, a.calculateMaxOffset(b, -0.0784000015258789, Axes.Y))
assertEquals(0.0, a.calculateMaxDistance(b, -0.0784000015258789, Axes.Y))
}
@Test
@ -57,7 +57,7 @@ internal class AABBTest {
val a = AABB(Vec3d(5.0, 0.0, 6.0), Vec3d(5.8, 1.0, 7.0))
val b = AABB(Vec3d(5.0, 1.0, 6.0), Vec3d(5.8, 2.8, 6.6))
assertEquals(0.0, a.calculateMaxOffset(b, -0.0784000015258789, Axes.Y))
assertEquals(0.0, a.calculateMaxDistance(b, -0.0784000015258789, Axes.Y))
}
@Test
@ -65,7 +65,7 @@ internal class AABBTest {
val a = AABB(Vec3d(5.0, 0.0, 6.0), Vec3d(5.8, 1.0, 7.0))
val b = AABB(Vec3d(5.1, 1.0, 5.9), Vec3d(5.5, 2.8, 7.1))
assertEquals(0.0, a.calculateMaxOffset(b, -0.0784000015258789, Axes.Y))
assertEquals(0.0, a.calculateMaxDistance(b, -0.0784000015258789, Axes.Y))
}