From 538f8a9b72e6db3ff2bef132e5d33f38532b5ed9 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Tue, 5 Sep 2023 21:12:23 +0700 Subject: [PATCH] Undo dynamic chunk dimensions, add render regions instead for solid grid rendering --- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 5 +- .../kstarbound/client/ClientChunk.kt | 42 ++-- .../kstarbound/client/ClientWorld.kt | 226 +++++++++++++++--- .../kstarbound/client/StarboundClient.kt | 2 +- .../kstarbound/client/render/TileRenderer.kt | 6 +- .../ru/dbotthepony/kstarbound/math/Utils.kt | 8 + .../ru/dbotthepony/kstarbound/world/Chunk.kt | 14 +- .../dbotthepony/kstarbound/world/ChunkPos.kt | 6 +- .../kstarbound/world/CoordinateMapper.kt | 45 ++-- .../ru/dbotthepony/kstarbound/world/World.kt | 166 ++++++------- .../kstarbound/world/entities/Entity.kt | 10 +- 11 files changed, 347 insertions(+), 183 deletions(-) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 17c2e0e6..ccf71e29 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -12,7 +12,6 @@ import ru.dbotthepony.kstarbound.player.Avatar import ru.dbotthepony.kstarbound.player.QuestDescriptor import ru.dbotthepony.kstarbound.player.QuestInstance import ru.dbotthepony.kstarbound.util.JVMTimeSource -import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kstarbound.world.entities.ItemEntity import ru.dbotthepony.kstarbound.world.entities.PlayerEntity @@ -90,7 +89,7 @@ fun main() { for (y in 0 .. 31) { for (x in 0 .. 31) { - val cell = client.world!!.chunkMap.getCellDirect(chunkX * 32 + x, chunkY * 32 + y) + val cell = client.world!!.getCellDirect(chunkX * 32 + x, chunkY * 32 + y) if (cell == null) { IChunkCell.skip(reader) @@ -160,7 +159,7 @@ fun main() { client.gl.font.render("${ent.position}", y = 100f, scale = 0.25f) client.gl.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f) client.gl.font.render("Camera: ${client.camera.pos} ${client.settings.zoom}", y = 140f, scale = 0.25f) - client.gl.font.render("World chunk: ${client.world!!.chunkMap.cellToChunk(client.camera.pos.toDoubleVector())}", y = 160f, scale = 0.25f) + client.gl.font.render("World chunk: ${client.world!!.chunkFromCell(client.camera.pos.toDoubleVector())}", y = 160f, scale = 0.25f) } client.onPreDrawWorld { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt index e990bfeb..7008cb27 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt @@ -26,7 +26,7 @@ import java.util.LinkedList const val Z_LEVEL_BACKGROUND = 60000 const val Z_LEVEL_LIQUID = 10000 -class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) : Chunk(world, pos, width, height), Closeable { +class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk(world, pos), Closeable { val state: GLStateTracker get() = world.client.gl private inner class TileLayerRenderer(private val view: ITileAccess, private val isBackground: Boolean) : AutoCloseable { @@ -48,8 +48,9 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) : layers.clear() - for (x in 0 until width) { - for (y in 0 until height) { + for (x in 0 until CHUNK_SIZE) { + for (y in 0 until CHUNK_SIZE) { + if (!world.inBounds(x, y)) continue val tile = view.getTile(x, y) ?: continue val material = tile.material @@ -97,31 +98,31 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) : override fun foregroundChanges() { super.foregroundChanges() - foregroundRenderer.isDirty = true + world.forEachRenderRegion(pos) { + it.foreground.isDirty = true + } forEachNeighbour { - it.foregroundRenderer.isDirty = true + world.forEachRenderRegion(it.pos) { + it.foreground.isDirty = true + } } } override fun backgroundChanges() { super.backgroundChanges() - backgroundRenderer.isDirty = true + world.forEachRenderRegion(pos) { + it.background.isDirty = true + } forEachNeighbour { - it.backgroundRenderer.isDirty = true + world.forEachRenderRegion(it.pos) { + it.background.isDirty = true + } } } - /** - * Тесселирует "статичную" геометрию в builders (к примеру тайлы), с проверкой, изменилось ли что либо, - * и загружает её в видеопамять. - * - * Может быть вызван вне рендер потока (ибо в любом случае он требует некой "стаитичности" данных в чанке) - * но только если до этого был вызыван loadRenderers() и геометрия чанка не поменялась - * - */ fun bake() { backgroundRenderer.bake() foregroundRenderer.bake() @@ -130,9 +131,6 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) : upload() } - /** - * Загружает в видеопамять всю геометрию напрямую, если есть что загружать - */ fun upload() { backgroundRenderer.upload() foregroundRenderer.upload() @@ -146,8 +144,8 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) : liquidTypes.clear() liquidTypesVer = liquidChangeset - for (x in 0 until width) { - for (y in 0 until height) { + for (x in 0 until CHUNK_SIZE) { + for (y in 0 until CHUNK_SIZE) { getCell(x, y).liquid.def?.let { liquidTypes.add(it) } } } @@ -199,8 +197,8 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) : for (type in types) { builder.builder.begin() - for (x in 0 until width) { - for (y in 0 until height) { + for (x in 0 until CHUNK_SIZE) { + for (y in 0 until CHUNK_SIZE) { val state = getCell(x, y) if (state.liquid.def === type) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt index 0cf479a7..3ef6c57f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt @@ -1,14 +1,26 @@ package ru.dbotthepony.kstarbound.client +import it.unimi.dsi.fastutil.longs.Long2ObjectFunction +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap +import ru.dbotthepony.kstarbound.client.render.ConfiguredStaticMesh import ru.dbotthepony.kstarbound.client.render.LayeredRenderer -import ru.dbotthepony.kstarbound.math.encasingIntAABB +import ru.dbotthepony.kstarbound.client.render.TileLayerList import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity +import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity import ru.dbotthepony.kstarbound.world.* +import ru.dbotthepony.kstarbound.world.api.ITileAccess +import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess +import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kvector.util2d.AABB +import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2f import ru.dbotthepony.kvector.vector.Vector2i -import kotlin.math.roundToInt +import java.util.function.Function +import kotlin.collections.ArrayList +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin class ClientWorld( val client: StarboundClient, @@ -21,31 +33,186 @@ class ClientWorld( physics.debugDraw = client.gl.box2dRenderer } + private fun determineChunkSize(cells: Int): Int { + for (i in 16 downTo 1) { + if (cells % i == 0) { + return i + } + } + + throw RuntimeException("unreachable code") + } + + val renderRegionWidth = if (size == null) 16 else determineChunkSize(size.x) + val renderRegionHeight = if (size == null) 16 else determineChunkSize(size.y) + + inner class RenderRegion(val x: Int, val y: Int) { + inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) { + private val state get() = client.gl + private val layers = TileLayerList() + val bakedMeshes = ArrayList>() + var isDirty = true + + fun bake() { + if (!isDirty) return + isDirty = false + + if (state.isSameThread()) { + for (mesh in bakedMeshes) { + mesh.first.close() + } + } + + bakedMeshes.clear() + + layers.clear() + + for (x in 0 until renderRegionWidth) { + for (y in 0 until renderRegionHeight) { + if (!inBounds(x, y)) continue + + val tile = view.getTile(x, y) ?: continue + val material = tile.material + + if (material != null) { + client.tileRenderers.getTileRenderer(material.materialName).tesselate(tile, view, layers, Vector2i(x, y), background = isBackground) + } + + val modifier = tile.modifier + + if (modifier != null) { + client.tileRenderers.getModifierRenderer(modifier.modName).tesselate(tile, view, layers, Vector2i(x, y), background = isBackground, isModifier = true) + } + } + } + + if (layers.isNotEmpty) { + for (mesh in bakedMeshes) { + mesh.first.close() + } + + bakedMeshes.clear() + + for ((baked, builder, zLevel) in layers.buildSortedLayerList()) { + bakedMeshes.add(ConfiguredStaticMesh(baked, builder) to zLevel) + } + + layers.clear() + } + } + } + + val view = OffsetCellAccess(this@ClientWorld, x * renderRegionWidth, y * renderRegionHeight) + + val background = Layer(TileView.Background(view), true) + val foreground = Layer(TileView.Foreground(view), false) + + fun addLayers(layers: LayeredRenderer, renderOrigin: Vector2f) { + background.bake() + foreground.bake() + + for ((baked, zLevel) in background.bakedMeshes) { + layers.add(zLevel + Z_LEVEL_BACKGROUND) { + it.push().last().translateWithMultiplication(renderOrigin.x, renderOrigin.y) + baked.renderStacked(it) + it.pop() + } + } + + for ((baked, zLevel) in foreground.bakedMeshes) { + layers.add(zLevel) { + it.push().last().translateWithMultiplication(renderOrigin.x, renderOrigin.y) + baked.renderStacked(it) + it.pop() + } + } + + /*for (renderer in entityRenderers.values) { + layers.add(renderer.layer) { + val relative = renderer.renderPos - posVector2d + it.push().last().translateWithMultiplication(renderOrigin.x + relative.x.toFloat(), renderOrigin.y + relative.y.toFloat()) + renderer.render(it) + it.pop() + } + }*/ + + /*val types = getLiquidTypes() + + if (types.isNotEmpty()) { + layers.add(Z_LEVEL_LIQUID) { + it.push().last().translateWithMultiplication(renderOrigin.x, renderOrigin.y) + + val program = state.programs.liquid + + program.use() + program.transform = it.last() + + val builder = program.builder + + for (type in types) { + builder.builder.begin() + + for (x in 0 until CHUNK_SIZE) { + for (y in 0 until CHUNK_SIZE) { + val state = getCell(x, y) + + if (state.liquid.def === type) { + builder.builder.quad(x.toFloat(), y.toFloat(), x + 1f, y + state.liquid.level) + } + } + } + + program.baselineColor = type.color + + builder.upload() + builder.draw() + } + + it.pop() + } + }*/ + } + } + + val renderRegions = Long2ObjectOpenHashMap() + + inline fun forEachRenderRegion(pos: ChunkPos, action: (RenderRegion) -> Unit) { + var (ix, iy) = pos.tile + ix /= renderRegionWidth + iy /= renderRegionHeight + + for (x in ix .. ix + CHUNK_SIZE / renderRegionWidth) { + for (y in iy .. iy + CHUNK_SIZE / renderRegionWidth) { + renderRegions[x.toLong() shl 32 or y.toLong()]?.let(action) + } + } + } + override fun chunkFactory(pos: ChunkPos): ClientChunk { - return ClientChunk(this, pos, chunkWidth, chunkHeight) + return ClientChunk(this, pos) } fun addLayers( size: AABB, layers: LayeredRenderer ) { - val rx = roundTowardsNegativeInfinity(size.mins.x) / chunkWidth - 1 - val ry = roundTowardsNegativeInfinity(size.mins.y) / chunkHeight - 1 + val rx = roundTowardsNegativeInfinity(size.mins.x) / renderRegionWidth - 1 + val ry = roundTowardsNegativeInfinity(size.mins.y) / renderRegionHeight - 1 - val dx = roundTowardsNegativeInfinity(size.maxs.x - size.mins.x) / chunkWidth + 2 - val dy = roundTowardsNegativeInfinity(size.maxs.y - size.mins.y) / chunkWidth + 2 + val dx = roundTowardsPositiveInfinity(size.maxs.x - size.mins.x) / renderRegionWidth + 2 + val dy = roundTowardsPositiveInfinity(size.maxs.y - size.mins.y) / renderRegionHeight + 2 for (x in rx .. rx + dx) { for (y in ry .. ry + dy) { - val chunk = chunkMap[x, y] ?: continue - val renderer = chunk.Renderer(Vector2f(x * chunkWidth.toFloat(), y * chunkHeight.toFloat())) + val renderer = renderRegions.computeIfAbsent(x.toLong() shl 32 or y.toLong(), Long2ObjectFunction { + RenderRegion((it ushr 32).toInt(), it.toInt()) + }) - renderer.addLayers(layers) - chunk.bake() + renderer.addLayers(layers, Vector2f(x * renderRegionWidth.toFloat(), y * renderRegionHeight.toFloat())) } } - val pos = client.screenToWorld(client.mouseCoordinatesF) + val pos = client.screenToWorld(client.mouseCoordinatesF).toDoubleVector() /*layers.add(-999999) { val lightsize = 16 @@ -65,29 +232,30 @@ class ClientWorld( } }*/ - /* - val rayFan = ArrayList() - for (i in 0 .. 359) { - rayFan.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI))) - } + layers.add(-999999) { + val rayFan = ArrayList() - for (ray in rayFan) { - val trace = castRayNaive(pos, ray, 16.0) + for (i in 0 .. 359) { + rayFan.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI))) + } - client.gl.quadWireframe { - for ((tpos, tile) in trace.traversedTiles) { - if (tile.material != null) - it.quad( - tpos.x.toFloat(), - tpos.y.toFloat(), - tpos.x + 1f, - tpos.y + 1f - ) + for (ray in rayFan) { + val trace = castRayNaive(pos, ray, 16.0) + + client.gl.quadWireframe { + for ((tpos, tile) in trace.traversedTiles) { + if (tile.foreground.material != null) + it.quad( + tpos.x.toFloat(), + tpos.y.toFloat(), + tpos.x + 1f, + tpos.y + 1f + ) + } } } } - */ //rayLightCircleNaive(pos, 48.0, falloffByTravel = 1.0, falloffByTile = 3.0) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index a533de5d..0d347088 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -227,7 +227,7 @@ class StarboundClient(val starbound: Starbound) : Closeable { val tileRenderers = TileRenderers(this) - var world: ClientWorld? = ClientWorld(this, 0L, null, true) + var world: ClientWorld? = ClientWorld(this, 0L, Vector2i(3000, 2000), true) init { putDebugLog("Initialized OpenGL context") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt index ad3ebf8d..daa2dd1d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt @@ -19,8 +19,8 @@ import ru.dbotthepony.kvector.vector.Vector2i import kotlin.collections.HashMap data class TileLayer( - val bakedProgramState: ConfiguredShaderProgram, - val vertexBuilder: VertexBuilder, + val program: ConfiguredShaderProgram, + val vertices: VertexBuilder, val zPos: Int ) @@ -35,7 +35,7 @@ class TileLayerList { fun computeIfAbsent(program: ConfiguredShaderProgram, zLevel: Int, compute: () -> VertexBuilder): VertexBuilder { return layers.computeIfAbsent(program) { Int2ObjectAVLTreeMap() }.computeIfAbsent(zLevel, Int2ObjectFunction { return@Int2ObjectFunction TileLayer(program, compute.invoke(), zLevel) - }).vertexBuilder + }).vertices } fun buildSortedLayerList(): List { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt index 4288a97e..3d4f5a04 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt @@ -58,6 +58,14 @@ fun roundTowardsPositiveInfinity(value: Double): Int { return value.toInt() } +fun divideUp(a: Int, b: Int): Int { + return if (a % b == 0) { + a / b + } else { + a / b + 1 + } +} + private const val EPSILON = 0.00000001 fun weakCompare(a: Double, b: Double, epsilon: Double = EPSILON): Int { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 5e1d8105..b79361eb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -42,8 +42,6 @@ import kotlin.collections.HashSet abstract class Chunk, This : Chunk>( val world: WorldType, val pos: ChunkPos, - val width: Int, - val height: Int ) : ICellAccess { var changeset = 0 private set(value) { @@ -72,7 +70,7 @@ abstract class Chunk, This : Chunk(width, height) + Object2DArray.nulls(CHUNK_SIZE, CHUNK_SIZE) } override fun getCell(x: Int, y: Int): IChunkCell { @@ -91,7 +89,7 @@ abstract class Chunk, This : Chunk, This : Chunk, This : Chunk, This : Chunk() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt index 2731c3ce..837ebd95 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt @@ -29,9 +29,9 @@ private fun circulate(value: Int, bounds: Int): Int { data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable { constructor(pos: IStruct2i) : this(pos.component1(), pos.component2()) - fun tile(width: Int, height: Int): Vector2i { - return Vector2i(x * width, y * height) - } + val tileX = x shl CHUNK_SIZE_BITS + val tileY = y shl CHUNK_SIZE_BITS + val tile = Vector2i(tileX, tileY) val top: ChunkPos get() { return ChunkPos(x, y + 1) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt index ccac407d..9ba4a7d6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt @@ -1,5 +1,7 @@ package ru.dbotthepony.kstarbound.world +import ru.dbotthepony.kstarbound.math.divideUp + abstract class CoordinateMapper { protected fun positiveModulo(a: Int, b: Int): Int { val result = a % b @@ -16,16 +18,16 @@ abstract class CoordinateMapper { return if (result < 0f) result + b else result } + abstract val chunks: Int + abstract fun cell(value: Int): Int abstract fun cell(value: Double): Double abstract fun cell(value: Float): Float abstract fun chunk(value: Int): Int - abstract fun cellToChunk(value: Int): Int - fun cellToChunk(value: Float): Int = cellToChunk(value.toInt()) - fun cellToChunk(value: Double): Int = cellToChunk(value.toInt()) - - abstract fun cellModulus(value: Int): Int + abstract fun chunkFromCell(value: Int): Int + fun chunkFromCell(value: Float): Int = chunkFromCell(value.toInt()) + fun chunkFromCell(value: Double): Int = chunkFromCell(value.toInt()) // inside world bounds abstract fun inBoundsCell(value: Int): Boolean @@ -36,25 +38,24 @@ abstract class CoordinateMapper { open fun isValidChunkIndex(value: Int): Boolean = inBoundsChunk(value) object Infinite : CoordinateMapper() { + override val chunks: Int + get() = Int.MAX_VALUE + override fun cell(value: Int): Int = value override fun cell(value: Double): Double = value override fun cell(value: Float): Float = value override fun chunk(value: Int): Int = value - override fun cellToChunk(value: Int): Int { + override fun chunkFromCell(value: Int): Int { return value shr CHUNK_SIZE_BITS } - override fun cellModulus(value: Int): Int { - return value and CHUNK_SIZE_MASK - } - override fun inBoundsCell(value: Int) = true override fun inBoundsChunk(value: Int) = true } - class Wrapper(private val cells: Int, private val chunkSize: Int) : CoordinateMapper() { - private val chunks = cells / chunkSize + class Wrapper(private val cells: Int) : CoordinateMapper() { + override val chunks = divideUp(cells, CHUNK_SIZE) override fun inBoundsCell(value: Int) = value in 0 until cells override fun inBoundsChunk(value: Int) = value in 0 until chunks @@ -62,12 +63,8 @@ abstract class CoordinateMapper { override fun isValidCellIndex(value: Int) = true override fun isValidChunkIndex(value: Int) = true - override fun cellToChunk(value: Int): Int { - return chunk(value / chunkSize) - } - - override fun cellModulus(value: Int): Int { - return positiveModulo(value, chunkSize) + override fun chunkFromCell(value: Int): Int { + return chunk(value shr CHUNK_SIZE_BITS) } override fun cell(value: Int): Int = positiveModulo(value, cells) @@ -76,19 +73,15 @@ abstract class CoordinateMapper { override fun chunk(value: Int): Int = positiveModulo(value, chunks) } - class Clamper(private val cells: Int, private val chunkSize: Int) : CoordinateMapper() { - private val chunks = cells / chunkSize + class Clamper(private val cells: Int) : CoordinateMapper() { + override val chunks = divideUp(cells, CHUNK_SIZE) override fun inBoundsCell(value: Int): Boolean { return value in 0 until cells } - override fun cellModulus(value: Int): Int { - return positiveModulo(value, chunkSize) - } - - override fun cellToChunk(value: Int): Int { - return chunk(value / chunkSize) + override fun chunkFromCell(value: Int): Int { + return chunk(value shr CHUNK_SIZE_BITS) } override fun inBoundsChunk(value: Int): Boolean { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index b7fe12e4..9b84a1c4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -22,7 +22,6 @@ import ru.dbotthepony.kvector.api.IStruct2i import ru.dbotthepony.kvector.arrays.Int2DArray import ru.dbotthepony.kvector.arrays.Object2DArray import ru.dbotthepony.kvector.util2d.AABB -import ru.dbotthepony.kvector.util2d.AABBi import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2i import java.lang.ref.ReferenceQueue @@ -34,69 +33,55 @@ const val CHUNK_SIZE = 1 shl CHUNK_SIZE_BITS // 32 const val CHUNK_SIZE_FF = CHUNK_SIZE - 1 const val CHUNK_SIZEd = CHUNK_SIZE.toDouble() +@Suppress("UNCHECKED_CAST") abstract class World, ChunkType : Chunk>( val seed: Long, val size: Vector2i?, - val loopX: Boolean, - val loopY: Boolean -) { - private fun determineChunkSize(cells: Int): Int { - for (i in 32 downTo 1) { - if (cells % i == 0) { - return i - } - } + loopX: Boolean, + loopY: Boolean +) : ICellAccess { + val x: CoordinateMapper = if (size == null) CoordinateMapper.Infinite else if (loopX) CoordinateMapper.Wrapper(size.x) else CoordinateMapper.Clamper(size.x) + val y: CoordinateMapper = if (size == null) CoordinateMapper.Infinite else if (loopY) CoordinateMapper.Wrapper(size.y) else CoordinateMapper.Clamper(size.y) - throw RuntimeException("unreachable code") + // whenever provided cell position is within actual world borders, ignoring wrapping + fun inBounds(x: Int, y: Int) = this.x.inBoundsCell(x) && this.y.inBoundsCell(y) + fun inBounds(value: IStruct2i) = this.x.inBoundsCell(value.component1()) && this.y.inBoundsCell(value.component2()) + + fun chunkFromCell(x: Int, y: Int) = ChunkPos(this.x.chunkFromCell(x), this.y.chunkFromCell(y)) + fun chunkFromCell(x: Double, y: Double) = ChunkPos(this.x.chunkFromCell(x.toInt()), this.y.chunkFromCell(y.toInt())) + fun chunkFromCell(value: IStruct2i) = chunkFromCell(value.component1(), value.component2()) + fun chunkFromCell(value: IStruct2d) = chunkFromCell(value.component1(), value.component2()) + + val background = TileView.Background(this) + val foreground = TileView.Foreground(this) + + final override fun randomLongFor(x: Int, y: Int) = super.randomLongFor(x, y) xor seed + + override fun getCellDirect(x: Int, y: Int): IChunkCell? { + if (!this.x.inBoundsCell(x) || !this.y.inBoundsCell(y)) return null + return getCell(x, y) } - val chunkWidth = size?.x?.let(::determineChunkSize) ?: CHUNK_SIZE - val chunkHeight = size?.y?.let(::determineChunkSize) ?: CHUNK_SIZE - - abstract inner class ChunkMap : ICellAccess { - abstract val x: CoordinateMapper - abstract val y: CoordinateMapper - - val background = TileView.Background(this) - val foreground = TileView.Foreground(this) - - fun inBounds(x: Int, y: Int) = this.x.inBoundsCell(x) && this.y.inBoundsCell(y) - fun inBounds(value: IStruct2i) = this.x.inBoundsCell(value.component1()) && this.y.inBoundsCell(value.component2()) - - fun cellToChunk(x: Int, y: Int) = ChunkPos(this.x.cellToChunk(x), this.y.cellToChunk(y)) - fun cellToChunk(x: Double, y: Double) = ChunkPos(this.x.cellToChunk(x.toInt()), this.y.cellToChunk(y.toInt())) - fun cellToChunk(value: IStruct2i) = cellToChunk(value.component1(), value.component2()) - fun cellToChunk(value: IStruct2d) = cellToChunk(value.component1(), value.component2()) - - final override fun randomLongFor(x: Int, y: Int) = super.randomLongFor(x, y) xor seed + override fun getCell(x: Int, y: Int): IChunkCell? { + return chunkMap.getCell(x, y) + } + abstract inner class ChunkMap { abstract operator fun get(x: Int, y: Int): ChunkType? - operator fun get(pos: ChunkPos) = get(pos.x, pos.y) abstract fun promote(self: ChunkType) + abstract fun purge() + abstract fun remove(x: Int, y: Int) + + abstract fun getCell(x: Int, y: Int): IChunkCell? protected val queue = ReferenceQueue() - - abstract fun purge() + operator fun get(pos: ChunkPos) = get(pos.x, pos.y) protected inner class Ref(chunk: ChunkType) : WeakReference(chunk, queue) { val pos = chunk.pos } - abstract fun remove(x: Int, y: Int) - - override fun getCell(x: Int, y: Int): IChunkCell? { - if (!this.x.isValidCellIndex(x) || !this.y.isValidCellIndex(y)) return null - val ix = this.x.cell(x) - val iy = this.y.cell(y) - return get(this.x.cellToChunk(ix), this.y.cellToChunk(iy))?.getCell(this.x.cellModulus(ix), this.y.cellModulus(iy)) - } - - override fun getCellDirect(x: Int, y: Int): IChunkCell? { - if (!this.x.inBoundsCell(x) || !this.y.inBoundsCell(y)) return null - return getCell(x, y) - } - protected fun create(x: Int, y: Int): ChunkType { purge() val pos = ChunkPos(x, y) @@ -106,7 +91,7 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk() - override val x = CoordinateMapper.Infinite - override val y = CoordinateMapper.Infinite - override fun get(x: Int, y: Int): ChunkType { + override fun getCell(x: Int, y: Int): IChunkCell? { + if (!this@World.x.isValidCellIndex(x) || !this@World.y.isValidCellIndex(y)) return null + val ix = this@World.x.cell(x) + val iy = this@World.y.cell(y) + return this[this@World.x.chunkFromCell(ix), this@World.y.chunkFromCell(iy)]?.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK) + } + + override fun get(x: Int, y: Int): ChunkType? { + if (!this@World.x.isValidChunkIndex(x) || !this@World.y.isValidChunkIndex(y)) return null + + val x = this@World.x.chunk(x) + val y = this@World.y.chunk(y) + return map[ChunkPos.toLong(x, y)]?.let { if (it is World<*, *>.ChunkMap.Ref) { it.get() as ChunkType? } else { it as ChunkType? } - } ?: create(x, y).also { - map[ChunkPos.toLong(x, y)] = Ref(it) - } + } ?: create(x, y).also { map[ChunkPos.toLong(x, y)] = Ref(it) } } override fun purge() { var next = queue.poll() as World<*, *>.ChunkMap.Ref? while (next != null) { - val get = map[ChunkPos.toLong(next.pos.x, next.pos.y)] + val k = ChunkPos.toLong(next.pos.x, next.pos.y) + val get = map[k] - if (get === next) { - map.remove(ChunkPos.toLong(next.pos.x, next.pos.y)) - } + if (get === next) + map.remove(k) next = queue.poll() as World<*, *>.ChunkMap.Ref? } @@ -167,6 +160,9 @@ abstract class World, ChunkType : Chunk.ChunkMap.Ref) { @@ -175,38 +171,44 @@ abstract class World, ChunkType : Chunk(divideUp(width, CHUNK_SIZE), divideUp(height, CHUNK_SIZE)) - private val map = Object2DArray.nulls(width / chunkWidth, height / chunkHeight) - - override fun get(x: Int, y: Int): ChunkType? { - if (!this.x.isValidChunkIndex(x) || !this.y.isValidChunkIndex(y)) return null - val ix = this.x.chunk(x) - val iy = this.x.chunk(y) - - return map[ix, iy]?.let { + private fun getRaw(x: Int, y: Int): ChunkType { + return map[x, y]?.let { if (it is World<*, *>.ChunkMap.Ref) { it.get() as ChunkType? } else { it as ChunkType? } - } ?: create(ix, iy).also { - map[ix, iy] = Ref(it) + } ?: create(x, y).also { + map[x, y] = Ref(it) } } + override fun getCell(x: Int, y: Int): IChunkCell? { + if (!this@World.x.isValidCellIndex(x) || !this@World.y.isValidCellIndex(y)) return null + + val ix = this@World.x.cell(x) + val iy = this@World.y.cell(y) + + return getRaw(ix ushr CHUNK_SIZE_BITS, iy ushr CHUNK_SIZE_BITS).getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK) + } + + override fun get(x: Int, y: Int): ChunkType? { + if (!this@World.x.isValidChunkIndex(x) || !this@World.y.isValidChunkIndex(y)) return null + return getRaw(this@World.x.chunk(x), this@World.y.chunk(y)) + } + override fun purge() { while (queue.poll() != null) {} } override fun promote(self: ChunkType) { val (x, y) = self.pos - val ref = map[x, y] if (ref !is World<*, *>.ChunkMap.Ref) { @@ -221,22 +223,20 @@ abstract class World, ChunkType : Chunk.ChunkMap.Ref) { old.clear() } - map[ix, iy] = null + map[x, y] = null } } - val chunkMap: ChunkMap = if (size != null) RectChunkMap() else InfiniteChunkMap() + val chunkMap: ChunkMap = if (size != null) ArrayChunkMap() else HashChunkMap() /** * Chunks, which have their collision mesh changed @@ -410,7 +410,7 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk) : IEntity { return } - val chunkPos = world.chunkMap.cellToChunk(position) + val chunkPos = world.chunkFromCell(position) if (value != null && chunkPos != value.pos) { throw IllegalStateException("Set proper position before setting chunk this Entity belongs to (expected chunk $chunkPos, got chunk ${value.pos})") @@ -120,13 +120,13 @@ abstract class Entity(override val world: World<*, *>) : IEntity { return val old = field - field = Vector2d(world.chunkMap.x.cell(value.x), world.chunkMap.x.cell(value.y)) + field = Vector2d(world.x.cell(value.x), world.y.cell(value.y)) movement.notifyPositionChanged() if (isSpawned && !isRemoved) { - val oldChunkPos = world.chunkMap.cellToChunk(old) - val newChunkPos = world.chunkMap.cellToChunk(field) + val oldChunkPos = world.chunkFromCell(old) + val newChunkPos = world.chunkFromCell(field) if (oldChunkPos != newChunkPos) { chunk = world.chunkMap[newChunkPos] @@ -154,7 +154,7 @@ abstract class Entity(override val world: World<*, *>) : IEntity { isSpawned = true world.entities.add(this) - chunk = world.chunkMap[world.chunkMap.cellToChunk(position)] + chunk = world.chunkMap[world.chunkFromCell(position)] if (chunk == null) { world.orphanedEntities.add(this)