Skip to content

Commit

Permalink
Supports sparse infinite tilemaps with BVH for spatial finding (#1080)
Browse files Browse the repository at this point in the history
* Moved BVH to KDS to be able to use it at the KDS layer

* Created a new IStackedIntArray2 interface to support additional interfaces

* Creates a SparseChunkedStackedIntArray2 implementation supporting chunked arrays implemented the interface

* Make TileMap to support the new IStackedIntArray2 interface, and adjust bounds to use startX, startY, endX, endY

* Updated TiledMap to use SparseChunkedStackedIntArray2 on infinite maps
  • Loading branch information
soywiz authored Oct 30, 2022
1 parent 069ca1c commit ea563da
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 87 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.soywiz.kds

import com.soywiz.kds.ds.*
import com.soywiz.kds.iterators.*
import kotlin.math.*

class SparseChunkedStackedIntArray2(override var empty: Int = -1) : IStackedIntArray2 {
var minX = 0
var minY = 0
var maxX = 0
var maxY = 0
override var maxLevel: Int = 0

val bvh = BVH<IStackedIntArray2>(dimensions = 2)
var first: IStackedIntArray2? = null
var last: IStackedIntArray2? = null

fun putChunk(chunk: IStackedIntArray2) {
if (first == null) {
first = chunk
empty = chunk.empty
minX = Int.MAX_VALUE
minY = Int.MAX_VALUE
maxX = Int.MIN_VALUE
maxY = Int.MIN_VALUE
}
last = chunk
bvh.insertOrUpdate(
BVHIntervals(chunk.startX, chunk.width, chunk.startY, chunk.height),
chunk
)
minX = min(minX, chunk.startX)
minY = min(minY, chunk.startY)
maxX = max(maxX, chunk.endX)
maxY = max(maxY, chunk.endY)
maxLevel = max(maxLevel, chunk.maxLevel)
}

override val startX: Int get() = minX
override val startY: Int get() = minY
override val width: Int get() = maxX - minX
override val height: Int get() = maxY - minY

fun findAllChunks(): List<IStackedIntArray2> = bvh.findAllValues()

private var lastSearchChunk: IStackedIntArray2? = null

private fun IStackedIntArray2.chunkX(x: Int): Int = x - this.startX
private fun IStackedIntArray2.chunkY(y: Int): Int = y - this.startY
private fun IStackedIntArray2.containsChunk(x: Int, y: Int): Boolean {
return x in startX until endX && y in startY until endY
}

fun getChunkAt(x: Int, y: Int): IStackedIntArray2? {
// Cache to be much faster while iterating rows
lastSearchChunk?.let {
if (it.containsChunk(x, y)) return it
}
lastSearchChunk = bvh.searchValues(BVHIntervals(x, 1, y, 1)).firstOrNull()
return lastSearchChunk
}

override fun set(x: Int, y: Int, level: Int, value: Int) {
getChunkAt(x, y)?.let { chunk ->
chunk[chunk.chunkX(x), chunk.chunkY(y), level] = value
}
}

override fun get(x: Int, y: Int, level: Int): Int {
getChunkAt(x, y)?.let { chunk ->
return chunk[chunk.chunkX(x), chunk.chunkY(y), level]
}
return empty
}

override fun getStackLevel(x: Int, y: Int): Int {
getChunkAt(x, y)?.let { chunk ->
return chunk.getStackLevel(chunk.chunkX(x), chunk.chunkY(y))
}
return 0
}

override fun push(x: Int, y: Int, value: Int) {
getChunkAt(x, y)?.let { chunk ->
chunk.push(chunk.chunkX(x), chunk.chunkY(y), value)
}
}

override fun removeLast(x: Int, y: Int) {
getChunkAt(x, y)?.let { chunk ->
chunk.removeLast(chunk.chunkX(x), chunk.chunkY(y))
}
}

override fun clone(): SparseChunkedStackedIntArray2 = SparseChunkedStackedIntArray2(empty).also { sparse ->
findAllChunks().fastForEach {
sparse.putChunk(it.clone())
}
}
}
113 changes: 91 additions & 22 deletions kds/src/commonMain/kotlin/com/soywiz/kds/StackedIntArray2.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,95 @@
package com.soywiz.kds

class StackedIntArray2(val width: Int, val height: Int, val empty: Int = -1) {
import com.soywiz.kds.internal.*

interface IStackedIntArray2 {
/** Annotation of where in [startX] this stack would be placed in a bigger container, not used for set or get methods */
val startX: Int
/** Annotation of where in [endY] this stack would be placed in a bigger container, not used for set or get methods */
val startY: Int

/** [width] of the data available here, get and set methods use values in the range x=0 until [width] */
val width: Int
/** [height] of the data available here, get and set methods use values in the range y=0 until [height] */
val height: Int

/** The [empty] value that will be returned if the specified cell it out of bounds, or empty */
val empty: Int
/** The maximum level of layers available on the whole stack */
val maxLevel: Int

/** Duplicates the contents of this [IStackedIntArray2] keeping its contents data */
fun clone(): IStackedIntArray2

/** Sets the [value] at [x], [y] at [level], [startX] and [startY] are NOT used here so 0,0 means the top-left element */
operator fun set(x: Int, y: Int, level: Int, value: Int)
/** Gets the value at [x], [y] at [level], [startX] and [startY] are NOT used here so 0,0 means the top-left element */
operator fun get(x: Int, y: Int, level: Int): Int

/** Number of values available at this [x], [y] */
fun getStackLevel(x: Int, y: Int): Int

/** Adds a new [value] on top of [x], [y] */
fun push(x: Int, y: Int, value: Int)

/** Removes the last value at [x], [y] */
fun removeLast(x: Int, y: Int)

/** Set the first [value] of a stack in the cell [x], [y] */
fun setFirst(x: Int, y: Int, value: Int) {
set(x, y, 0, value)
}

/** Gets the first value of the stack in the cell [x], [y] */
fun getFirst(x: Int, y: Int): Int {
val level = getStackLevel(x, y)
if (level == 0) return empty
return get(x, y, 0)
}

/** Gets the last value of the stack in the cell [x], [y] */
fun getLast(x: Int, y: Int): Int {
val level = getStackLevel(x, y)
if (level == 0) return empty
return get(x, y, level - 1)
}
}

/** Shortcut for [IStackedIntArray2.startX] + [IStackedIntArray2.width] */
val IStackedIntArray2.endX: Int get() = startX + width
/** Shortcut for [IStackedIntArray2.startY] + [IStackedIntArray2.height] */
val IStackedIntArray2.endY: Int get() = startY + height

class StackedIntArray2(
override val width: Int,
override val height: Int,
override val empty: Int = -1,
override val startX: Int = 0,
override val startY: Int = 0,
) : IStackedIntArray2 {

override fun clone(): StackedIntArray2 {
return StackedIntArray2(width, height, empty, startX, startY).also { out ->
arraycopy(this.level.data, 0, out.level.data, 0, out.level.data.size)
out.data.addAll(this.data.map { it.clone() })
}
}

val level = IntArray2(width, height, 0)
val data = fastArrayListOf<IntArray2>()

val maxLevel: Int get() = data.size
override val maxLevel: Int get() = data.size

companion object {
operator fun invoke(vararg layers: IntArray2, width: Int = layers.first().width, height: Int = layers.first().height): StackedIntArray2 {
val stacked = StackedIntArray2(width, height)
operator fun invoke(
vararg layers: IntArray2,
width: Int = layers.first().width,
height: Int = layers.first().height,
empty: Int = -1,
startX: Int = 0,
startY: Int = 0,
): StackedIntArray2 {
val stacked = StackedIntArray2(width, height, empty, startX = startX, startY = startY)
stacked.level.fill { layers.size }
stacked.data.addAll(layers)
return stacked
Expand All @@ -24,37 +105,25 @@ class StackedIntArray2(val width: Int, val height: Int, val empty: Int = -1) {
this.data[level] = data
}

operator fun set(level: Int, x: Int, y: Int, value: Int) {
override operator fun set(x: Int, y: Int, level: Int, value: Int) {
ensureLevel(level)
data[level][x, y] = value
}

operator fun get(x: Int, y: Int, level: Int): Int {
override operator fun get(x: Int, y: Int, level: Int): Int {
if (level > this.level[x, y]) return empty
return data[level][x, y]
}

fun getStackLevel(x: Int, y: Int): Int {
override fun getStackLevel(x: Int, y: Int): Int {
return this.level[x, y]
}

fun getFirst(x: Int, y: Int): Int {
val level = this.level[x, y]
if (level == 0) return empty
return data[0][x, y]
}

fun getLast(x: Int, y: Int): Int {
val level = this.level[x, y]
if (level == 0) return empty
return data[level - 1][x, y]
}

fun push(x: Int, y: Int, value: Int) {
set(level[x, y]++, x, y, value)
override fun push(x: Int, y: Int, value: Int) {
set(x, y, level[x, y]++, value)
}

fun removeLast(x: Int, y: Int) {
override fun removeLast(x: Int, y: Int) {
level[x, y] = (level[x, y] - 1).coerceAtLeast(0)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ Jon-Carlos Rivera - imbcmdth@hotmail.com
******************************************************************************/
@file:Suppress("LocalVariableName", "FunctionName")

package com.soywiz.korma.geom.ds
package com.soywiz.kds.ds

import com.soywiz.kds.FastArrayList
import com.soywiz.kds.fastArrayListOf
import com.soywiz.kds.*
import kotlin.collections.HashMap
import kotlin.collections.List
import kotlin.collections.contains
Expand All @@ -55,7 +54,7 @@ class BVH<T>(
val dimensions: Int = 2,
width: Int = dimensions * 3,
val allowUpdateObjects: Boolean = true
) {
) : Iterable<BVH.Node<T>> {
// Variables to control tree
// Number of "interval pairs" per node
// Maximum width of any node before a split
Expand Down Expand Up @@ -112,6 +111,11 @@ class BVH<T>(
nodes = fastArrayListOf()
)

fun isEmpty(): Boolean {
val nodes = root.nodes ?: return true
return nodes.isEmpty()
}

/* expands intervals A to include intervals B, intervals B is untouched
* [ rectangle a ] = expand_rectangle(rectangle a, rectangle b)
* @static function
Expand Down Expand Up @@ -735,6 +739,35 @@ class BVH<T>(
)
}

override fun iterator(): Iterator<Node<T>> = iterator<Node<T>> {
val deque = Deque<Node<T>>()
deque.addLast(root)
while (deque.isNotEmpty()) {
val node = deque.removeFirst()
yield(node)
node.nodes?.let { deque.addAll(it) }
}
}

fun findAll(): List<Node<T>> {
return this.toList()
}

fun findAllValues(): List<T> {
return findAll().mapNotNull { it.value }
}

fun searchValues(
intervals: BVHIntervals,
comparators: Comparators = Comparators,
): List<T> {
return _search_subtree(
intervals = intervals,
root = this.root,
comparators = comparators
).mapNotNull { it.value }
}

/* non-recursive insert function
* [] = NTree.insert(intervals, object to insert)
*/
Expand Down Expand Up @@ -804,12 +837,19 @@ class BVH<T>(
}
}

/**
* In the format:
*
* [x, width, y, height]
* [x, width, y, height, z, depth]
*/
@Suppress("INLINE_CLASS_DEPRECATED")
inline class BVHIntervals(val data: DoubleArray) {
constructor(dimensions: Int) : this(DoubleArray(dimensions * 2))

companion object {
operator fun invoke(vararg values: Double) = BVHIntervals(values)
operator fun invoke(vararg values: Double): BVHIntervals = BVHIntervals(values)
operator fun invoke(vararg values: Int): BVHIntervals = BVHIntervals(DoubleArray(values.size) { values[it].toDouble() })
}

fun checkDimensions(dimensions: Int) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
package com.soywiz.kds.ds/*
// Copyright(C) David W. Jeske, 2014, and released to the public domain.
//
// Dynamic BVH (Bounding Volume Hierarchy) using incremental refit and tree-rotations
Expand Down
Loading

0 comments on commit ea563da

Please sign in to comment.