Entity spatial index

This commit is contained in:
DBotThePony 2024-03-30 12:36:17 +07:00
parent f452cbeeb1
commit 6302661019
Signed by: DBot
GPG Key ID: DCC23B5715498507
21 changed files with 801 additions and 258 deletions

View File

@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
kotlinVersion=1.9.10 kotlinVersion=1.9.10
kotlinCoroutinesVersion=1.8.0 kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.11.0 kommonsVersion=2.11.1
ffiVersion=2.2.13 ffiVersion=2.2.13
lwjglVersion=3.3.0 lwjglVersion=3.3.0

View File

@ -308,7 +308,7 @@ class ClientWorld(
} }
} }
override fun tickInner() { override fun tick0() {
} }

View File

@ -2,10 +2,6 @@ package ru.dbotthepony.kstarbound.math
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
fun lerp(t: Double, a: Double, b: Double): Double {
return a * (1.0 - t) + b * t
}
/** /**
* Выполняет преобразование [value] типа [Double] в [Int] так, * Выполняет преобразование [value] типа [Double] в [Int] так,
* что выходной [Int] всегда будет больше или равен по модулю [value] * что выходной [Int] всегда будет больше или равен по модулю [value]
@ -59,11 +55,7 @@ fun roundTowardsPositiveInfinity(value: Double): Int {
} }
fun divideUp(a: Int, b: Int): Int { fun divideUp(a: Int, b: Int): Int {
return if (a % b == 0) { return (a + b - 1) / b
a / b
} else {
a / b + 1
}
} }
private const val EPSILON = 0.00000001 private const val EPSILON = 0.00000001

View File

@ -190,7 +190,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
val result = ArrayList<AABBi>() val result = ArrayList<AABBi>()
var mins = Vector2i(windowXMin - GlobalDefaults.client.windowMonitoringBorder, windowYMin - GlobalDefaults.client.windowMonitoringBorder) var mins = Vector2i(windowXMin - GlobalDefaults.client.windowMonitoringBorder, windowYMin - GlobalDefaults.client.windowMonitoringBorder)
var maxs = Vector2i(windowWidth + GlobalDefaults.client.windowMonitoringBorder, windowHeight + GlobalDefaults.client.windowMonitoringBorder) var maxs = Vector2i(windowWidth + GlobalDefaults.client.windowMonitoringBorder * 2, windowHeight + GlobalDefaults.client.windowMonitoringBorder * 2)
if (maxs.x - mins.x > 1000) { if (maxs.x - mins.x > 1000) {
// holy shit // holy shit

View File

@ -37,7 +37,9 @@ class EntityUpdateSetPacket(val forConnection: Int, val deltas: Int2ObjectMap<By
connection.disconnect("Updating entity with ID $id outside of allowed range ${connection.entityIDRange}") connection.disconnect("Updating entity with ID $id outside of allowed range ${connection.entityIDRange}")
break break
} else { } else {
entities[id]?.networkGroup?.read(delta, Starbound.TIMESTEP * 3.0, connection.isLegacy) val entity = entities[id] ?: continue
entity.networkGroup.read(delta, Starbound.TIMESTEP * 3.0, connection.isLegacy)
entity.onNetworkUpdate()
} }
} }
} }

View File

@ -164,7 +164,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
if (world == shipWorld) { if (world == shipWorld) {
disconnect("ShipWorld refused to accept its owner: $it") disconnect("ShipWorld refused to accept its owner: $it")
} else { } else {
enqueueWarp(WarpAlias.OwnShip) enqueueWarp(returnWarp ?: WarpAlias.OwnShip)
} }
} }
} }
@ -242,6 +242,10 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
shipWorld.thread.start() shipWorld.thread.start()
enqueueWarp(WarpAlias.OwnShip) enqueueWarp(WarpAlias.OwnShip)
warpingAllowed = true warpingAllowed = true
if (server.channels.connections.size == 2) {
enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!))
}
} }
}.exceptionally { }.exceptionally {
LOGGER.error("Error while initializing shipworld for $this", it) LOGGER.error("Error while initializing shipworld for $this", it)

View File

@ -40,6 +40,7 @@ import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Consumer import java.util.function.Consumer
import java.util.function.Predicate
import java.util.function.Supplier import java.util.function.Supplier
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
@ -104,7 +105,6 @@ class ServerWorld private constructor(
val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS) val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS)
private val str = "Server World ${if (worldID == WorldID.Limbo) "limbo(${server.limboWorldIndex.getAndIncrement()})" else worldID.toString()}" private val str = "Server World ${if (worldID == WorldID.Limbo) "limbo(${server.limboWorldIndex.getAndIncrement()})" else worldID.toString()}"
val thread = Thread(spinner, str) val thread = Thread(spinner, str)
val ticketListLock = ReentrantLock()
private val isClosed = AtomicBoolean() private val isClosed = AtomicBoolean()
@ -180,7 +180,7 @@ class ServerWorld private constructor(
return topMost return topMost
} }
override fun tickInner() { override fun tick0() {
val packet = StepUpdatePacket(ticks) val packet = StepUpdatePacket(ticks)
players.forEach { players.forEach {
@ -207,7 +207,11 @@ class ServerWorld private constructor(
val chunk = chunkMap[it.pos] val chunk = chunkMap[it.pos]
if (chunk != null) { if (chunk != null) {
val unloadable = chunk.entities.filter { it.isApplicableForUnloading } val unloadable = entityIndex
.query(
chunk.aabb,
predicate = Predicate { it.isApplicableForUnloading && chunk.aabb.isInside(it.position) },
distinct = true, withEdges = false)
storage.saveCells(it.pos, chunk.copyCells()) storage.saveCells(it.pos, chunk.copyCells())
storage.saveEntities(it.pos, unloadable) storage.saveEntities(it.pos, unloadable)
@ -237,13 +241,14 @@ class ServerWorld private constructor(
private val ticketMap = Long2ObjectOpenHashMap<TicketList>() private val ticketMap = Long2ObjectOpenHashMap<TicketList>()
private val ticketLists = ArrayList<TicketList>() private val ticketLists = ArrayList<TicketList>()
private val ticketListLock = ReentrantLock()
private fun getTicketList(pos: ChunkPos): TicketList { private fun getTicketList(pos: ChunkPos): TicketList {
return ticketMap.computeIfAbsent(geometry.wrapToLong(pos), Long2ObjectFunction { TicketList(it) }) return ticketMap.computeIfAbsent(geometry.wrapToLong(pos), Long2ObjectFunction { TicketList(it) })
} }
fun permanentChunkTicket(pos: ChunkPos): ITicket { fun permanentChunkTicket(pos: ChunkPos): ITicket {
lock.withLock { ticketListLock.withLock {
return getTicketList(pos).Ticket() return getTicketList(pos).Ticket()
} }
} }
@ -251,7 +256,7 @@ class ServerWorld private constructor(
fun temporaryChunkTicket(pos: ChunkPos, time: Int): ITicket { fun temporaryChunkTicket(pos: ChunkPos, time: Int): ITicket {
require(time > 0) { "Invalid ticket time: $time" } require(time > 0) { "Invalid ticket time: $time" }
lock.withLock { ticketListLock.withLock {
return getTicketList(pos).TimedTicket(time) return getTicketList(pos).TimedTicket(time)
} }
} }
@ -308,16 +313,6 @@ class ServerWorld private constructor(
return temporary.isNotEmpty() || permanent.isNotEmpty() return temporary.isNotEmpty() || permanent.isNotEmpty()
} }
override fun onEntityAdded(entity: AbstractEntity) {
permanent.forEach { it.listener?.onEntityAdded(entity) }
temporary.forEach { it.listener?.onEntityAdded(entity) }
}
override fun onEntityRemoved(entity: AbstractEntity) {
permanent.forEach { it.listener?.onEntityRemoved(entity) }
temporary.forEach { it.listener?.onEntityRemoved(entity) }
}
override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {
permanent.forEach { it.listener?.onCellChanges(x, y, cell) } permanent.forEach { it.listener?.onCellChanges(x, y, cell) }
temporary.forEach { it.listener?.onCellChanges(x, y, cell) } temporary.forEach { it.listener?.onCellChanges(x, y, cell) }
@ -374,10 +369,9 @@ class ServerWorld private constructor(
final override fun cancel() { final override fun cancel() {
if (isCanceled) return if (isCanceled) return
lock.withLock { ticketListLock.withLock {
if (isCanceled) return if (isCanceled) return
isCanceled = true isCanceled = true
chunk?.entities?.forEach { e -> listener?.onEntityRemoved(e) }
loadFuture?.cancel(false) loadFuture?.cancel(false)
listener = null listener = null
cancel0() cancel0()
@ -389,14 +383,6 @@ class ServerWorld private constructor(
get() = chunkMap[pos] get() = chunkMap[pos]
final override var listener: IChunkListener? = null final override var listener: IChunkListener? = null
set(value) {
if (field != value) {
val chunk = chunk
chunk?.entities?.forEach { e -> field?.onEntityRemoved(e) }
chunk?.entities?.forEach { e -> value?.onEntityAdded(e) }
field = value
}
}
} }
inner class Ticket : AbstractTicket() { inner class Ticket : AbstractTicket() {
@ -428,7 +414,7 @@ class ServerWorld private constructor(
override fun prolong(ticks: Int) { override fun prolong(ticks: Int) {
if (ticks == 0 || isCanceled) return if (ticks == 0 || isCanceled) return
lock.withLock { ticketListLock.withLock {
if (isCanceled) return if (isCanceled) return
temporary.remove(this) temporary.remove(this)

View File

@ -3,8 +3,9 @@ package ru.dbotthepony.kstarbound.server.world
import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
@ -16,6 +17,7 @@ import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket
import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.CentralStructureUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.CentralStructureUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket
@ -69,9 +71,6 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
} }
private inner class Ticket(val ticket: ServerWorld.ITicket, val pos: ChunkPos) : IChunkListener { private inner class Ticket(val ticket: ServerWorld.ITicket, val pos: ChunkPos) : IChunkListener {
override fun onEntityAdded(entity: AbstractEntity) {}
override fun onEntityRemoved(entity: AbstractEntity) {}
override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {
if (pos !in pendingSend) { if (pos !in pendingSend) {
send(LegacyTileUpdatePacket(pos.tile + Vector2i(x, y), cell.toLegacyNet())) send(LegacyTileUpdatePacket(pos.tile + Vector2i(x, y), cell.toLegacyNet()))
@ -141,11 +140,13 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
client.playerEntity = world.entities[client.playerID] as? PlayerEntity client.playerEntity = world.entities[client.playerID] as? PlayerEntity
run { val trackingRegions = client.trackingTileRegions()
val newTrackedChunks = ObjectArraySet<ChunkPos>()
for (region in client.trackingTileRegions()) { run {
newTrackedChunks.addAll(world.geometry.tileRegion2Chunks(region)) val newTrackedChunks = ArrayList<ChunkPos>()
for (region in trackingRegions) {
newTrackedChunks.addAll(world.geometry.region2Chunks(region))
} }
val itr = tickets.entries.iterator() val itr = tickets.entries.iterator()
@ -186,26 +187,55 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
} }
} }
for ((id, entity) in world.entities) { run {
if (entity.connectionID != client.connectionID && entity is PlayerEntity) { val trackingEntities = ObjectAVLTreeSet<AbstractEntity>()
if (entityVersions.get(id) == -1L) {
// never networked
val initial = FastByteArrayOutputStream()
entity.writeNetwork(DataOutputStream(initial), client.isLegacy)
val (data, version) = entity.networkGroup.write(isLegacy = client.isLegacy)
entityVersions.put(id, version) for (region in trackingRegions) {
// we don't care about distinct values here, since
// we handle this by ourselves
trackingEntities.addAll(world.entityIndex.query(region, filter = { it.connectionID != client.connectionID }, distinct = false))
}
send(EntityCreatePacket( val unseen = IntArrayList(entityVersions.keys)
entity.type,
ByteArrayList.wrap(initial.array, initial.length), for (entity in trackingEntities) {
data, val id = entity.entityID
entity.entityID unseen.rem(id)
))
if (entity is PlayerEntity) {
if (entityVersions.get(id) == -1L) {
// never networked
val initial = FastByteArrayOutputStream()
entity.writeNetwork(DataOutputStream(initial), client.isLegacy)
val (data, version) = entity.networkGroup.write(isLegacy = client.isLegacy)
entityVersions.put(id, version)
send(EntityCreatePacket(
entity.type,
ByteArrayList.wrap(initial.array, initial.length),
data,
entity.entityID
))
} else {
val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy)
entityVersions.put(id, version)
send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data)))
}
}
}
val itr = unseen.iterator()
while (itr.hasNext()) {
val id = itr.nextInt()
val entity = world.entities[id]
val version = entityVersions.remove(id)
if (entity == null) {
send(EntityDestroyPacket(id, ByteArrayList(), false))
} else { } else {
val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy) send(EntityDestroyPacket(id, entity.networkGroup.write(version, isLegacy = client.isLegacy).first, false))
entityVersions.put(id, version)
send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data)))
} }
} }
} }

View File

@ -20,6 +20,7 @@ import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
import ru.dbotthepony.kstarbound.world.entities.TileEntity import ru.dbotthepony.kstarbound.world.entities.TileEntity
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
/** /**
@ -51,10 +52,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
var backgroundChangeset = 0 var backgroundChangeset = 0
private set private set
val entities = ReferenceOpenHashSet<AbstractEntity>() protected val subscribers = CopyOnWriteArraySet<IChunkListener>()
val dynamicEntities = ReferenceOpenHashSet<DynamicEntity>()
val tileEntities = ReferenceOpenHashSet<TileEntity>()
protected val subscribers = ObjectArraySet<IChunkListener>()
// local cells' tile access // local cells' tile access
val localBackgroundView = TileView.Background(this) val localBackgroundView = TileView.Background(this)
@ -245,94 +243,18 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
fun addListener(subscriber: IChunkListener): Boolean { fun addListener(subscriber: IChunkListener): Boolean {
if (subscribers.add(subscriber)) { return subscribers.add(subscriber)
entities.forEach { subscriber.onEntityAdded(it) }
return true
}
return false
} }
fun removeListener(subscriber: IChunkListener): Boolean { fun removeListener(subscriber: IChunkListener): Boolean {
if (subscribers.remove(subscriber)) { return subscribers.remove(subscriber)
entities.forEach { subscriber.onEntityRemoved(it) }
return true
}
return false
}
fun addEntity(entity: AbstractEntity) {
world.lock.withLock {
if (!entities.add(entity))
throw IllegalArgumentException("Already having having entity $entity")
if (entity is TileEntity)
tileEntities.add(entity)
if (entity is DynamicEntity)
dynamicEntities.add(entity)
changeset++
subscribers.forEach { it.onEntityAdded(entity) }
}
}
fun transferEntity(entity: AbstractEntity, otherChunk: Chunk<*, *>) {
world.lock.withLock {
if (otherChunk == this)
throw IllegalArgumentException("what?")
if (world != otherChunk.world)
throw IllegalArgumentException("Chunks belong to different worlds: this: $this / other: $otherChunk")
changeset++
otherChunk.changeset++
entities.add(entity)
otherChunk.entities.remove(entity)
if (entity is TileEntity) {
tileEntities.add(entity)
otherChunk.tileEntities.remove(entity)
}
if (entity is DynamicEntity) {
dynamicEntities.add(entity)
otherChunk.dynamicEntities.remove(entity)
}
otherChunk.subscribers.forEach { it.onEntityRemoved(entity) }
subscribers.forEach { it.onEntityAdded(entity) }
}
}
fun removeEntity(entity: AbstractEntity) {
world.lock.withLock {
if (!entities.remove(entity))
throw IllegalArgumentException("Already not having entity $entity")
if (entity is TileEntity)
tileEntities.remove(entity)
if (entity is DynamicEntity)
dynamicEntities.remove(entity)
changeset++
subscribers.forEach { it.onEntityRemoved(entity) }
}
} }
override fun toString(): String { override fun toString(): String {
return "${this::class.simpleName}(pos=$pos, entityCount=${entities.size}, world=$world)" return "${this::class.simpleName}(pos=$pos, world=$world)"
} }
open fun remove() { open fun remove() {
world.lock.withLock {
for (ent in ObjectArrayList(entities)) {
ent.chunk = null
}
}
} }
open fun tick() { open fun tick() {

View File

@ -1,7 +1,9 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.math.divideUp import ru.dbotthepony.kstarbound.math.divideUp
import kotlin.math.pow
fun positiveModulo(a: Int, b: Int): Int { fun positiveModulo(a: Int, b: Int): Int {
val result = a % b val result = a % b
@ -36,6 +38,7 @@ abstract class CoordinateMapper {
fun chunkFromCell(value: Double): Int = chunkFromCell(value.toInt()) fun chunkFromCell(value: Double): Int = chunkFromCell(value.toInt())
data class SplitResult(val rangeA: Vector2i, val rangeB: Vector2i?, val offset: Int) data class SplitResult(val rangeA: Vector2i, val rangeB: Vector2i?, val offset: Int)
data class SplitResultDouble(val rangeA: Vector2d, val rangeB: Vector2d?, val offset: Double)
/** /**
* One of: * One of:
@ -49,6 +52,11 @@ abstract class CoordinateMapper {
abstract fun split(first: Int, last: Int): SplitResult abstract fun split(first: Int, last: Int): SplitResult
// splitting doubles is tricky, because we have to decide what is world's edge
// In case of ints, world edge is determined to be width - 1, but what about doubles?
// width - 0.1 is clearly not out of bounds, but how can we represent this?
abstract fun split(first: Double, last: Double): SplitResultDouble
// inside world bounds // inside world bounds
abstract fun inBoundsCell(value: Int): Boolean abstract fun inBoundsCell(value: Int): Boolean
abstract fun inBoundsChunk(value: Int): Boolean abstract fun inBoundsChunk(value: Int): Boolean
@ -59,6 +67,20 @@ abstract class CoordinateMapper {
class Wrapper(private val cells: Int) : CoordinateMapper() { class Wrapper(private val cells: Int) : CoordinateMapper() {
override val chunks = divideUp(cells, CHUNK_SIZE) override val chunks = divideUp(cells, CHUNK_SIZE)
private val cellsD = cells.toDouble()
// try to approach reasonable "edge" value, which is just little less than `cells`
private var cellsEdge = cellsD
init {
var power = -64.0
do {
cellsEdge -= 2.0.pow(++power)
} while (cellsEdge >= cellsD)
}
private val edgeDelta = cellsD - cellsEdge
override fun inBoundsCell(value: Int) = value in 0 until cells override fun inBoundsCell(value: Int) = value in 0 until cells
override fun inBoundsChunk(value: Int) = value in 0 until chunks override fun inBoundsChunk(value: Int) = value in 0 until chunks
@ -109,10 +131,68 @@ abstract class CoordinateMapper {
} }
} }
} }
override fun split(first: Double, last: Double): SplitResultDouble {
if (first >= last) {
// point or empty range
val wrap = cell(first)
return SplitResultDouble(Vector2d(wrap, wrap), null, 0.0)
} else if (first <= 0.0 && last >= cellsEdge) {
// covers entire world along this axis
return SplitResultDouble(Vector2d(0.0, cellsEdge), null, 0.0)
} else if (first >= 0.0 && last < cellsEdge) {
// within range along this axis
return SplitResultDouble(Vector2d(first, last), null, 0.0)
} else {
val newFirst = cell(first)
val newLast = cell(last)
if (first < 0.0) {
// wrapped around left edge
return SplitResultDouble(
Vector2d(0.0, newLast.coerceAtLeast(edgeDelta)), // handles case where splitting happens *exactly* at world's border
Vector2d(newFirst, cellsEdge),
newFirst - cellsD
)
} else {
// wrapped around right edge
return SplitResultDouble(
Vector2d(newFirst, cellsEdge),
Vector2d(0.0, newLast.coerceAtLeast(edgeDelta)), // handles case where splitting happens *exactly* at world's border
-newLast - 1
)
}
}
}
} }
class Clamper(private val cells: Int) : CoordinateMapper() { class Clamper(private val cells: Int) : CoordinateMapper() {
override val chunks = divideUp(cells, CHUNK_SIZE) override val chunks = divideUp(cells, CHUNK_SIZE)
private val cellsD = cells.toDouble()
private val cellsF = cells.toFloat()
// try to approach reasonable "edge" value, which is just little less than `cells`
private var cellsEdge = cellsD
init {
var power = -64.0
do {
cellsEdge -= 2.0.pow(++power)
} while (cellsEdge >= cellsD)
}
private var cellsEdgeFloat = cellsF
init {
var power = -64f
do {
cellsEdgeFloat -= 2f.pow(++power)
} while (cellsEdgeFloat >= cellsF)
}
override fun inBoundsCell(value: Int): Boolean { override fun inBoundsCell(value: Int): Boolean {
return value in 0 until cells return value in 0 until cells
@ -131,16 +211,21 @@ abstract class CoordinateMapper {
return SplitResult(Vector2i(cell(first), cell(last)), null, 0) return SplitResult(Vector2i(cell(first), cell(last)), null, 0)
} }
override fun split(first: Double, last: Double): SplitResultDouble {
// just clamp
return SplitResultDouble(Vector2d(cell(first), cell(last)), null, 0.0)
}
override fun cell(value: Int): Int { override fun cell(value: Int): Int {
return value.coerceIn(0, cells - 1) return value.coerceIn(0, cells - 1)
} }
override fun cell(value: Double): Double { override fun cell(value: Double): Double {
return value.coerceIn(0.0, cells - 1.0) return value.coerceIn(0.0, cellsEdge)
} }
override fun cell(value: Float): Float { override fun cell(value: Float): Float {
return value.coerceIn(0f, cells - 1f) return value.coerceIn(0f, cellsEdgeFloat)
} }
override fun chunk(value: Int): Int { override fun chunk(value: Int): Int {

View File

@ -4,8 +4,6 @@ import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
interface IChunkListener { interface IChunkListener {
fun onEntityAdded(entity: AbstractEntity) {}
fun onEntityRemoved(entity: AbstractEntity) {}
fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {} fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {}
fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) {} fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) {}
} }

View File

@ -0,0 +1,301 @@
package ru.dbotthepony.kstarbound.world
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.longs.LongArrayList
import it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
import kotlin.concurrent.withLock
// After some thinking, I decided to go with separate spatial index over
// using chunk/chunkmap as spatial indexing of entities (just like original engine does).
// Advantages of this system include allowing to put entities outside any chunks
// into index and getting rid of "orphaned entities" collection,
// while also setting up ground for better spatial index strategies, if they
// have to be done in the future.
class SpatialIndex<T>(val geometry: WorldGeometry) {
private val lock = ReentrantLock()
private val map = Long2ObjectOpenHashMap<Sector>()
private val factory = Long2ObjectFunction { Sector(it) }
private val counter = AtomicInteger()
private fun index(x: Int, y: Int): Long {
require(x in 0 .. Int.MAX_VALUE) { "x out of range: $x" }
require(y in 0 .. Int.MAX_VALUE) { "y out of range: $y" }
return (x.toLong() shl 32) or y.toLong()
}
private fun values(index: Long): Pair<Int, Int> {
return index.ushr(32).toInt() to index.toInt()
}
private inner class Sector(val index: Long) : Comparable<Sector> {
val entries = ObjectAVLTreeSet<Entry>()
override fun compareTo(other: Sector): Int {
return index.compareTo(other.index)
}
fun add(entry: Entry) {
entries.add(entry)
}
fun remove(entry: Entry) {
check(entries.remove(entry)) { "Expected to remove spatial entry from spatial sector" }
if (entries.isEmpty()) {
check(map.remove(index) == this) { "Tried to remove self from spatial index, but removed something else!" }
}
}
}
inner class Entry(val value: T) : Comparable<Entry> {
private val sectors = Object2IntAVLTreeMap<Sector>()
private val id = counter.getAndIncrement()
private val fixtures = ArrayList<Fixture>(1)
// default fixture since in most cases it should be enough
val fixture = Fixture()
fun intersects(rect: AABB, withEdges: Boolean): Boolean {
return fixtures.any { it.intersects(rect, withEdges) }
}
override fun compareTo(other: Entry): Int {
return id.compareTo(other.id)
}
private fun ref(sector: Sector) {
val new = sectors.getInt(sector) + 1
if (new == 1) {
sector.add(this)
}
check(new >= 1)
sectors.put(sector, new)
}
private fun deref(sector: Sector) {
val result = sectors.getInt(sector) - 1
if (result <= 0) {
check(result == 0)
sectors.removeInt(sector)
sector.remove(this)
} else {
sectors.put(sector, result)
}
}
inner class Fixture {
init {
lock.withLock {
fixtures.add(this)
}
}
private var isRemoved = false
private val sectors = ObjectAVLTreeSet<Sector>()
private var boxes: List<AABB> = listOf()
fun intersects(aabb: AABB, withEdges: Boolean): Boolean {
return boxes.any { if (withEdges) aabb.intersect(it) else aabb.intersectWeak(it) }
}
fun move(aabb: AABB) {
if (isRemoved) return
val newSectors = LongArrayList()
val split = geometry.split(aabb).first
boxes = split
for (actualRegion in split) {
val xMin = geometry.x.chunkFromCell(actualRegion.mins.x)
val xMax = geometry.x.chunkFromCell(actualRegion.maxs.x)
val yMin = geometry.y.chunkFromCell(actualRegion.mins.y)
val yMax = geometry.y.chunkFromCell(actualRegion.maxs.y)
for (x in xMin .. xMax) {
for (y in yMin .. yMax) {
newSectors.add(index(x, y))
}
}
}
if (newSectors.isEmpty) {
// how?
clear()
} else {
val newSectors0 = newSectors.toLongArray()
newSectors0.sort()
lock.withLock {
if (isRemoved) return
val addSectors = ArrayList<Sector>(0)
val existingItr = sectors.iterator()
val newItr = newSectors0.iterator()
var existing = if (existingItr.hasNext()) existingItr.next() else null
var sector = newItr.nextLong()
while (true) {
if (existing != null && existing.index == sector) {
// everything is correct
existing = if (existingItr.hasNext()) existingItr.next() else null
if (!newItr.hasNext()) {
break
} else {
var nextSector = newItr.nextLong()
while (nextSector == sector && newItr.hasNext())
nextSector = newItr.nextLong()
if (nextSector != sector)
sector = nextSector
else
break
}
continue
}
if (existing == null || existing.index > sector) {
// we started to reference new sector
val get = map.computeIfAbsent(sector, factory)
addSectors.add(get)
ref(get)
// println("Entered sector ${values(sector)}")
if (!newItr.hasNext()) {
break
} else {
var nextSector = newItr.nextLong()
while (nextSector == sector && newItr.hasNext())
nextSector = newItr.nextLong()
if (nextSector != sector)
sector = nextSector
else
break
}
continue
}
// `existing` references sector which is no longer valid for this fixture
deref(existing)
existingItr.remove()
// println("Left sector ${values(existing.index)}")
existing = if (existingItr.hasNext()) existingItr.next() else null
}
while (existing != null || existingItr.hasNext()) {
if (existing == null)
existing = existingItr.next()
if (existing!!.index > newSectors0.last()) {
// println("Left sector ${values(existing.index)}")
existingItr.remove()
}
existing = null
}
addSectors.forEach { sectors.add(it) }
}
}
}
fun clear() {
lock.withLock {
sectors.forEach { deref(it) }
sectors.clear()
}
}
fun remove() {
lock.withLock {
isRemoved = true
clear()
fixtures.remove(this)
}
}
}
fun remove() {
lock.withLock {
fixtures.forEach { it.clear() }
sectors.keys.forEach { it.remove(this) }
sectors.clear()
}
}
}
/**
* [filter] might be invoked for same entry multiple times, regardless pf [distinct]
*/
fun query(rect: AABBi, filter: Predicate<T> = Predicate { true }, distinct: Boolean = true, withEdges: Boolean = true): List<T> {
return query(rect.toDoubleAABB(), filter, distinct, withEdges)
}
/**
* [predicate] might be invoked for same entry multiple times, regardless pf [distinct]
*/
fun query(rect: AABB, predicate: Predicate<T> = Predicate { true }, distinct: Boolean = true, withEdges: Boolean = true): List<T> {
val entries = ArrayList<Entry>()
for (actualRegion in geometry.split(rect).first) {
val xMin = geometry.x.chunkFromCell(actualRegion.mins.x)
val xMax = geometry.x.chunkFromCell(actualRegion.maxs.x)
val yMin = geometry.y.chunkFromCell(actualRegion.mins.y)
val yMax = geometry.y.chunkFromCell(actualRegion.maxs.y)
for (x in xMin .. xMax) {
for (y in yMin .. yMax) {
val sector = map[index(x, y)] ?: continue
for (entry in sector.entries) {
if (predicate.test(entry.value) && entry.intersects(actualRegion, withEdges)) {
entries.add(entry)
}
}
}
}
}
if (distinct) {
if (entries.isEmpty())
return listOf()
entries.sort()
val entries0 = ArrayList<T>(entries.size)
var previous: Entry? = null
for (entry in entries) {
if (entry != previous) {
previous = entry
entries0.add(entry.value)
}
}
return entries0
} else {
if (entries.isEmpty())
return listOf()
return entries.map { it.value }
}
}
}

View File

@ -79,21 +79,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
operator fun get(pos: ChunkPos) = get(pos.x, pos.y) operator fun get(pos: ChunkPos) = get(pos.x, pos.y)
protected fun create(x: Int, y: Int): ChunkType { protected fun create(x: Int, y: Int): ChunkType {
val pos = ChunkPos(x, y) return chunkFactory(ChunkPos(x, y))
val chunk = chunkFactory(pos)
val orphanedInThisChunk = ArrayList<AbstractEntity>()
for (ent in orphanedEntities) {
if (ent.chunkPos == pos) {
orphanedInThisChunk.add(ent)
}
}
for (ent in orphanedInThisChunk) {
ent.chunk = chunk
}
return chunk
} }
abstract val size: Int abstract val size: Int
@ -229,10 +215,10 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
// generic lock // generic lock
val lock = ReentrantLock() val lock = ReentrantLock()
val orphanedEntities = ReferenceOpenHashSet<AbstractEntity>()
val entities = Int2ObjectOpenHashMap<AbstractEntity>() val entities = Int2ObjectOpenHashMap<AbstractEntity>()
val dynamicEntities = ReferenceOpenHashSet<DynamicEntity>() val entityIndex = SpatialIndex<AbstractEntity>(geometry)
val tileEntities = ReferenceOpenHashSet<TileEntity>() val dynamicEntities = ArrayList<DynamicEntity>()
val tileEntities = ArrayList<TileEntity>()
var playerSpawnPosition = Vector2d.ZERO var playerSpawnPosition = Vector2d.ZERO
protected set protected set
@ -271,7 +257,12 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
ticks++ ticks++
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
Starbound.EXECUTOR.submit(ParallelPerform(dynamicEntities.spliterator(), { if (!it.isRemote) it.movement.move() })).join() Starbound.EXECUTOR.submit(ParallelPerform(dynamicEntities.spliterator(), {
if (!it.isRemote) {
it.movement.move()
}
})).join()
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
entities.values.forEach { it.tick() } entities.values.forEach { it.tick() }
@ -282,20 +273,20 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
tickInner() tick0()
} catch(err: Throwable) { } catch(err: Throwable) {
throw RuntimeException("Ticking world $this", err) throw RuntimeException("Ticking world $this", err)
} }
} }
protected abstract fun tickInner() protected abstract fun tick0()
protected abstract fun chunkFactory(pos: ChunkPos): ChunkType protected abstract fun chunkFactory(pos: ChunkPos): ChunkType
override fun close() { override fun close() {
mailbox.shutdownNow() mailbox.shutdownNow()
} }
fun queryCollisions(aabb: AABB): MutableList<CollisionPoly> { fun queryTileCollisions(aabb: AABB): MutableList<CollisionPoly> {
val result = ArrayList<CollisionPoly>() val result = ArrayList<CollisionPoly>()
val tiles = aabb.encasingIntAABB() val tiles = aabb.encasingIntAABB()
@ -310,7 +301,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
fun collide(with: Poly, filter: Predicate<CollisionPoly>): Stream<Poly.Penetration> { fun collide(with: Poly, filter: Predicate<CollisionPoly>): Stream<Poly.Penetration> {
return queryCollisions(with.aabb.enlarge(1.0, 1.0)).stream() return queryTileCollisions(with.aabb.enlarge(1.0, 1.0)).stream()
.filter(filter) .filter(filter)
.map { with.intersect(it.poly) } .map { with.intersect(it.poly) }
.filterNotNull() .filterNotNull()

View File

@ -4,6 +4,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import ru.dbotthepony.kommons.io.readVector2i import ru.dbotthepony.kommons.io.readVector2i
import ru.dbotthepony.kommons.io.writeStruct2i import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2f import ru.dbotthepony.kommons.util.IStruct2f
@ -105,11 +106,49 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Bool
} }
} }
fun tileRegion2Chunks(region: AABBi): Set<ChunkPos> { /**
* TODO: While for entities it is completely legal to touch world border
* (e.g. have coordinate equal to world's size), it is not for tiles
*
* Currently, this splits rectangles with tile grid's point of view,
* where coordinate at exactly world border is treated as "wrap around"
*/
fun split(region: AABB): Pair<List<AABB>, Vector2d> {
val splitX = x.split(region.mins.x, region.maxs.x)
val splitY = y.split(region.mins.y, region.maxs.y)
val offset = Vector2d(splitX.offset, splitY.offset)
if (splitX.rangeB == null && splitY.rangeB == null) {
// confined
return listOf(AABB(Vector2d(splitX.rangeA.x, splitY.rangeA.x), Vector2d(splitX.rangeA.y, splitY.rangeA.y))) to offset
} else if (splitX.rangeB != null && splitY.rangeB == null) {
// wrapped around X axis
val a = AABB(Vector2d(splitX.rangeA.x, splitY.rangeA.x), Vector2d(splitX.rangeA.y, splitY.rangeA.y))
val b = AABB(Vector2d(splitX.rangeB.x, splitY.rangeA.x), Vector2d(splitX.rangeB.y, splitY.rangeA.y))
return listOf(a, b) to offset
} else if (splitX.rangeB == null && splitY.rangeB != null) {
// wrapped around Y axis
val a = AABB(Vector2d(splitX.rangeA.x, splitY.rangeA.x), Vector2d(splitX.rangeA.y, splitY.rangeA.y))
val b = AABB(Vector2d(splitX.rangeA.x, splitY.rangeB.x), Vector2d(splitX.rangeA.y, splitY.rangeB.y))
return listOf(a, b) to offset
} else {
// wrapped around X and Y axis
splitX.rangeB!!
splitY.rangeB!!
val a = AABB(Vector2d(splitX.rangeA.x, splitY.rangeA.x), Vector2d(splitX.rangeA.y, splitY.rangeA.y))
val b = AABB(Vector2d(splitX.rangeB.x, splitY.rangeA.x), Vector2d(splitX.rangeB.y, splitY.rangeA.y))
val c = AABB(Vector2d(splitX.rangeA.x, splitY.rangeB.x), Vector2d(splitX.rangeA.y, splitY.rangeB.y))
val d = AABB(Vector2d(splitX.rangeB.x, splitY.rangeB.x), Vector2d(splitX.rangeB.y, splitY.rangeB.y))
return listOf(a, b, c, d) to offset
}
}
fun region2Chunks(region: AABBi): Set<ChunkPos> {
if (region.mins == region.maxs) if (region.mins == region.maxs)
return emptySet() return emptySet()
val result = ObjectArraySet<ChunkPos>() val result = ObjectArrayList<ChunkPos>()
for (actualRegion in split(region).first) { for (actualRegion in split(region).first) {
val xMin = this.x.chunkFromCell(actualRegion.mins.x) val xMin = this.x.chunkFromCell(actualRegion.mins.x)
@ -125,6 +164,53 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Bool
} }
} }
return result result.sort()
val itr = result.iterator()
var previous: ChunkPos? = null
for (value in itr) {
if (value == previous)
itr.remove()
else
previous = value
}
return ObjectArraySet.ofUnchecked(*result.toTypedArray())
}
fun region2Chunks(region: AABB): Set<ChunkPos> {
if (region.mins == region.maxs)
return emptySet()
val result = ObjectArrayList<ChunkPos>()
for (actualRegion in split(region).first) {
val xMin = this.x.chunkFromCell(actualRegion.mins.x)
val xMax = this.x.chunkFromCell(actualRegion.maxs.x)
val yMin = this.y.chunkFromCell(actualRegion.mins.y)
val yMax = this.y.chunkFromCell(actualRegion.maxs.y)
for (x in xMin .. xMax) {
for (y in yMin .. yMax) {
result.add(ChunkPos(x, y))
}
}
}
result.sort()
val itr = result.iterator()
var previous: ChunkPos? = null
for (value in itr) {
if (value == previous)
itr.remove()
else
previous = value
}
return ObjectArraySet.ofUnchecked(*result.toTypedArray())
} }
} }

View File

@ -12,45 +12,13 @@ import ru.dbotthepony.kstarbound.defs.JsonDriven
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket
import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.SpatialIndex
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.function.Consumer import java.util.function.Consumer
import kotlin.concurrent.withLock
abstract class AbstractEntity(path: String) : JsonDriven(path) { abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<AbstractEntity> {
/**
* The chunk this entity resides in
*/
var chunk: Chunk<*, *>? = null
set(value) {
if (innerWorld == null) {
throw IllegalStateException("Trying to set chunk this entity belong to before spawning in world")
} else if (value != null && innerWorld != value.world) {
throw IllegalArgumentException("$this belongs to $innerWorld, $value belongs to ${value.world}")
} else if (value == field) {
return
}
val oldChunk = field
field = value
world.lock.withLock {
if (oldChunk == null && value != null) {
world.orphanedEntities.remove(this)
value.addEntity(this)
} else if (oldChunk != null && value == null) {
world.orphanedEntities.add(this)
oldChunk.removeEntity(this)
} else if (oldChunk != null && value != null) {
value.transferEntity(this, oldChunk)
}
}
}
abstract val chunkPos: ChunkPos
abstract val position: Vector2d abstract val position: Vector2d
var entityID: Int = 0 var entityID: Int = 0
@ -68,6 +36,10 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
var connectionID: Int = 0 var connectionID: Int = 0
private set private set
final override fun compareTo(other: AbstractEntity): Int {
return entityID.compareTo(other.entityID)
}
private val exceptionLogger = Consumer<Throwable> { private val exceptionLogger = Consumer<Throwable> {
LOGGER.error("Error while executing queued task on $this", it) LOGGER.error("Error while executing queued task on $this", it)
} }
@ -110,6 +82,13 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
val networkGroup = MasterElement(NetworkedGroup()) val networkGroup = MasterElement(NetworkedGroup())
abstract fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) abstract fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean)
protected var spatialEntry: SpatialIndex<AbstractEntity>.Entry? = null
private set
open fun onNetworkUpdate() {
}
fun joinWorld(world: World<*, *>) { fun joinWorld(world: World<*, *>) {
if (innerWorld != null) if (innerWorld != null)
throw IllegalStateException("Already spawned (in world $innerWorld)") throw IllegalStateException("Already spawned (in world $innerWorld)")
@ -126,7 +105,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
innerWorld = world innerWorld = world
world.entities[entityID] = this world.entities[entityID] = this
world.orphanedEntities.add(this) spatialEntry = world.entityIndex.Entry(this)
onJoinWorld(world) onJoinWorld(world)
} }
@ -135,11 +114,10 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
world.ensureSameThread() world.ensureSameThread()
mailbox.shutdownNow() mailbox.shutdownNow()
chunk = null
check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" } check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" }
world.orphanedEntities.remove(this)
onRemove(world) onRemove(world)
world.broadcast(EntityDestroyPacket(entityID, ByteArrayList(), false)) spatialEntry?.remove()
spatialEntry = null
innerWorld = null innerWorld = null
} }
@ -160,7 +138,9 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
} }
protected open fun tickRemote() { protected open fun tickRemote() {
networkGroup.upstream.tickInterpolation(Starbound.TIMESTEP) if (networkGroup.upstream.isInterpolating) {
networkGroup.upstream.tickInterpolation(Starbound.TIMESTEP)
}
} }
protected open fun tickLocal() { protected open fun tickLocal() {

View File

@ -21,7 +21,7 @@ import kotlin.math.PI
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.sign import kotlin.math.sign
class ActorMovementController : MovementController() { class ActorMovementController() : MovementController() {
var controlRun: Boolean = false var controlRun: Boolean = false
var controlCrouch: Boolean = false var controlCrouch: Boolean = false
var controlDown: Boolean = false var controlDown: Boolean = false

View File

@ -2,13 +2,11 @@ package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.RenderLayer import ru.dbotthepony.kstarbound.client.render.RenderLayer
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.SpatialIndex
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import java.util.function.Consumer
/** /**
* Entities with dynamics (Player, Drops, Projectiles, NPCs, etc) * Entities with dynamics (Player, Drops, Projectiles, NPCs, etc)
@ -24,29 +22,28 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) {
abstract val movement: MovementController abstract val movement: MovementController
final override var chunkPos: ChunkPos = ChunkPos.ZERO override fun onNetworkUpdate() {
private set super.onNetworkUpdate()
movement.updateFixtures()
}
override fun tickRemote() {
super.tickRemote()
if (networkGroup.upstream.isInterpolating) {
movement.updateFixtures()
}
}
override fun onJoinWorld(world: World<*, *>) { override fun onJoinWorld(world: World<*, *>) {
world.dynamicEntities.add(this) world.dynamicEntities.add(this)
movement.world = world movement.initialize(world, spatialEntry)
forceChunkRepos = true forceChunkRepos = true
/*movement.positionListeners.addListener(Consumer { field ->
val oldChunkPos = world.geometry.chunkFromCell(old)
val newChunkPos = world.geometry.chunkFromCell(field)
chunkPos = newChunkPos
if (oldChunkPos != newChunkPos || forceChunkRepos) {
chunk = world.chunkMap[newChunkPos]
forceChunkRepos = false
}
})*/
} }
override fun onRemove(world: World<*, *>) { override fun onRemove(world: World<*, *>) {
world.dynamicEntities.remove(this) world.dynamicEntities.remove(this)
movement.remove()
} }
override fun render(client: StarboundClient, layers: LayeredRenderer) { override fun render(client: StarboundClient, layers: LayeredRenderer) {
@ -56,7 +53,7 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) {
hitboxes.forEach { it.render(client) } hitboxes.forEach { it.render(client) }
world.queryCollisions( world.queryTileCollisions(
hitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().enlarge(2.0, 2.0) hitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().enlarge(2.0, 2.0)
).filter(movement::shouldCollideWithBody).forEach { it.poly.render(client, BLOCK_COLLISION_COLOR) } ).filter(movement::shouldCollideWithBody).forEach { it.poly.render(client, BLOCK_COLLISION_COLOR) }
} }

View File

@ -22,6 +22,7 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedPoly import ru.dbotthepony.kstarbound.network.syncher.networkedPoly
import ru.dbotthepony.kstarbound.world.SpatialIndex
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
@ -32,10 +33,25 @@ import kotlin.math.absoluteValue
import kotlin.math.acos import kotlin.math.acos
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
import kotlin.properties.Delegates
open class MovementController() { open class MovementController() {
var world: World<*, *> by Delegates.notNull() private var world0: World<*, *>? = null
val world: World<*, *> get() = world0!!
private var spatialEntry: SpatialIndex<*>.Entry? = null
fun initialize(world: World<*, *>, entry: SpatialIndex<*>.Entry?) {
fixtures.clear()
spatialEntry?.remove()
this.world0 = world
spatialEntry = entry
}
fun remove() {
fixtures.clear()
spatialEntry?.remove()
spatialEntry = null
world0 = null
}
val localHitboxes: Stream<Poly> val localHitboxes: Stream<Poly>
get() { return (movementParameters.collisionPoly?.map({ Stream.of(it) }, { it.stream() }) ?: return Stream.of()).map { it.rotate(rotation) + position } } get() { return (movementParameters.collisionPoly?.map({ Stream.of(it) }, { it.stream() }) ?: return Stream.of()).map { it.rotate(rotation) + position } }
@ -79,11 +95,34 @@ open class MovementController() {
private val relativeXSurfaceVelocity = networkGroup.add(networkedFixedPoint(0.0125).also { it.interpolator = Interpolator.Linear }) private val relativeXSurfaceVelocity = networkGroup.add(networkedFixedPoint(0.0125).also { it.interpolator = Interpolator.Linear })
private val relativeYSurfaceVelocity = networkGroup.add(networkedFixedPoint(0.0125).also { it.interpolator = Interpolator.Linear }) private val relativeYSurfaceVelocity = networkGroup.add(networkedFixedPoint(0.0125).also { it.interpolator = Interpolator.Linear })
private val fixtures = ArrayList<SpatialIndex<*>.Entry.Fixture>()
var fixturesChangeset: Int = 0
private set
fun updateFixtures() {
val spatialEntry = spatialEntry ?: return
fixturesChangeset++
val localHitboxes = localHitboxes.toList()
while (fixtures.size > localHitboxes.size) {
fixtures.last().remove()
}
while (fixtures.size < localHitboxes.size) {
fixtures.add(spatialEntry.Fixture())
}
for ((i, hitbox) in localHitboxes.withIndex()) {
fixtures[i].move(hitbox.aabb)
}
}
var position: Vector2d var position: Vector2d
get() = Vector2d(xPosition, yPosition) get() = Vector2d(xPosition, yPosition)
set(value) { set(value) {
xPosition = value.x xPosition = value.x
yPosition = value.y yPosition = value.y
updateFixtures()
} }
val positionListeners = Listenable.Impl<Vector2d>() val positionListeners = Listenable.Impl<Vector2d>()
@ -233,7 +272,7 @@ open class MovementController() {
var queryBounds = aabb.enlarge(maximumCorrection, maximumCorrection) var queryBounds = aabb.enlarge(maximumCorrection, maximumCorrection)
queryBounds = queryBounds.combine(queryBounds + movement) queryBounds = queryBounds.combine(queryBounds + movement)
val polies = world.queryCollisions(queryBounds).filter(this::shouldCollideWithBody) val polies = world.queryTileCollisions(queryBounds).filter(this::shouldCollideWithBody)
val results = ArrayList<CollisionResult>(localHitboxes.size) val results = ArrayList<CollisionResult>(localHitboxes.size)

View File

@ -4,30 +4,18 @@ import com.google.gson.JsonObject
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.SpatialIndex
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
/** /**
* (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid * (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid
*/ */
abstract class TileEntity(path: String) : AbstractEntity(path) { abstract class TileEntity(path: String) : AbstractEntity(path) {
private var forceChunkRepos = false
var tilePosition = Vector2i() var tilePosition = Vector2i()
set(value) { set(value) {
val old = field
if (isSpawned) { if (isSpawned) {
field = world.geometry.wrap(value) field = world.geometry.wrap(value)
// spatialEntry?.fixture?.move()
val oldChunkPos = world.geometry.chunkFromCell(old)
val newChunkPos = world.geometry.chunkFromCell(field)
chunkPos = newChunkPos
if (oldChunkPos != newChunkPos || forceChunkRepos) {
chunk = world.chunkMap[newChunkPos]
forceChunkRepos = false
}
} else { } else {
field = value field = value
} }
@ -36,12 +24,8 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
override val position: Vector2d override val position: Vector2d
get() = tilePosition.toDoubleVector() get() = tilePosition.toDoubleVector()
final override var chunkPos: ChunkPos = ChunkPos.ZERO
private set
override fun onJoinWorld(world: World<*, *>) { override fun onJoinWorld(world: World<*, *>) {
world.tileEntities.add(this) world.tileEntities.add(this)
forceChunkRepos = true
tilePosition = tilePosition tilePosition = tilePosition
} }

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.world.entities.player
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.bytes.ByteArrayList
import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
@ -24,6 +25,9 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedEnumExtraStupid
import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter
import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint
import ru.dbotthepony.kstarbound.network.syncher.networkedString import ru.dbotthepony.kstarbound.network.syncher.networkedString
import ru.dbotthepony.kstarbound.world.SpatialIndex
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.Animator import ru.dbotthepony.kstarbound.world.entities.Animator
import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity
import ru.dbotthepony.kstarbound.world.entities.StatusController import ru.dbotthepony.kstarbound.world.entities.StatusController
@ -95,6 +99,29 @@ class PlayerEntity() : HumanoidActorEntity("/") {
networkGroup.upstream.add(techController.networkGroup) networkGroup.upstream.add(techController.networkGroup)
} }
private var fixturesChangeset = -1
private var metaFixture: SpatialIndex<AbstractEntity>.Entry.Fixture? = null
override fun onJoinWorld(world: World<*, *>) {
super.onJoinWorld(world)
metaFixture = spatialEntry!!.Fixture()
}
override fun onRemove(world: World<*, *>) {
super.onRemove(world)
metaFixture?.remove()
metaFixture = null
}
override fun tickShared() {
super.tickShared()
if (fixturesChangeset != movement.fixturesChangeset) {
fixturesChangeset = movement.fixturesChangeset
metaFixture!!.move(GlobalDefaults.player.metaBoundBox + position)
}
}
override val aimPosition: Vector2d override val aimPosition: Vector2d
get() = Vector2d(xAimPosition, yAimPosition) get() = Vector2d(xAimPosition, yAimPosition)

View File

@ -1,9 +1,14 @@
package ru.dbotthepony.kstarbound.test package ru.dbotthepony.kstarbound.test
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.world.SpatialIndex
import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.WorldGeometry
object WorldTests { object WorldTests {
@ -32,4 +37,118 @@ object WorldTests {
} }
} }
} }
@Test
@DisplayName("Spatial index test")
fun spatialIndex() {
val geometry = WorldGeometry(Vector2i(3000, 2000), true, false)
val index = SpatialIndex<Int>(geometry)
val entry = index.Entry(0)
// simple
entry.fixture.move(AABB(
Vector2d(1.0, 1.0),
Vector2d(4.0, 4.0),
))
assertTrue(index.query(AABB(
Vector2d(14.0, 14.0),
Vector2d(15.0, 17.0),
)).isEmpty())
assertTrue(index.query(AABB(
Vector2d(14.0, 14.0),
Vector2d(15.0, 34.0),
)).isEmpty())
assertFalse(index.query(AABB(
Vector2d(0.0, 0.5),
Vector2d(1.5, 1.5),
)).isEmpty())
// move
entry.fixture.move(AABB(
Vector2d(14.0, 14.0),
Vector2d(15.0, 17.0),
))
assertTrue(index.query(AABB(
Vector2d(0.0, 0.5),
Vector2d(1.5, 1.5),
)).isEmpty())
assertFalse(index.query(AABB(
Vector2d(13.0, 13.0),
Vector2d(15.0, 15.0),
)).isEmpty())
// move to edge of two sectors
entry.fixture.move(AABB(
Vector2d(30.0, 30.0),
Vector2d(40.0, 30.0),
))
assertTrue(index.query(AABB(
Vector2d(13.0, 13.0),
Vector2d(15.0, 15.0),
)).isEmpty())
assertFalse(index.query(AABB(
Vector2d(31.0, 30.0),
Vector2d(32.0, 31.0),
)).isEmpty())
assertFalse(index.query(AABB(
Vector2d(38.0, 30.0),
Vector2d(39.0, 31.0),
)).isEmpty())
// move to edge of four sectors
entry.fixture.move(AABB(
Vector2d(30.0, 30.0),
Vector2d(40.0, 40.0),
))
assertFalse(index.query(AABB(
Vector2d(38.0, 30.0),
Vector2d(39.0, 31.0),
)).isEmpty())
assertFalse(index.query(AABB(
Vector2d(38.0, 33.0),
Vector2d(39.0, 34.0),
)).isEmpty())
assertFalse(index.query(AABB(
Vector2d(30.0, 33.0),
Vector2d(31.0, 34.0),
)).isEmpty())
// move further, trigger sector "removal"
entry.fixture.move(AABB(
Vector2d(60.0, 30.0),
Vector2d(70.0, 40.0),
))
assertTrue(index.query(AABB(
Vector2d(30.0, 33.0),
Vector2d(31.0, 34.0),
)).isEmpty())
assertTrue(index.query(AABB(
Vector2d(50.0, 33.0),
Vector2d(55.0, 34.0),
)).isEmpty())
assertFalse(index.query(AABB(
Vector2d(61.0, 33.0),
Vector2d(62.0, 34.0),
)).isEmpty())
assertFalse(index.query(AABB(
Vector2d(65.0, 33.0),
Vector2d(66.0, 34.0),
)).isEmpty())
}
} }