From 0657ee8ef755a945df8d7a9870eca7601bc78c96 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sun, 3 Sep 2023 23:08:10 +0700 Subject: [PATCH] Chunk map, circular worlds second attempt --- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 19 +- .../kstarbound/client/ClientChunk.kt | 39 +- .../kstarbound/client/ClientWorld.kt | 9 +- .../kstarbound/client/StarboundClient.kt | 5 +- .../kstarbound/client/render/TileRenderer.kt | 1 + .../kstarbound/defs/tile/RenderTemplate.kt | 12 +- .../ru/dbotthepony/kstarbound/math/AABB.kt | 4 +- .../dbotthepony/kstarbound/world/CellView.kt | 8 +- .../ru/dbotthepony/kstarbound/world/Chunk.kt | 57 +- .../dbotthepony/kstarbound/world/ChunkPos.kt | 62 +- .../dbotthepony/kstarbound/world/Helpers.kt | 4 +- .../kstarbound/world/Raycasting.kt | 229 +++++++ .../ru/dbotthepony/kstarbound/world/Tuples.kt | 59 -- .../ru/dbotthepony/kstarbound/world/World.kt | 573 +++++++----------- .../kstarbound/world/api/Constants.kt | 1 + .../kstarbound/world/api/ICellAccess.kt | 9 + .../kstarbound/world/api/IChunk.kt | 8 +- .../kstarbound/world/api/ITileAccess.kt | 36 ++ .../kstarbound/world/api/ITileChunk.kt | 22 - .../kstarbound/world/entities/Entity.kt | 13 +- .../dbotthepony/kstarbound/test/MathTests.kt | 28 +- 21 files changed, 623 insertions(+), 575 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/Tuples.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ICellAccess.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ITileAccess.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ITileChunk.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 6836f8ca..61e96011 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -65,7 +65,8 @@ fun main() { var parse = 0L //for (chunkX in 17 .. 18) { - for (chunkX in 14 .. 24) { + //for (chunkX in 14 .. 24) { + for (chunkX in 0 .. 100) { // for (chunkY in 21 .. 21) { for (chunkY in 18 .. 24) { var t = System.currentTimeMillis() @@ -73,7 +74,7 @@ fun main() { find += System.currentTimeMillis() - t if (data != null) { - val chunk = client.world!!.computeIfAbsent(ChunkPos(chunkX, chunkY)) + val chunk = client.world!!.chunkMap.computeIfAbsent(ChunkPos(chunkX, chunkY)) val inflater = Inflater() inflater.setInput(data) @@ -105,12 +106,15 @@ fun main() { val item = starbound.items.values.random() val rand = java.util.Random() - for (i in 0 .. 10) { + client.world!!.physics.gravity = Vector2d.ZERO + + for (i in 0 .. 0) { val item = ItemEntity(client.world!!, item.value) - item.position = Vector2d(600.0 + 16.0 + i, 721.0 + 48.0) + item.position = Vector2d(7.0 + i, 685.0) item.spawn() - item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0)) + //item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0)) + item.movement.applyVelocity(Vector2d(-1.0, 0.0)) } // println(Starbound.statusEffects["firecharge"]) @@ -144,13 +148,14 @@ fun main() { //ent.position += Vector2d(y = 14.0, x = -10.0) ent.position = Vector2d(600.0 + 16.0, 721.0 + 48.0) - client.camera.pos = Vector2f(578f, 695f) + client.camera.pos = Vector2f(7f, 685f) client.onDrawGUI { 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("${client.camera.pos} ${client.settings.zoom}", y = 140f, scale = 0.25f) - client.gl.font.render("${ChunkPos.fromTilePosition(client.camera.pos.toDoubleVector())}", y = 160f, scale = 0.25f) + client.gl.font.render("Camera: ${ChunkPos.fromPosition(client.camera.pos.toDoubleVector())}", y = 160f, scale = 0.25f) + client.gl.font.render("World: ${client.world!!.chunkMap.cellToGrid(client.camera.pos.toDoubleVector())}", y = 180f, 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 55d58e58..93efbb6d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt @@ -18,6 +18,7 @@ import ru.dbotthepony.kstarbound.world.* import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEd import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEf +import ru.dbotthepony.kstarbound.world.api.ITileAccess import ru.dbotthepony.kstarbound.world.api.ITileChunk import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kvector.arrays.Matrix4fStack @@ -42,12 +43,13 @@ const val Z_LEVEL_LIQUID = 10000 class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk(world, pos), Closeable { val state: GLStateTracker get() = world.client.gl - private inner class TileLayerRenderer(private val layerChangeset: IntSupplier, private val view: () -> ITileChunk, private val isBackground: Boolean) : AutoCloseable { + private inner class TileLayerRenderer(private val view: () -> ITileChunk, private val isBackground: Boolean) : AutoCloseable { private val layers = TileLayerList() val bakedMeshes = LinkedList>() - private var changeset = -1 + var isDirty = true fun bake() { + isDirty = false val view = view() if (state.isSameThread()) { @@ -60,7 +62,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk(seed, widthInChunks) { + size: Vector2i? = null, + loopX: Boolean = false, + loopY: Boolean = false +) : World(seed, size, loopX, loopY) { init { physics.debugDraw = client.gl.box2dRenderer } @@ -195,8 +198,6 @@ class ClientWorld( //frame.close() //texture.close() - physics.debugDraw() - /*for (renderer in determineRenderers) { renderer.renderDebug() }*/ diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 1311bafd..df933bc9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -26,6 +26,7 @@ import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2f +import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector3f import java.io.Closeable import java.nio.ByteBuffer @@ -235,7 +236,7 @@ class StarboundClient(val starbound: Starbound) : Closeable { lightRenderer.resizeFramebuffer(viewportWidth, viewportHeight) } - var world: ClientWorld? = ClientWorld(this, 0L, 0) + var world: ClientWorld? = ClientWorld(this, 0L, Vector2i(3000, 2000), true) init { putDebugLog("Initialized OpenGL context") @@ -345,6 +346,8 @@ class StarboundClient(val starbound: Starbound) : Closeable { layers.render(gl.matrixStack) + world.physics.debugDraw() + for (lambda in onPostDrawWorld) { lambda.invoke() } 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 80d9f08a..d86cdf9f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt @@ -11,6 +11,7 @@ import ru.dbotthepony.kstarbound.client.gl.shader.GLTileProgram import ru.dbotthepony.kstarbound.client.gl.vertex.GLAttributeList import ru.dbotthepony.kstarbound.client.gl.vertex.* import ru.dbotthepony.kstarbound.defs.tile.* +import ru.dbotthepony.kstarbound.world.api.ITileAccess import ru.dbotthepony.kstarbound.world.api.ITileChunk import ru.dbotthepony.kstarbound.world.api.ITileState import ru.dbotthepony.kstarbound.world.api.TileColor diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt index cd01799c..7fbbffcc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt @@ -6,7 +6,7 @@ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory import ru.dbotthepony.kstarbound.util.WriteOnce -import ru.dbotthepony.kstarbound.world.api.ITileChunk +import ru.dbotthepony.kstarbound.world.api.ITileAccess import ru.dbotthepony.kstarbound.world.api.ITileState import ru.dbotthepony.kvector.vector.Vector2i @@ -38,7 +38,7 @@ data class RenderRuleList( val matchHue: Boolean = false, val inverse: Boolean = false, ) { - private fun doTest(getter: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { + private fun doTest(getter: ITileAccess, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { return when (type) { "EqualsSelf" -> equalityTester.test(getter.getTile(thisPos), getter.getTile(thisPos + offsetPos)) "Connects" -> getter.getTile(thisPos + offsetPos).material != null @@ -53,7 +53,7 @@ data class RenderRuleList( } } - fun test(getter: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { + fun test(getter: ITileAccess, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { if (inverse) { return !doTest(getter, equalityTester, thisPos, offsetPos) } @@ -67,7 +67,7 @@ data class RenderRuleList( } } - fun test(getter: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i, offset: Vector2i): Boolean { + fun test(getter: ITileAccess, equalityTester: EqualityRuleTester, thisPos: Vector2i, offset: Vector2i): Boolean { when (join) { Combination.ALL -> { for (entry in entries) { @@ -120,7 +120,7 @@ data class RenderMatch( ) { var rule by WriteOnce() - fun test(getter: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { + fun test(getter: ITileAccess, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { return rule.test(getter, equalityTester, thisPos, offset) } @@ -156,7 +156,7 @@ data class RenderMatch( * * [equalityTester] требуется для проверки раенства между "этим" тайлом и другим */ - fun test(tileAccess: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { + fun test(tileAccess: ITileAccess, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { for (matcher in matchAllPoints) { if (!matcher.test(tileAccess, equalityTester, thisPos)) { return false diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt index 5d7bb8b7..fee7bfb1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt @@ -14,8 +14,8 @@ fun AABB.encasingIntAABB(): AABBi { fun AABB.encasingChunkPosAABB(): AABBi { return AABBi( - Vector2i(ChunkPos.tileToChunkComponent(roundTowardsNegativeInfinity(mins.x)), ChunkPos.tileToChunkComponent(roundTowardsNegativeInfinity(mins.y))), - Vector2i(ChunkPos.tileToChunkComponent(roundTowardsPositiveInfinity(maxs.x)), ChunkPos.tileToChunkComponent(roundTowardsPositiveInfinity(maxs.y))), + Vector2i(ChunkPos.component(roundTowardsNegativeInfinity(mins.x)), ChunkPos.component(roundTowardsNegativeInfinity(mins.y))), + Vector2i(ChunkPos.component(roundTowardsPositiveInfinity(maxs.x)), ChunkPos.component(roundTowardsPositiveInfinity(maxs.y))), ) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CellView.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CellView.kt index 89d795b1..f45ad5e9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CellView.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CellView.kt @@ -1,8 +1,8 @@ package ru.dbotthepony.kstarbound.world -import ru.dbotthepony.kstarbound.world.api.BackgroundView +import ru.dbotthepony.kstarbound.world.api.BackgroundChunkView import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE -import ru.dbotthepony.kstarbound.world.api.ForegroundView +import ru.dbotthepony.kstarbound.world.api.ForegroundChunkView import ru.dbotthepony.kstarbound.world.api.IChunk import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kvector.arrays.Object2DArray @@ -29,8 +29,8 @@ class CellView( val bottom: IChunk?, val bottomRight: IChunk?, ) : IChunk { - val backgroundView = BackgroundView(this) - val foregroundView = ForegroundView(this) + val backgroundView = BackgroundChunkView(this) + val foregroundView = ForegroundChunkView(this) override fun getCell(x: Int, y: Int): IChunkCell { val ix = x + CHUNK_SIZE diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 6f6bebf6..9555d8de 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -7,12 +7,12 @@ import ru.dbotthepony.kbox2d.dynamics.B2Fixture import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.TileDefinition -import ru.dbotthepony.kstarbound.world.api.BackgroundView +import ru.dbotthepony.kstarbound.world.api.BackgroundChunkView import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_BITS import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEd -import ru.dbotthepony.kstarbound.world.api.ForegroundView +import ru.dbotthepony.kstarbound.world.api.ForegroundChunkView import ru.dbotthepony.kstarbound.world.api.IChunk import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kstarbound.world.api.ILiquidState @@ -38,8 +38,7 @@ import kotlin.collections.HashSet * * Весь игровой мир будет измеряться в Starbound Unit'ах */ -abstract class Chunk, This : Chunk>(val world: WorldType, final override val pos: ChunkPos) : - IChunk { +abstract class Chunk, This : Chunk>(val world: WorldType, final override val pos: ChunkPos) : IChunk { var changeset = 0 private set var tileChangeset = 0 @@ -56,14 +55,34 @@ abstract class Chunk, This : Chunk Unit) { + world.chunkMap[pos.left]?.let(block) + world.chunkMap[pos.right]?.let(block) + world.chunkMap[pos.top]?.let(block) + world.chunkMap[pos.bottom]?.let(block) + world.chunkMap[pos.topLeft]?.let(block) + world.chunkMap[pos.topRight]?.let(block) + world.chunkMap[pos.bottomLeft]?.let(block) + world.chunkMap[pos.bottomRight]?.let(block) + } val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble()) @@ -79,8 +98,8 @@ abstract class Chunk, This : Chunk, This : Chunk 0) { "Bounds must be positive ($bounds given)" } @@ -130,64 +128,48 @@ class ChunkPos(val x: Int, val y: Int) : Comparable { return x.toLong() or (y.toLong() shl 32) } - fun fromTilePosition(input: IStruct2i): ChunkPos { + fun longFromPosition(x: Int, y: Int): Long { + return toLong(component(x), component(y)) + } + + fun fromPosition(input: IStruct2i): ChunkPos { val (x, y) = input - return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y)) + return ChunkPos(component(x), component(y)) } - fun fromTilePosition(input: IStruct2i, xWrap: Int): ChunkPos { + fun fromPosition(input: IStruct2d): ChunkPos { val (x, y) = input - return ChunkPos(circulate(tileToChunkComponent(x), xWrap), tileToChunkComponent(y)) + return fromPosition(x, y) } - fun fromTilePosition(input: IStruct2d): ChunkPos { - val (x, y) = input - return fromTilePosition(x, y) + fun fromPosition(x: Int, y: Int): ChunkPos { + return ChunkPos(component(x), component(y)) } - fun fromTilePosition(input: IStruct2d, xWrap: Int): ChunkPos { - val (x, y) = input - return fromTilePosition(x, y, xWrap) + fun fromPosition(x: Int, y: Int, xWrap: Int): ChunkPos { + return ChunkPos(circulate(component(x), xWrap), component(y)) } - fun fromTilePosition(x: Int, y: Int): ChunkPos { - return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y)) - } - - fun fromTilePosition(x: Int, y: Int, xWrap: Int): ChunkPos { - return ChunkPos(circulate(tileToChunkComponent(x), xWrap), tileToChunkComponent(y)) - } - - fun fromTilePosition(x: Double, y: Double): ChunkPos { + fun fromPosition(x: Double, y: Double): ChunkPos { return ChunkPos( - tileToChunkComponent(roundByAbsoluteValue(x)), - tileToChunkComponent(roundByAbsoluteValue(y)) + component(roundByAbsoluteValue(x)), + component(roundByAbsoluteValue(y)) ) } - fun fromTilePosition(x: Double, y: Double, xWrap: Int): ChunkPos { + fun fromPosition(x: Double, y: Double, xWrap: Int): ChunkPos { return ChunkPos( - circulate(tileToChunkComponent(roundByAbsoluteValue(x)), xWrap), - tileToChunkComponent(roundByAbsoluteValue(y)) + circulate(component(roundByAbsoluteValue(x)), xWrap), + component(roundByAbsoluteValue(y)) ) } - fun normalizeCoordinate(input: Int): Int { - val band = input and CHUNK_SIZE_FF - - if (band < 0) { - return band + CHUNK_SIZE_FF + fun component(value: Int): Int { + if (value < 0) { + return -((-value) shr CHUNK_SIZE_BITS) - 1 } - return band - } - - fun tileToChunkComponent(comp: Int): Int { - if (comp < 0) { - return -(comp.absoluteValue shr CHUNK_SIZE_BITS) - 1 - } - - return comp shr CHUNK_SIZE_BITS + return value shr CHUNK_SIZE_BITS } } } \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Helpers.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Helpers.kt index 2ed25ebe..62ee1749 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Helpers.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Helpers.kt @@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.world import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.api.IChunk import ru.dbotthepony.kstarbound.world.api.IChunkCell -import ru.dbotthepony.kstarbound.world.api.ITileChunk +import ru.dbotthepony.kstarbound.world.api.ITileAccess import ru.dbotthepony.kstarbound.world.api.ITileState import ru.dbotthepony.kvector.vector.Vector2i @@ -35,7 +35,7 @@ fun IChunk.iterate(fromX: Int = 0, fromY: Int = 0, toX: Int = fromX + CHUNK_SIZE } } -fun ITileChunk.iterate(fromX: Int = 0, fromY: Int = 0, toX: Int = fromX + CHUNK_SIZE, toY: Int = fromY + CHUNK_SIZE): Iterator> { +fun ITileAccess.iterateTiles(fromX: Int = 0, fromY: Int = 0, toX: Int = fromX + CHUNK_SIZE, toY: Int = fromY + CHUNK_SIZE): Iterator> { return object : Iterator> { private var x = fromX private var y = fromY diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt new file mode 100644 index 00000000..86d794b8 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt @@ -0,0 +1,229 @@ +package ru.dbotthepony.kstarbound.world + +import com.google.common.collect.ImmutableList +import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT +import ru.dbotthepony.kstarbound.world.api.ICellAccess +import ru.dbotthepony.kstarbound.world.api.IChunkCell +import ru.dbotthepony.kvector.arrays.Double2DArray +import ru.dbotthepony.kvector.vector.Vector2d +import ru.dbotthepony.kvector.vector.Vector2i +import java.util.* +import kotlin.collections.ArrayList +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.roundToInt +import kotlin.math.sin + + +const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT + +data class RayCastResult( + val traversedTiles: List>, + val hitTile: Pair?, + val fraction: Double +) + +private fun makeDirFan(step: Double): List { + var i = 0.0 + val result = ImmutableList.builder() + + while (i < 360.0) { + i += step + result.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI))) + } + + return result.build() +} + +private val potatoDirFan by lazy { makeDirFan(4.0) } +private val veryRoughDirFan by lazy { makeDirFan(3.0) } +private val roughDirFan by lazy { makeDirFan(2.0) } +private val dirFan by lazy { makeDirFan(1.0) } +private val preciseFan by lazy { makeDirFan(0.5) } +private val veryPreciseFan by lazy { makeDirFan(0.25) } + +private fun chooseLightRayFan(size: Double): List { + return when (size) { + in 0.0 .. 8.0 -> potatoDirFan + in 8.0 .. 16.0 -> veryRoughDirFan + in 16.0 .. 24.0 -> roughDirFan + in 24.0 .. 48.0 -> dirFan + in 48.0 .. 96.0 -> preciseFan + // in 32.0 .. 48.0 -> veryPreciseFan + else -> veryPreciseFan + } +} + +/** + * [HIT] - луч попал по объекту и трассировка прекращается; объект записывается в коллекцию объектов, в которые попал луч. + * + * [HIT_SKIP] - луч попал по объекту и трассировка прекращается; объект не записывается в коллекцию объектов, в которые попал луч. + * + * [SKIP] - луч не попал по объекту, объект не записывается в коллекцию объектов, в которые попал луч. + * + * [CONTINUE] - луч не попал по объекту; объект записывается в коллекцию объектов, в которые попал луч. + */ +enum class RayFilterResult { + HIT, + HIT_SKIP, + SKIP, + CONTINUE; + + companion object { + fun of(boolean: Boolean): RayFilterResult { + return if (boolean) HIT else CONTINUE + } + } +} + +fun interface TileRayFilter { + fun test(state: IChunkCell, fraction: Double, position: Vector2i): RayFilterResult +} + +/** + * Считает все тайлы неблокирующими + */ +val AnythingRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.CONTINUE } + +/** + * Попадает по первому не-пустому тайлу + */ +val NonSolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material != null) } + +/** + * Попадает по первому пустому тайлу + */ +val SolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material == null) } + +/** + * Попадает по первому тайлу который блокирует проход света + */ +val LineOfSightRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material?.renderParameters?.lightTransparent == false) } + +/** + * Бросает луч напротив тайлов мира с заданными позициями и фильтром + */ +fun ICellAccess.castRayNaive( + rayStart: Vector2d, + rayEnd: Vector2d, + filter: TileRayFilter = AnythingRayFilter +): RayCastResult { + if (rayStart == rayEnd) { + return RayCastResult(listOf(), null, 1.0) + } + + var t = 0.0 + val dir = rayEnd - rayStart + val inc = 0.5 / dir.length + + val tiles = LinkedList>() + var prev = Vector2i(Int.MIN_VALUE, Int.MAX_VALUE) + var hitTile: Pair? = null + + while (t < 1.0) { + val (x, y) = rayStart + dir * t + val tilePos = Vector2i(x.roundToInt(), y.roundToInt()) + + if (tilePos != prev) { + val tile = getCell(tilePos) + + when (filter.test(tile, t, tilePos)) { + RayFilterResult.HIT -> { + hitTile = tilePos to tile + tiles.add(hitTile) + break + } + + RayFilterResult.HIT_SKIP -> { + hitTile = tilePos to tile + break + } + + RayFilterResult.SKIP -> {} + RayFilterResult.CONTINUE -> tiles.add(tilePos to tile) + } + + prev = tilePos + } + + t += inc + } + + return RayCastResult(tiles, hitTile, t) +} + +/** + * Бросает луч напротив тайлов мира с заданной позицией, направлением и фильтром + */ +fun ICellAccess.castRayNaive( + rayPosition: Vector2d, + direction: Vector2d, + length: Double, + filter: TileRayFilter = AnythingRayFilter +): RayCastResult { + return castRayNaive(rayPosition, rayPosition + direction.unitVector * length, filter) +} + +/** + * Выпускает луч света с заданной силой (определяет длину луча и способность проходить сквозь тайлы), позицией и направлением. + * + * Позволяет указать отдельно [falloffByTile] потерю силы света при прохождении через тайлы. + */ +fun ICellAccess.rayLightNaive( + position: Vector2d, + direction: Vector2d, + intensity: Double, + falloffByTile: Double = 2.0, + falloffByTravel: Double = 1.0, +): List> { + val result = ArrayList>() + + var currentIntensity = intensity + + castRayNaive(position, direction, intensity) { state, t, pos -> + if (state.foreground.material?.renderParameters?.lightTransparent == false) { + currentIntensity -= falloffByTile + } else { + currentIntensity -= falloffByTravel + } + + //result.add(currentIntensity to pos) + + if (currentIntensity <= 0.0) { + return@castRayNaive RayFilterResult.HIT_SKIP + } else { + return@castRayNaive RayFilterResult.SKIP + } + } + + return result +} + +/** + * Трассирует лучи света вокруг себя с заданной позицией, интенсивностью, + * падением интенсивности за проход сквозь тайл [falloffByTile] и + * падением интенсивности за проход по пустому месту [falloffByTravel]. + */ +fun ICellAccess.rayLightCircleNaive( + position: Vector2d, + intensity: Double, + falloffByTile: Double = 2.0, + falloffByTravel: Double = 1.0, +): Double2DArray { + val combinedResult = Double2DArray.allocate(intensity.roundToInt() * 2, intensity.roundToInt() * 2) + val baselineX = position.x.roundToInt() - intensity.roundToInt() + val baselineY = position.y.roundToInt() - intensity.roundToInt() + + val dirs = chooseLightRayFan(intensity) + val mul = 1.0 / dirs.size + + for (dir in dirs) { + val result2 = rayLightNaive(position, dir, intensity, falloffByTile, falloffByTravel) + + for (pair in result2) { + combinedResult[pair.second.y - baselineY, pair.second.x - baselineX] += pair.first * mul + } + } + + return combinedResult +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Tuples.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Tuples.kt deleted file mode 100644 index 6e7df40d..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Tuples.kt +++ /dev/null @@ -1,59 +0,0 @@ -package ru.dbotthepony.kstarbound.world - - -/** - * Кортеж чанка, который содержит родителя (мир) и соседей (кортежи чанков) - */ -interface IWorldChunkTuple, ChunkType : Chunk> { - val world: WorldType - val chunk: ChunkType - val top: IWorldChunkTuple? - val left: IWorldChunkTuple? - val right: IWorldChunkTuple? - val bottom: IWorldChunkTuple? - - val topLeft: IWorldChunkTuple? - val topRight: IWorldChunkTuple? - val bottomLeft: IWorldChunkTuple? - val bottomRight: IWorldChunkTuple? -} - -class ProxiedWorldChunkTuple, ChunkType : Chunk>( - private val parent: IWorldChunkTuple -) : IWorldChunkTuple { - override val world get() = parent.world - override val chunk get() = parent.chunk - - override val top: IWorldChunkTuple? get() = parent.top?.let(::ProxiedWorldChunkTuple) - override val left: IWorldChunkTuple? get() = parent.left?.let(::ProxiedWorldChunkTuple) - override val right: IWorldChunkTuple? get() = parent.right?.let(::ProxiedWorldChunkTuple) - override val bottom: IWorldChunkTuple? get() = parent.bottom?.let(::ProxiedWorldChunkTuple) - override val topLeft: IWorldChunkTuple? get() = parent.topLeft?.let(::ProxiedWorldChunkTuple) - override val topRight: IWorldChunkTuple? get() = parent.topRight?.let(::ProxiedWorldChunkTuple) - override val bottomLeft: IWorldChunkTuple? get() = parent.bottomLeft?.let(::ProxiedWorldChunkTuple) - override val bottomRight: IWorldChunkTuple? get() = parent.bottomRight?.let(::ProxiedWorldChunkTuple) -} - -class InstantWorldChunkTuple, ChunkType : Chunk>( - override val world: WorldType, - override val chunk: ChunkType -) : IWorldChunkTuple { - - private val _top = world[chunk.top] - private val _left = world[chunk.left] - private val _right = world[chunk.right] - private val _bottom = world[chunk.bottom] - private val _topLeft = world[chunk.topLeft] - private val _topRight = world[chunk.topRight] - private val _bottomLeft = world[chunk.bottomLeft] - private val _bottomRight = world[chunk.bottomRight] - - override val top: IWorldChunkTuple? by lazy { _top?.let { InstantWorldChunkTuple(world, it) } } - override val left: IWorldChunkTuple? by lazy { _left?.let { InstantWorldChunkTuple(world, it) } } - override val right: IWorldChunkTuple? by lazy { _right?.let { InstantWorldChunkTuple(world, it) } } - override val bottom: IWorldChunkTuple? by lazy { _bottom?.let { InstantWorldChunkTuple(world, it) } } - override val topLeft: IWorldChunkTuple? by lazy { _topLeft?.let { InstantWorldChunkTuple(world, it) } } - override val topRight: IWorldChunkTuple? by lazy { _topRight?.let { InstantWorldChunkTuple(world, it) } } - override val bottomLeft: IWorldChunkTuple? by lazy { _bottomLeft?.let { InstantWorldChunkTuple(world, it) } } - override val bottomRight: IWorldChunkTuple? by lazy { _bottomRight?.let { InstantWorldChunkTuple(world, it) } } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 65023d30..4f9cc661 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -1,8 +1,7 @@ package ru.dbotthepony.kstarbound.world -import com.google.common.collect.ImmutableList -import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import ru.dbotthepony.kbox2d.api.ContactImpulse import ru.dbotthepony.kbox2d.api.IContactFilter import ru.dbotthepony.kbox2d.api.IContactListener @@ -10,128 +9,212 @@ import ru.dbotthepony.kbox2d.api.Manifold import ru.dbotthepony.kbox2d.dynamics.B2Fixture import ru.dbotthepony.kbox2d.dynamics.B2World import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact -import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.util.Timer +import ru.dbotthepony.kstarbound.world.api.BackgroundAccessView import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE +import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_BITS +import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_MASK +import ru.dbotthepony.kstarbound.world.api.ForegroundAccessView +import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kstarbound.world.entities.Entity -import ru.dbotthepony.kvector.arrays.Double2DArray +import ru.dbotthepony.kvector.api.IStruct2d +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.util.LinkedList -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.roundToInt -import kotlin.math.sin - -const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT - -data class RayCastResult( - val traversedTiles: List>, - val hitTile: Pair?, - val fraction: Double -) - -private fun makeDirFan(step: Double): List { - var i = 0.0 - val result = ImmutableList.builder() - - while (i < 360.0) { - i += step - result.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI))) - } - - return result.build() -} - -private val potatoDirFan by lazy { makeDirFan(4.0) } -private val veryRoughDirFan by lazy { makeDirFan(3.0) } -private val roughDirFan by lazy { makeDirFan(2.0) } -private val dirFan by lazy { makeDirFan(1.0) } -private val preciseFan by lazy { makeDirFan(0.5) } -private val veryPreciseFan by lazy { makeDirFan(0.25) } - -private fun chooseLightRayFan(size: Double): List { - return when (size) { - in 0.0 .. 8.0 -> potatoDirFan - in 8.0 .. 16.0 -> veryRoughDirFan - in 16.0 .. 24.0 -> roughDirFan - in 24.0 .. 48.0 -> dirFan - in 48.0 .. 96.0 -> preciseFan - // in 32.0 .. 48.0 -> veryPreciseFan - else -> veryPreciseFan - } -} - -/** - * [HIT] - луч попал по объекту и трассировка прекращается; объект записывается в коллекцию объектов, в которые попал луч. - * - * [HIT_SKIP] - луч попал по объекту и трассировка прекращается; объект не записывается в коллекцию объектов, в которые попал луч. - * - * [SKIP] - луч не попал по объекту, объект не записывается в коллекцию объектов, в которые попал луч. - * - * [CONTINUE] - луч не попал по объекту; объект записывается в коллекцию объектов, в которые попал луч. - */ -enum class RayFilterResult { - HIT, - HIT_SKIP, - SKIP, - CONTINUE; - - companion object { - fun of(boolean: Boolean): RayFilterResult { - return if (boolean) HIT else CONTINUE - } - } -} - -fun interface TileRayFilter { - fun test(state: IChunkCell, fraction: Double, position: Vector2i): RayFilterResult -} - -/** - * Считает все тайлы неблокирующими - */ -val AnythingRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.CONTINUE } - -/** - * Попадает по первому не-пустому тайлу - */ -val NonSolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material != null) } - -/** - * Попадает по первому пустому тайлу - */ -val SolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material == null) } - -/** - * Попадает по первому тайлу который блокирует проход света - */ -val LineOfSightRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material?.renderParameters?.lightTransparent == false) } abstract class World, ChunkType : Chunk>( val seed: Long, - val widthInChunks: Int + val size: Vector2i?, + val loopX: Boolean, + val loopY: Boolean ) { - protected val chunkMap = Long2ObjectOpenHashMap() + abstract class AbstractCoordinatesWrapper { + 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 chunk(value: Double): Double - /** - * Является ли мир "сферическим" - * - * Данный флаг говорит о том, что [widthInChunks] имеет осмысленное значение, - * попытка получить чанк с X координатой меньше нуля или больше [widthInChunks] - * приведёт к замыканию на конец/начало мира соответственно - */ - val isCircular = widthInChunks > 0 + protected fun positiveModulo(a: Int, b: Int): Int { + val result = a % b + return if (result < 0) result + b else result + } + + protected fun positiveModulo(a: Double, b: Int): Double { + val result = a % b + return if (result < 0.0) result + b else result + } + + protected fun positiveModulo(a: Float, b: Int): Float { + val result = a % b + return if (result < 0f) result + b else result + } + } + + object PassthroughWrapper : AbstractCoordinatesWrapper() { + 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 chunk(value: Double): Double = value + } + + class CoordinatesWrapper(private val cell: Int, private val chunk: Int) : AbstractCoordinatesWrapper() { + override fun cell(value: Int): Int { + return positiveModulo(value, cell) + } + + override fun cell(value: Double): Double { + return positiveModulo(value, cell) + } + + override fun cell(value: Float): Float { + return positiveModulo(value, cell) + } + + override fun chunk(value: Int): Int { + return positiveModulo(value, chunk) + } + + override fun chunk(value: Double): Double { + return positiveModulo(value, chunk) + } + } + + class CoordinatesClamper(private val cell: Int, private val chunk: Int) : AbstractCoordinatesWrapper() { + override fun cell(value: Int): Int { + return value.coerceIn(0, cell - 1) + } + + override fun cell(value: Double): Double { + return value.coerceIn(0.0, cell - 1.0) + } + + override fun cell(value: Float): Float { + return value.coerceIn(0f, cell - 1f) + } + + override fun chunk(value: Int): Int { + return value.coerceIn(0, chunk - 1) + } + + override fun chunk(value: Double): Double { + return value.coerceIn(0.0, chunk - 1.0) + } + } + + abstract inner class ChunkMap : ICellAccess { + abstract val x: AbstractCoordinatesWrapper + abstract val y: AbstractCoordinatesWrapper + + val backgroundView = BackgroundAccessView(this) + val foregroundView = ForegroundAccessView(this) + + abstract operator fun get(x: Int, y: Int): ChunkType? + operator fun get(pos: ChunkPos) = get(pos.x, pos.y) + + override fun getCell(x: Int, y: Int): IChunkCell { + return get(ChunkPos.component(x), ChunkPos.component(y))?.getCell(x and CHUNK_SIZE_MASK, y and CHUNK_SIZE_MASK) ?: IChunkCell.Companion + } + + abstract operator fun set(x: Int, y: Int, chunk: ChunkType) + abstract fun remove(x: Int, y: Int) + + fun cellToGrid(x: Int, y: Int) = ChunkPos.fromPosition(this.x.cell(x), this.y.cell(y)) + fun cellToGrid(x: Double, y: Double) = ChunkPos.fromPosition(this.x.cell(x), this.y.cell(y)) + fun cellToGrid(position: IStruct2i) = cellToGrid(position.component1(), position.component2()) + fun cellToGrid(position: IStruct2d) = cellToGrid(position.component1(), position.component2()) + + fun computeIfAbsent(pos: ChunkPos) = computeIfAbsent(pos.x, pos.y) + + fun computeIfAbsent(x: Int, y: Int): ChunkType { + val existing = get(x, y) + + if (existing != null) + return existing + + val pos = ChunkPos(this.x.chunk(x), this.y.chunk(y)) + val chunk = chunkFactory(pos) + val orphanedInThisChunk = ArrayList() + + for (ent in orphanedEntities) { + val (ex, ey) = ent.position + + if (ChunkPos.fromPosition(this.x.cell(ex), this.y.cell(ey)) == pos) { + orphanedInThisChunk.add(ent) + } + } + + for (ent in orphanedInThisChunk) { + ent.chunk = chunk + } + + set(x, y, chunk) + return chunk + } + } + + inner class InfiniteChunkMap : ChunkMap() { + private val map = Long2ObjectOpenHashMap() + + override fun get(x: Int, y: Int): ChunkType? { + return map[ChunkPos.toLong(x, y)] + } + + override fun set(x: Int, y: Int, chunk: ChunkType) { + map[ChunkPos.toLong(x, y)] = chunk + } + + override fun remove(x: Int, y: Int) { + map.remove(ChunkPos.toLong(x, y)) + } + + override val x = PassthroughWrapper + override val y = PassthroughWrapper + } + + inner class RectChunkMap : ChunkMap() { + val width = size!!.x + val height = size!!.y + + val widthInChunks = if (width and CHUNK_SIZE_MASK == 0) width / CHUNK_SIZE else width / CHUNK_SIZE + 1 + val heightInChunks = if (height and CHUNK_SIZE_MASK == 0) height / CHUNK_SIZE else height / CHUNK_SIZE + 1 + + override val x: AbstractCoordinatesWrapper = if (loopX) CoordinatesWrapper(width, widthInChunks) else CoordinatesClamper(width, widthInChunks) + override val y: AbstractCoordinatesWrapper = if (loopY) CoordinatesWrapper(height, heightInChunks) else CoordinatesClamper(height, heightInChunks) + + private val map = Object2DArray.nulls(widthInChunks, heightInChunks) + + override fun get(x: Int, y: Int): ChunkType? { + return map[this.x.chunk(x), this.y.chunk(y)] + } + + override fun getCell(x: Int, y: Int): IChunkCell { + if (x < 0 || y < 0) return IChunkCell.Companion + return get(x ushr CHUNK_SIZE_BITS, y ushr CHUNK_SIZE_BITS)?.getCell(x and CHUNK_SIZE_MASK, y and CHUNK_SIZE_MASK) ?: IChunkCell.Companion + } + + override fun set(x: Int, y: Int, chunk: ChunkType) { + map[this.x.chunk(x), this.y.chunk(y)] = chunk + } + + override fun remove(x: Int, y: Int) { + map[this.x.chunk(x), this.y.chunk(y)] = null + } + } + + val chunkMap: ChunkMap = if (size != null) RectChunkMap() else InfiniteChunkMap() /** * Chunks, which have their collision mesh changed */ - val dirtyPhysicsChunks = HashSet() + val dirtyPhysicsChunks = ObjectOpenHashSet() val physics = B2World(Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION)) @@ -293,270 +376,20 @@ abstract class World, ChunkType : Chunk? { - return this[pos]?.let { InstantWorldChunkTuple(this as This, it) } - } - - /** - * Возвращает чанк на заданной позиции, создаёт его если он не существует - */ - open fun computeIfAbsent(pos: ChunkPos): ChunkType { - @Suppress("Name_Shadowing") - val pos = if (isCircular) pos.circular(widthInChunks) else pos - - return chunkMap.computeIfAbsent(pos.toLong(), Long2ObjectFunction { - val chunk = chunkFactory(pos) - - val orphanedInThisChunk = ArrayList() - - for (ent in orphanedEntities) { - val cPos = if (isCircular) ChunkPos.fromTilePosition(ent.position, widthInChunks) else ChunkPos.fromTilePosition(ent.position) - - if (cPos == pos) { - orphanedInThisChunk.add(ent) - } - } - - for (ent in orphanedInThisChunk) { - ent.chunk = chunk - } - - return@Long2ObjectFunction chunk - }) - } - - /** - * Позволяет получать чанки/тайлы с минимальным кешем. Если один чанк считывается очень большое число раз, - * то использование этого класса сильно ускорит работу. - * - * Так же реализует raycasting методы. - */ - inner class CachedGetter { - private var lastChunk: ChunkType? = null - private var lastPos: ChunkPos? = null - - operator fun get(pos: ChunkPos): ChunkType? { - if (lastPos == pos) { - return lastChunk - } - - lastChunk = this@World[pos] - lastPos = pos - return lastChunk - } - - fun getCell(pos: Vector2i): IChunkCell? { - return get(ChunkPos.fromTilePosition(pos))?.getCell(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)) - } - - /** - * Бросает луч напротив тайлов мира с заданными позициями и фильтром - */ - fun castRayNaive( - rayStart: Vector2d, - rayEnd: Vector2d, - filter: TileRayFilter = AnythingRayFilter - ): RayCastResult { - if (rayStart == rayEnd) { - return RayCastResult(listOf(), null, 1.0) - } - - var t = 0.0 - val dir = rayEnd - rayStart - val inc = 0.5 / dir.length - - val tiles = LinkedList>() - var prev = Vector2i(Int.MIN_VALUE, Int.MAX_VALUE) - var hitTile: Pair? = null - - while (t < 1.0) { - val (x, y) = rayStart + dir * t - val tilePos = Vector2i(x.roundToInt(), y.roundToInt()) - - if (tilePos != prev) { - val tile = getCell(tilePos) ?: IChunkCell.Companion - - when (filter.test(tile, t, tilePos)) { - RayFilterResult.HIT -> { - hitTile = tilePos to tile - tiles.add(hitTile) - break - } - - RayFilterResult.HIT_SKIP -> { - hitTile = tilePos to tile - break - } - - RayFilterResult.SKIP -> {} - RayFilterResult.CONTINUE -> tiles.add(tilePos to tile) - } - - prev = tilePos - } - - t += inc - } - - return RayCastResult(tiles, hitTile, t) - } - - /** - * Бросает луч напротив тайлов мира с заданной позицией, направлением и фильтром - */ - fun castRayNaive( - rayPosition: Vector2d, - direction: Vector2d, - length: Double, - filter: TileRayFilter = AnythingRayFilter - ): RayCastResult { - return castRayNaive(rayPosition, rayPosition + direction.unitVector * length, filter) - } - - /** - * Выпускает луч света с заданной силой (определяет длину луча и способность проходить сквозь тайлы), позицией и направлением. - * - * Позволяет указать отдельно [falloffByTile] потерю силы света при прохождении через тайлы. - */ - fun rayLightNaive( - position: Vector2d, - direction: Vector2d, - intensity: Double, - falloffByTile: Double = 2.0, - falloffByTravel: Double = 1.0, - ): List> { - val result = ArrayList>() - - var currentIntensity = intensity - - castRayNaive(position, direction, intensity) { state, t, pos -> - if (state.foreground.material?.renderParameters?.lightTransparent == false) { - currentIntensity -= falloffByTile - } else { - currentIntensity -= falloffByTravel - } - - //result.add(currentIntensity to pos) - - if (currentIntensity <= 0.0) { - return@castRayNaive RayFilterResult.HIT_SKIP - } else { - return@castRayNaive RayFilterResult.SKIP - } - } - - return result - } - - /** - * Трассирует лучи света вокруг себя с заданной позицией, интенсивностью, - * падением интенсивности за проход сквозь тайл [falloffByTile] и - * падением интенсивности за проход по пустому месту [falloffByTravel]. - */ - fun rayLightCircleNaive( - position: Vector2d, - intensity: Double, - falloffByTile: Double = 2.0, - falloffByTravel: Double = 1.0, - ): Double2DArray { - val combinedResult = Double2DArray.allocate(intensity.roundToInt() * 2, intensity.roundToInt() * 2) - val baselineX = position.x.roundToInt() - intensity.roundToInt() - val baselineY = position.y.roundToInt() - intensity.roundToInt() - - val dirs = chooseLightRayFan(intensity) - val mul = 1.0 / dirs.size - - for (dir in dirs) { - val result2 = rayLightNaive(position, dir, intensity, falloffByTile, falloffByTravel) - - for (pair in result2) { - combinedResult[pair.second.y - baselineY, pair.second.x - baselineX] += pair.first * mul - } - } - - return combinedResult - } - } - - /** - * @see CachedGetter.castRayNaive - */ - fun castRayNaive( - rayStart: Vector2d, - rayEnd: Vector2d, - filter: TileRayFilter = AnythingRayFilter - ): RayCastResult { - return CachedGetter().castRayNaive(rayStart, rayEnd, filter) - } - - /** - * @see CachedGetter.castRayNaive - */ - fun castRayNaive( - rayPosition: Vector2d, - direction: Vector2d, - length: Double, - filter: TileRayFilter = AnythingRayFilter - ): RayCastResult { - return CachedGetter().castRayNaive(rayPosition, direction, length, filter) - } - - /** - * @see CachedGetter.rayLightNaive - */ - fun rayLightNaive( - position: Vector2d, - direction: Vector2d, - intensity: Double, - falloffByTile: Double = 2.0, - falloffByTravel: Double = 1.0, - ): List> { - return CachedGetter().rayLightNaive(position, direction, intensity, falloffByTile, falloffByTravel) - } - - /** - * @see CachedGetter.rayLightCircleNaive - */ - fun rayLightCircleNaive( - position: Vector2d, - intensity: Double, - falloffByTile: Double = 2.0, - falloffByTravel: Double = 2.0, - ): Double2DArray { - return CachedGetter().rayLightCircleNaive(position, intensity, falloffByTile, falloffByTravel) - } + protected abstract fun chunkFactory(pos: ChunkPos): ChunkType fun getView(pos: ChunkPos): CellView { - val tuple = get(pos)?.let { InstantWorldChunkTuple(this as This, it) } - return CellView( pos = pos, - center = tuple?.chunk, - left = tuple?.left?.chunk, - top = tuple?.top?.chunk, - topLeft = tuple?.topLeft?.chunk, - topRight = tuple?.topRight?.chunk, - right = tuple?.right?.chunk, - bottom = tuple?.bottom?.chunk, - bottomLeft = tuple?.bottomLeft?.chunk, - bottomRight = tuple?.bottomRight?.chunk, + center = chunkMap[pos], + left = chunkMap[pos.left], + top = chunkMap[pos.top], + topLeft = chunkMap[pos.topLeft], + topRight = chunkMap[pos.topRight], + right = chunkMap[pos.right], + bottom = chunkMap[pos.bottom], + bottomLeft = chunkMap[pos.bottomLeft], + bottomRight = chunkMap[pos.bottomRight], ) } @@ -567,9 +400,9 @@ abstract class World, ChunkType : Chunk() for (pos in boundingBox.chunkPositions) { - val chunk = get(pos) + val chunk = chunkMap[pos] - if (chunk != null) { + if (chunk != null && chunk !in output) { output.add(chunk) } } @@ -584,9 +417,9 @@ abstract class World, ChunkType : Chunk>() for (pos in boundingBox.chunkPositions) { - val chunk = get(pos) + val chunk = chunkMap[pos] - if (chunk != null) { + if (chunk != null && !output.any { it.second === chunk }) { output.add(pos to chunk) } } @@ -715,7 +548,7 @@ abstract class World, ChunkType : Chunk) : IEntity { return } - val chunkPos = if (world.isCircular) ChunkPos.fromTilePosition(position, world.widthInChunks) else ChunkPos.fromTilePosition(position) + val chunkPos = world.chunkMap.cellToGrid(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})") @@ -121,16 +120,16 @@ abstract class Entity(override val world: World<*, *>) : IEntity { return val old = field - field = value + field = Vector2d(world.chunkMap.x.cell(value.x), world.chunkMap.x.cell(value.y)) movement.notifyPositionChanged() if (isSpawned && !isRemoved) { - val oldChunkPos = ChunkPos.fromTilePosition(old) - val newChunkPos = ChunkPos.fromTilePosition(value) + val oldChunkPos = world.chunkMap.cellToGrid(old) + val newChunkPos = world.chunkMap.cellToGrid(field) if (oldChunkPos != newChunkPos) { - chunk = world[newChunkPos] + chunk = world.chunkMap[newChunkPos] } } } @@ -155,7 +154,7 @@ abstract class Entity(override val world: World<*, *>) : IEntity { isSpawned = true world.entities.add(this) - chunk = world[ChunkPos.fromTilePosition(position)] + chunk = world.chunkMap[world.chunkMap.cellToGrid(position)] if (chunk == null) { world.orphanedEntities.add(this) diff --git a/src/test/kotlin/ru/dbotthepony/kstarbound/test/MathTests.kt b/src/test/kotlin/ru/dbotthepony/kstarbound/test/MathTests.kt index ff021b59..34552e93 100644 --- a/src/test/kotlin/ru/dbotthepony/kstarbound/test/MathTests.kt +++ b/src/test/kotlin/ru/dbotthepony/kstarbound/test/MathTests.kt @@ -51,21 +51,21 @@ object MathTests { @Test @DisplayName("ChunkPos class tests") fun chunkPosTests() { - check(ChunkPos.fromTilePosition(0, 0) == ChunkPos(0, 0)) - check(ChunkPos.fromTilePosition(1, 0) == ChunkPos(0, 0)) - check(ChunkPos.fromTilePosition(0, 1) == ChunkPos(0, 0)) - check(ChunkPos.fromTilePosition(1, 1) == ChunkPos(0, 0)) - check(ChunkPos.fromTilePosition(-1, 1) == ChunkPos(-1, 0)) - check(ChunkPos.fromTilePosition(-1, -1) == ChunkPos(-1, -1)) + check(ChunkPos.fromPosition(0, 0) == ChunkPos(0, 0)) + check(ChunkPos.fromPosition(1, 0) == ChunkPos(0, 0)) + check(ChunkPos.fromPosition(0, 1) == ChunkPos(0, 0)) + check(ChunkPos.fromPosition(1, 1) == ChunkPos(0, 0)) + check(ChunkPos.fromPosition(-1, 1) == ChunkPos(-1, 0)) + check(ChunkPos.fromPosition(-1, -1) == ChunkPos(-1, -1)) - check(ChunkPos.fromTilePosition(CHUNK_SIZE_FF, 0) == ChunkPos(0, 0)) - check(ChunkPos.fromTilePosition(CHUNK_SIZE, 0) == ChunkPos(1, 0)) - check(ChunkPos.fromTilePosition(0, CHUNK_SIZE_FF) == ChunkPos(0, 0)) - check(ChunkPos.fromTilePosition(0, CHUNK_SIZE) == ChunkPos(0, 1)) + check(ChunkPos.fromPosition(CHUNK_SIZE_FF, 0) == ChunkPos(0, 0)) + check(ChunkPos.fromPosition(CHUNK_SIZE, 0) == ChunkPos(1, 0)) + check(ChunkPos.fromPosition(0, CHUNK_SIZE_FF) == ChunkPos(0, 0)) + check(ChunkPos.fromPosition(0, CHUNK_SIZE) == ChunkPos(0, 1)) - check(ChunkPos.fromTilePosition(-CHUNK_SIZE_FF, 0) == ChunkPos(-1, 0)) { ChunkPos.fromTilePosition(-CHUNK_SIZE_FF, 0).toString() } - check(ChunkPos.fromTilePosition(-CHUNK_SIZE, 0) == ChunkPos(-2, 0)) { ChunkPos.fromTilePosition(-CHUNK_SIZE, 0) } - check(ChunkPos.fromTilePosition(0, -CHUNK_SIZE_FF) == ChunkPos(0, -1)) { ChunkPos.fromTilePosition(0, -CHUNK_SIZE_FF) } - check(ChunkPos.fromTilePosition(0, -CHUNK_SIZE) == ChunkPos(0, -2)) { ChunkPos.fromTilePosition(0, -CHUNK_SIZE) } + check(ChunkPos.fromPosition(-CHUNK_SIZE_FF, 0) == ChunkPos(-1, 0)) { ChunkPos.fromPosition(-CHUNK_SIZE_FF, 0).toString() } + check(ChunkPos.fromPosition(-CHUNK_SIZE, 0) == ChunkPos(-2, 0)) { ChunkPos.fromPosition(-CHUNK_SIZE, 0) } + check(ChunkPos.fromPosition(0, -CHUNK_SIZE_FF) == ChunkPos(0, -1)) { ChunkPos.fromPosition(0, -CHUNK_SIZE_FF) } + check(ChunkPos.fromPosition(0, -CHUNK_SIZE) == ChunkPos(0, -2)) { ChunkPos.fromPosition(0, -CHUNK_SIZE) } } }