From c3863d8ea2688cd16631352fcf13c13adeee31e1 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Mon, 7 Feb 2022 11:08:54 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9E=D1=82=D1=80=D0=B8=D1=81=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=81=D1=83=D1=89=D0=BD=D0=BE=D1=81=D1=82=D0=B5?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 8 +- .../kstarbound/client/ClientChunk.kt | 312 ++++++++++++++++++ .../kstarbound/client/ClientWorld.kt | 52 +-- .../kstarbound/client/StarboundClient.kt | 3 - .../kstarbound/client/gl/GLStateTracker.kt | 6 +- .../client/render/BakedProgramState.kt | 3 + .../kstarbound/client/render/ChunkRenderer.kt | 287 ---------------- .../client/render/EntityRenderer.kt | 33 ++ ...yeredRenderTree.kt => ILayeredRenderer.kt} | 4 +- .../ru/dbotthepony/kstarbound/math/Vector.kt | 1 + .../ru/dbotthepony/kstarbound/world/Chunk.kt | 51 ++- .../ru/dbotthepony/kstarbound/world/World.kt | 165 ++++----- .../kstarbound/world/entities/AliveEntity.kt | 8 +- .../kstarbound/world/entities/Entity.kt | 43 ++- .../kstarbound/world/entities/PlayerEntity.kt | 2 +- 15 files changed, 533 insertions(+), 445 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt rename src/main/kotlin/ru/dbotthepony/kstarbound/client/render/{LayeredRenderTree.kt => ILayeredRenderer.kt} (95%) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index c8cd7b10..91d8529f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -30,7 +30,7 @@ fun main() { Starbound.terminateLoading = true } - var chunkA: Chunk? = null + var chunkA: Chunk<*, *>? = null val ent = PlayerEntity(client.world!!) @@ -122,12 +122,6 @@ fun main() { client.camera.pos.y = ent.pos.y.toFloat() } - client.onPostDrawWorld { - client.gl.quadWireframe { - it.quad(ent.worldaabb) - } - } - ent.spawn() while (client.renderFrame()) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt new file mode 100644 index 00000000..49be590e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt @@ -0,0 +1,312 @@ +package ru.dbotthepony.kstarbound.client + +import ru.dbotthepony.kstarbound.client.gl.GLStateTracker +import ru.dbotthepony.kstarbound.client.render.BakedStaticMesh +import ru.dbotthepony.kstarbound.client.render.EntityRenderer +import ru.dbotthepony.kstarbound.client.render.ILayeredRenderer +import ru.dbotthepony.kstarbound.client.render.TileLayerList +import ru.dbotthepony.kstarbound.math.Matrix4fStack +import ru.dbotthepony.kstarbound.math.Vector2d +import ru.dbotthepony.kstarbound.world.* +import ru.dbotthepony.kstarbound.world.entities.Entity +import java.io.Closeable + +/** + * Псевдо zPos у фоновых тайлов + * + * Добавление этого числа к zPos гарантирует, что фоновые тайлы будут отрисованы + * первыми (на самом дальнем плане) + */ +const val Z_LEVEL_BACKGROUND = 60000 + +class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk(world, pos), Closeable, ILayeredRenderer { + val state: GLStateTracker get() = world.client.gl + + private inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable { + private val layers = TileLayerList() + val bakedMeshes = ArrayList>() + private var changeset = -1 + + fun bake(view: ITileChunk) { + if (state.isSameThread()) { + for (mesh in bakedMeshes) { + mesh.first.close() + } + + bakedMeshes.clear() + } else { + for (mesh in bakedMeshes) { + unloadableBakedMeshes.add(mesh.first) + } + + bakedMeshes.clear() + } + + layers.clear() + + for ((pos, tile) in view.posToTile) { + if (tile != null) { + val renderer = state.tileRenderers.get(tile.def.materialName) + renderer.tesselate(view, layers, pos, background = isBackground) + } + } + } + + fun loadRenderers(view: ITileChunk) { + for ((_, tile) in view.posToTile) { + if (tile != null) { + state.tileRenderers.get(tile.def.materialName) + } + } + } + + fun uploadStatic(clear: Boolean = true) { + for ((baked, builder, zLevel) in layers.buildList()) { + bakedMeshes.add(BakedStaticMesh(baked, builder) to zLevel) + } + + if (clear) { + layers.clear() + } + } + + fun render(stack: Matrix4fStack) { + val transform = stack.last + + for (mesh in bakedMeshes) { + mesh.first.render(transform) + } + } + + fun autoBake(provider: () -> ITileChunk) { + if (changeset != layerChangeset.invoke()) { + this.bake(provider.invoke()) + changeset = layerChangeset.invoke() + } + } + + fun autoUpload() { + if (layers.isNotEmpty) { + for (mesh in bakedMeshes) { + mesh.first.close() + } + + bakedMeshes.clear() + + for ((baked, builder, zLevel) in layers.buildList()) { + bakedMeshes.add(BakedStaticMesh(baked, builder) to zLevel) + } + + layers.clear() + } + } + + fun autoBakeAndUpload(provider: () -> ITileChunk) { + autoBake(provider) + autoUpload() + } + + fun bakeAndRender(transform: Matrix4fStack, provider: () -> ITileChunk) { + autoBakeAndUpload(provider) + render(transform) + } + + override fun close() { + for (mesh in bakedMeshes) { + mesh.first.close() + } + } + } + + val debugCollisions get() = world.client.settings.debugCollisions + val posVector2d = Vector2d(x = pos.x * CHUNK_SIZEd, y = pos.y * CHUNK_SIZEd) + + private val unloadableBakedMeshes = ArrayList() + + private val foregroundRenderer = TileLayerRenderer(foreground::changeset, isBackground = false) + private val backgroundRenderer = TileLayerRenderer(background::changeset, isBackground = true) + + private fun getForegroundView(): ITileChunk { + return world.getForegroundView(pos)!! + } + + private fun getBackgroundView(): ITileChunk { + return world.getBackgroundView(pos)!! + } + + /** + * Принудительно подгружает в GLStateTracker все необходимые рендереры (ибо им нужны текстуры и прочее) + * + * Вызывается перед tesselateStatic() + */ + fun loadRenderers() { + unloadUnused() + + foregroundRenderer.loadRenderers(getForegroundView()) + backgroundRenderer.loadRenderers(getBackgroundView()) + } + + private fun unloadUnused() { + if (unloadableBakedMeshes.size != 0) { + for (baked in unloadableBakedMeshes) { + baked.close() + } + + unloadableBakedMeshes.clear() + } + } + + /** + * Отрисовывает всю геометрию напрямую + */ + fun render(stack: Matrix4fStack) { + unloadUnused() + + backgroundRenderer.render(stack) + foregroundRenderer.render(stack) + } + + /** + * Отрисовывает всю геометрию напрямую, с проверкой, изменился ли чанк + */ + fun bakeAndRender(stack: Matrix4fStack) { + unloadUnused() + + backgroundRenderer.bakeAndRender(stack, this::getBackgroundView) + foregroundRenderer.bakeAndRender(stack, this::getForegroundView) + } + + /** + * Тесселирует "статичную" геометрию в builders (к примеру тайлы), с проверкой, изменилось ли что либо, + * и загружает её в видеопамять. + * + * Может быть вызван вне рендер потока (ибо в любом случае он требует некой "стаитичности" данных в чанке) + * но только если до этого был вызыван loadRenderers() и геометрия чанка не поменялась + * + */ + fun bake() { + if (state.isSameThread()) + unloadUnused() + + backgroundRenderer.autoBake(this::getBackgroundView) + foregroundRenderer.autoBake(this::getForegroundView) + + if (state.isSameThread()) + upload() + } + + /** + * Загружает в видеопамять всю геометрию напрямую, если есть что загружать + */ + fun upload() { + unloadUnused() + + backgroundRenderer.autoUpload() + foregroundRenderer.autoUpload() + } + + fun renderDebug() { + if (debugCollisions) { + state.quadWireframe { + it.quad(aabb.mins.x.toFloat(), aabb.mins.y.toFloat(), aabb.maxs.x.toFloat(), aabb.maxs.y.toFloat()) + + for (layer in foreground.collisionLayers()) { + it.quad(layer.mins.x.toFloat(), layer.mins.y.toFloat(), layer.maxs.x.toFloat(), layer.maxs.y.toFloat()) + } + } + } + + for (renderer in entityRenderers.values) { + renderer.renderDebug() + } + } + + private val layerQueue = ArrayDeque Unit, Int>>() + + override fun renderLayerFromStack(zPos: Int, stack: Matrix4fStack): Int { + if (layerQueue.isEmpty()) + return -1 + + stack.push().translateWithScale(x = pos.x * CHUNK_SIZEf, y = pos.y * CHUNK_SIZEf) + var pair = layerQueue.last() + + while (pair.second >= zPos) { + pair.first.invoke(stack) + + layerQueue.removeLast() + + if (layerQueue.isEmpty()) { + stack.pop() + return -1 + } + + pair = layerQueue.last() + } + + stack.pop() + return layerQueue.last().second + } + + override fun bottomMostZLevel(): Int { + if (layerQueue.isEmpty()) { + return -1 + } + + return layerQueue.last().second + } + + override fun prepareForLayeredRender() { + layerQueue.clear() + + for ((baked, zLevel) in backgroundRenderer.bakedMeshes) { + layerQueue.add(baked::renderStacked to (zLevel + Z_LEVEL_BACKGROUND)) + } + + for ((baked, zLevel) in foregroundRenderer.bakedMeshes) { + layerQueue.add(baked::renderStacked to zLevel) + } + + for (renderer in entityRenderers.values) { + layerQueue.add(lambda@{ it: Matrix4fStack -> + val relative = renderer.renderPos - posVector2d + it.push().translateWithScale(relative.x.toFloat(), relative.y.toFloat()) + renderer.render(it) + it.pop() + return@lambda + } to renderer.layer) + } + + layerQueue.sortBy { + return@sortBy it.second + } + } + + private val entityRenderers = HashMap() + + override fun onEntityAdded(entity: Entity) { + entityRenderers[entity] = EntityRenderer(state, entity, this) + } + + override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) { + val renderer = otherChunk.entityRenderers[entity] ?: throw IllegalStateException("$otherChunk has no renderer for $entity!") + entityRenderers[entity] = renderer + renderer.chunk = this + } + + override fun onEntityTransferedFromThis(entity: Entity, otherChunk: ClientChunk) { + entityRenderers.remove(entity) + } + + override fun onEntityRemoved(entity: Entity) { + entityRenderers.remove(entity)!!.close() + } + + override fun close() { + backgroundRenderer.close() + foregroundRenderer.close() + + for (renderer in entityRenderers.values) { + renderer.close() + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt index 47eaad74..2496a283 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt @@ -1,49 +1,14 @@ package ru.dbotthepony.kstarbound.client -import org.lwjgl.glfw.GLFW.glfwGetTime -import ru.dbotthepony.kstarbound.api.IStruct2d -import ru.dbotthepony.kstarbound.api.IStruct2f -import ru.dbotthepony.kstarbound.client.render.ChunkRenderer import ru.dbotthepony.kstarbound.client.render.renderLayeredList import ru.dbotthepony.kstarbound.math.AABB -import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.world.* -class ClientWorldChunkTuple( - world: World<*>, - chunk: Chunk, - top: IWorldChunkTuple?, - left: IWorldChunkTuple?, - right: IWorldChunkTuple?, - bottom: IWorldChunkTuple?, - - val renderer: ChunkRenderer -) : MutableWorldChunkTuple( - world, - chunk, - top, - left, - right, - bottom, -) - -class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World(seed) { - override fun tupleFactory( - chunk: Chunk, - top: IWorldChunkTuple?, - left: IWorldChunkTuple?, - right: IWorldChunkTuple?, - bottom: IWorldChunkTuple? - ): ClientWorldChunkTuple { - return ClientWorldChunkTuple( +class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World(seed) { + override fun chunkFactory(pos: ChunkPos): ClientChunk { + return ClientChunk( world = this, - chunk = chunk, - top = top, - left = left, - right = right, - bottom = bottom, - - renderer = ChunkRenderer(client.gl, chunk, this) + pos = pos, ) } @@ -58,14 +23,11 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World() + val determineRenderers = ArrayList() for (chunk in collectInternal(size.encasingChunkPosAABB())) { - determineRenderers.add(chunk.renderer) - } - - for (renderer in determineRenderers) { - renderer.autoBakeStatic() + determineRenderers.add(chunk.chunk) + chunk.chunk.bake() } renderLayeredList(client.gl.matrixStack, determineRenderers) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index ed608d7f..0c4e954a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -198,9 +198,6 @@ class StarboundClient : AutoCloseable { glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) gl.matrixStack.clear(viewportMatrixGame.toMutableMatrix()) - val mins = Vector2f((-viewportWidth / 2f) / settings.scale, (-viewportHeight / 2f) / settings.scale) - val maxs = -mins - gl.matrixStack.push() .translateWithScale(viewportWidth / 2f, viewportHeight / 2f, 2f) // центр экрана + координаты отрисовки мира .scale(x = settings.scale * PIXELS_IN_STARBOUND_UNITf, y = settings.scale * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt index b3977c71..40590ab4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt @@ -111,12 +111,12 @@ class GLStateTracker { var cleanManual = false val cleanable = cleaner.register(ref) { + if (!cleanManual) + LOGGER.error("{} with ID {} got leaked.", name, nativeRef) + cleanerHits.add { fn(nativeRef) checkForGLError() - - if (!cleanManual) - LOGGER.error("{} with ID {} got leaked.", name, nativeRef) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt index ba343b9d..e2252a13 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt @@ -6,6 +6,7 @@ import ru.dbotthepony.kstarbound.client.gl.GLVertexArrayObject import ru.dbotthepony.kstarbound.client.gl.DynamicVertexBuilder import ru.dbotthepony.kstarbound.client.gl.checkForGLError import ru.dbotthepony.kstarbound.math.FloatMatrix +import ru.dbotthepony.kstarbound.math.Matrix4fStack /** * Служит для быстрой настройки состояния для будущей отрисовки @@ -83,6 +84,8 @@ class BakedStaticMesh( checkForGLError() } + fun renderStacked(transform: Matrix4fStack) = render(transform.last) + var isValid = true private set diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt deleted file mode 100644 index a4c8b6c2..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt +++ /dev/null @@ -1,287 +0,0 @@ -package ru.dbotthepony.kstarbound.client.render - -import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf -import ru.dbotthepony.kstarbound.client.ClientWorld -import ru.dbotthepony.kstarbound.client.gl.GLStateTracker -import ru.dbotthepony.kstarbound.math.FloatMatrix -import ru.dbotthepony.kstarbound.math.Matrix4f -import ru.dbotthepony.kstarbound.math.Matrix4fStack -import ru.dbotthepony.kstarbound.world.CHUNK_SIZE -import ru.dbotthepony.kstarbound.world.CHUNK_SIZEf -import ru.dbotthepony.kstarbound.world.Chunk -import ru.dbotthepony.kstarbound.world.ITileChunk -import kotlin.collections.ArrayList - -/** - * Псевдо zPos у фоновых тайлов - * - * Добавление этого числа к zPos гарантирует, что фоновые тайлы будут отрисованы - * первыми (на самом дальнем плане) - */ -const val Z_LEVEL_BACKGROUND = 60000 - -class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: ClientWorld? = null) : AutoCloseable, ILayeredRenderer { - private inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable { - private val layers = TileLayerList() - val bakedMeshes = ArrayList>() - private var changeset = -1 - - fun tesselateStatic(view: ITileChunk) { - if (state.isSameThread()) { - for (mesh in bakedMeshes) { - mesh.first.close() - } - - bakedMeshes.clear() - } else { - for (mesh in bakedMeshes) { - unloadableBakedMeshes.add(mesh.first) - } - - bakedMeshes.clear() - } - - layers.clear() - - for ((pos, tile) in view.posToTile) { - if (tile != null) { - val renderer = state.tileRenderers.get(tile.def.materialName) - renderer.tesselate(view, layers, pos, background = isBackground) - } - } - } - - fun loadRenderers(view: ITileChunk) { - for ((_, tile) in view.posToTile) { - if (tile != null) { - state.tileRenderers.get(tile.def.materialName) - } - } - } - - fun uploadStatic(clear: Boolean = true) { - for ((baked, builder, zLevel) in layers.buildList()) { - bakedMeshes.add(BakedStaticMesh(baked, builder) to zLevel) - } - - if (clear) { - layers.clear() - } - } - - fun render(transform: FloatMatrix<*>) { - for (mesh in bakedMeshes) { - mesh.first.render(transform) - } - } - - fun bakeAndRender(transform: FloatMatrix<*>, provider: () -> ITileChunk) { - if (changeset != layerChangeset.invoke()) { - this.tesselateStatic(provider.invoke()) - this.uploadStatic() - - changeset = layerChangeset.invoke() - } - - render(transform) - } - - fun autoBake(provider: () -> ITileChunk) { - if (changeset != layerChangeset.invoke()) { - this.tesselateStatic(provider.invoke()) - this.uploadStatic() - - changeset = layerChangeset.invoke() - } - } - - fun autoUpload() { - if (layers.isNotEmpty) { - for (mesh in bakedMeshes) { - mesh.first.close() - } - - bakedMeshes.clear() - - for ((baked, builder, zLevel) in layers.buildList()) { - bakedMeshes.add(BakedStaticMesh(baked, builder) to zLevel) - } - - layers.clear() - } - } - - override fun close() { - for (mesh in bakedMeshes) { - mesh.first.close() - } - } - } - - val debugCollisions get() = world?.client?.settings?.debugCollisions ?: false - - val transform = Matrix4f().translate(x = chunk.pos.x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf, y = chunk.pos.x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf) - - private val unloadableBakedMeshes = ArrayList() - - private val foreground = TileLayerRenderer(chunk.foreground::changeset, isBackground = false) - private val background = TileLayerRenderer(chunk.background::changeset, isBackground = true) - - private fun getForeground(): ITileChunk { - return world?.getForegroundView(chunk.pos) ?: chunk.foreground - } - - private fun getBackground(): ITileChunk { - return world?.getBackgroundView(chunk.pos) ?: chunk.background - } - - /** - * Тесселирует "статичную" геометрию в builders (к примеру тайлы). - * - * Может быть вызван вне рендер потока (ибо в любом случае он требует некой "стаитичности" данных в чанке) - * но только если до этого был вызыван loadRenderers() и геометрия чанка не поменялась - */ - fun tesselateStatic() { - foreground.tesselateStatic(getForeground()) - background.tesselateStatic(getBackground()) - } - - /** - * Принудительно подгружает в GLStateTracker все необходимые рендереры (ибо им нужны текстуры и прочее) - * - * Вызывается перед tesselateStatic() - */ - fun loadRenderers() { - unloadUnused() - - foreground.loadRenderers(getForeground()) - background.loadRenderers(getBackground()) - } - - private fun unloadUnused() { - if (unloadableBakedMeshes.size != 0) { - for (baked in unloadableBakedMeshes) { - baked.close() - } - - unloadableBakedMeshes.clear() - } - } - - fun uploadStatic(clear: Boolean = true) { - unloadUnused() - - foreground.uploadStatic(clear) - background.uploadStatic(clear) - } - - /** - * Отрисовывает всю геометрию напрямую - */ - fun render(transform: FloatMatrix<*> = state.matrixStack.last) { - unloadUnused() - - background.render(transform) - foreground.render(transform) - } - - /** - * Отрисовывает всю геометрию напрямую, с проверкой, изменился ли чанк - */ - fun bakeAndRender(transform: FloatMatrix<*> = state.matrixStack.last) { - unloadUnused() - - background.bakeAndRender(transform, this::getBackground) - foreground.bakeAndRender(transform, this::getForeground) - } - - /** - * Запекает всю геометрию напрямую, с проверкой, изменился ли чанк, - * и загружает её, если вызвано в рендер потоке - */ - fun autoBakeStatic() { - if (state.isSameThread()) - unloadUnused() - - background.autoBake(this::getBackground) - foreground.autoBake(this::getForeground) - - if (state.isSameThread()) - autoUploadStatic() - } - - /** - * Загружает в видеопамять всю геометрию напрямую, если есть что загружать - */ - fun autoUploadStatic() { - unloadUnused() - - background.autoUpload() - foreground.autoUpload() - } - - fun renderDebug() { - if (debugCollisions) { - state.quadWireframe { - it.quad(chunk.aabb.mins.x.toFloat(), chunk.aabb.mins.y.toFloat(), chunk.aabb.maxs.x.toFloat(), chunk.aabb.maxs.y.toFloat()) - - for (layer in chunk.foreground.collisionLayers()) { - it.quad(layer.mins.x.toFloat(), layer.mins.y.toFloat(), layer.maxs.x.toFloat(), layer.maxs.y.toFloat()) - } - } - } - } - - private val meshDeque = ArrayDeque>() - - override fun renderLayerFromStack(zPos: Int, transform: Matrix4fStack): Int { - if (meshDeque.isEmpty()) - return -1 - - transform.push().translateWithScale(x = chunk.pos.x * CHUNK_SIZEf, y = chunk.pos.y * CHUNK_SIZEf) - var pair = meshDeque.last() - - while (pair.second >= zPos) { - pair.first.render(transform.last) - - meshDeque.removeLast() - - if (meshDeque.isEmpty()) { - transform.pop() - return -1 - } - - pair = meshDeque.last() - } - - transform.pop() - return meshDeque.last().second - } - - override fun bottomMostZLevel(): Int { - if (meshDeque.isEmpty()) { - return -1 - } - - return meshDeque.last().second - } - - override fun prepareForLayeredRender() { - meshDeque.clear() - - for ((baked, zLevel) in background.bakedMeshes) { - meshDeque.add(baked to (zLevel + Z_LEVEL_BACKGROUND)) - } - - meshDeque.addAll(foreground.bakedMeshes) - - meshDeque.sortBy { - return@sortBy it.second - } - } - - override fun close() { - background.close() - foreground.close() - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt new file mode 100644 index 00000000..7ef388bc --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt @@ -0,0 +1,33 @@ +package ru.dbotthepony.kstarbound.client.render + +import ru.dbotthepony.kstarbound.client.ClientChunk +import ru.dbotthepony.kstarbound.client.gl.GLStateTracker +import ru.dbotthepony.kstarbound.math.Matrix4fStack +import ru.dbotthepony.kstarbound.math.Vector2d +import ru.dbotthepony.kstarbound.world.entities.Entity +import java.io.Closeable + +/** + * Базовый класс, отвечающий за отрисовку определённого ентити в мире + * + * Считается, что процесс отрисовки ограничен лишь одним слоем (т.е. отрисовка происходит в один проход) + */ +open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open var chunk: ClientChunk?) : Closeable { + open val renderPos: Vector2d get() = entity.pos + + open fun render(stack: Matrix4fStack) { + + } + + open fun renderDebug() { + if (chunk?.world?.client?.settings?.debugCollisions == true) { + state.quadWireframe(entity.worldaabb) + } + } + + open val layer: Int = 100 + + override fun close() { + + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderTree.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ILayeredRenderer.kt similarity index 95% rename from src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderTree.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ILayeredRenderer.kt index 6a9fe3d6..012e4168 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderTree.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ILayeredRenderer.kt @@ -1,7 +1,5 @@ package ru.dbotthepony.kstarbound.client.render -import ru.dbotthepony.kstarbound.math.FloatMatrix -import ru.dbotthepony.kstarbound.math.Matrix4f import ru.dbotthepony.kstarbound.math.Matrix4fStack /** @@ -24,7 +22,7 @@ interface ILayeredRenderer { * Если следующего слоя нет, вернуть -1, и данный объект * будет считаться отрисованным. */ - fun renderLayerFromStack(zPos: Int, transform: Matrix4fStack): Int + fun renderLayerFromStack(zPos: Int, stack: Matrix4fStack): Int /** * Возвращает наибольшее zPos в данной стопке. diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt index 5a7e779c..e11e4ccc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt @@ -342,6 +342,7 @@ abstract class IVector2d> : IMatrixLike, IMatrixLikeDouble, ISt } protected abstract fun make(x: Double, y: Double): T + fun toFloatVector(): Vector2f = Vector2f(x.toFloat(), y.toFloat()) } data class Vector2d(override val x: Double = 0.0, override val y: Double = 0.0) : IVector2d() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index f189f65c..67dbd2c2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -4,14 +4,16 @@ import ru.dbotthepony.kstarbound.api.IStruct2d import ru.dbotthepony.kstarbound.api.IStruct2i import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.math.* +import ru.dbotthepony.kstarbound.world.entities.Entity import java.util.* import kotlin.collections.ArrayList +import kotlin.collections.HashSet import kotlin.math.absoluteValue /** * Представляет из себя класс, который содержит состояние тайла на заданной позиции */ -data class ChunkTile(val chunk: Chunk.TileLayer, val def: TileDefinition) { +data class ChunkTile(val chunk: Chunk<*, *>.TileLayer, val def: TileDefinition) { var color = 0 set(value) { field = value @@ -322,7 +324,7 @@ class MutableTileChunkView( * * Весь игровой мир будет измеряться в Starbound Unit'ах */ -open class Chunk(val world: World<*>?, val pos: ChunkPos) { +abstract class Chunk, This : Chunk>(val world: WorldType, val pos: ChunkPos) { /** * Возвращает счётчик изменений чанка */ @@ -355,8 +357,6 @@ open class Chunk(val world: World<*>?, val pos: ChunkPos) { // TODO: https://ru.wikipedia.org/wiki/R-дерево_(структура_данных) private fun bakeCollisions() { collisionChangeset = changeset - val seen = BooleanArray(tiles.size) - collisionCache.clear() val xAdd = pos.x * CHUNK_SIZEd @@ -448,6 +448,49 @@ open class Chunk(val world: World<*>?, val pos: ChunkPos) { val foreground = TileLayer() val background = TileLayer() + protected val entities = HashSet() + val entitiesAccess = Collections.unmodifiableSet(entities) + + protected abstract fun onEntityAdded(entity: Entity) + protected abstract fun onEntityTransferedToThis(entity: Entity, otherChunk: This) + protected abstract fun onEntityTransferedFromThis(entity: Entity, otherChunk: This) + protected abstract fun onEntityRemoved(entity: Entity) + + fun addEntity(entity: Entity) { + if (!entities.add(entity)) { + throw IllegalArgumentException("Already having having entity $entity") + } + + onEntityAdded(entity) + } + + fun transferEntity(entity: Entity, otherChunk: Chunk<*, *>) { + if (otherChunk == this) + throw IllegalArgumentException("what?") + + if (this::class.java != otherChunk::class.java) { + throw IllegalArgumentException("Incompatible types: $this !is $otherChunk") + } + + if (!entities.add(entity)) { + throw IllegalArgumentException("Already containing $entity") + } + + onEntityTransferedToThis(entity, otherChunk as This) + otherChunk.onEntityTransferedFromThis(entity, this as This) + + if (!otherChunk.entities.remove(entity)) { + throw IllegalStateException("Unable to remove $entity from $otherChunk after transfer") + } + } + + fun removeEntity(entity: Entity) { + if (!entities.remove(entity)) { + throw IllegalArgumentException("Already not having entity $entity") + } + + onEntityRemoved(entity) + } companion object { val EMPTY = object : IMutableTileChunk { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 3b234c56..6c949571 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -8,41 +8,78 @@ import ru.dbotthepony.kstarbound.world.entities.Entity import kotlin.math.absoluteValue /** - * Возвращает кортеж чанка, который содержит родителя (мир) и соседей (кортежи чанков) + * Кортеж чанка, который содержит родителя (мир) и соседей (кортежи чанков) */ -interface IWorldChunkTuple { - val world: World<*> - val chunk: Chunk - val top: IWorldChunkTuple? - val left: IWorldChunkTuple? - val right: IWorldChunkTuple? - val bottom: IWorldChunkTuple? +interface IWorldChunkTuple, ChunkType : Chunk> { + val world: WorldType + val chunk: ChunkType + val top: IWorldChunkTuple? + val left: IWorldChunkTuple? + val right: IWorldChunkTuple? + val bottom: IWorldChunkTuple? } -interface IMutableWorldChunkTuple : IWorldChunkTuple { - override var top: IWorldChunkTuple? - override var left: IWorldChunkTuple? - override var right: IWorldChunkTuple? - override var bottom: IWorldChunkTuple? +interface IMutableWorldChunkTuple, ChunkType : Chunk> : IWorldChunkTuple { + override var top: IMutableWorldChunkTuple? + override var left: IMutableWorldChunkTuple? + override var right: IMutableWorldChunkTuple? + override var bottom: IMutableWorldChunkTuple? } -data class WorldChunkTuple( - override val world: World<*>, - override val chunk: Chunk, - override val top: IWorldChunkTuple?, - override val left: IWorldChunkTuple?, - override val right: IWorldChunkTuple?, - override val bottom: IWorldChunkTuple?, -) : IWorldChunkTuple +class WorldChunkTuple, ChunkType : Chunk>( + private val parent: IWorldChunkTuple +) : IWorldChunkTuple { + override val world get() = parent.world + override val chunk get() = parent.chunk + override val top: IWorldChunkTuple? get() { + val getValue = parent.top -open class MutableWorldChunkTuple( - override val world: World<*>, - override val chunk: Chunk, - override var top: IWorldChunkTuple?, - override var left: IWorldChunkTuple?, - override var right: IWorldChunkTuple?, - override var bottom: IWorldChunkTuple?, -) : IMutableWorldChunkTuple + if (getValue != null) { + return WorldChunkTuple(getValue) + } + + return null + } + + override val left: IWorldChunkTuple? get() { + val getValue = parent.left + + if (getValue != null) { + return WorldChunkTuple(getValue) + } + + return null + } + + override val right: IWorldChunkTuple? get() { + val getValue = parent.right + + if (getValue != null) { + return WorldChunkTuple(getValue) + } + + return null + } + + override val bottom: IWorldChunkTuple? get() { + val getValue = parent.bottom + + if (getValue != null) { + return WorldChunkTuple(getValue) + } + + return null + } +} + +open class MutableWorldChunkTuple, ChunkType : Chunk>( + override val world: WorldType, + override val chunk: ChunkType, + override var top: IMutableWorldChunkTuple?, + override var left: IMutableWorldChunkTuple?, + override var right: IMutableWorldChunkTuple?, + override var bottom: IMutableWorldChunkTuple?, +) : IMutableWorldChunkTuple const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT @@ -55,9 +92,9 @@ data class WorldSweepResult( private const val EPSILON = 0.00001 -abstract class World(val seed: Long = 0L) { - protected val chunkMap = HashMap() - protected var lastAccessedChunk: T? = null +abstract class World, ChunkType : Chunk>(val seed: Long = 0L) { + protected val chunkMap = HashMap>() + protected var lastAccessedChunk: IMutableWorldChunkTuple? = null /** * Таймер этого мира, в секундах. @@ -106,15 +143,11 @@ abstract class World(val seed: Long = 0L) { */ var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION) - protected abstract fun tupleFactory( - chunk: Chunk, - top: IWorldChunkTuple?, - left: IWorldChunkTuple?, - right: IWorldChunkTuple?, - bottom: IWorldChunkTuple?, - ): T + protected abstract fun chunkFactory( + pos: ChunkPos, + ): ChunkType - protected fun getChunkInternal(pos: ChunkPos): T? { + protected fun getChunkInternal(pos: ChunkPos): IMutableWorldChunkTuple? { if (lastAccessedChunk?.chunk?.pos == pos) { return lastAccessedChunk } @@ -122,36 +155,30 @@ abstract class World(val seed: Long = 0L) { return chunkMap[pos] } - open fun getChunk(pos: ChunkPos): IWorldChunkTuple? { + open fun getChunk(pos: ChunkPos): IWorldChunkTuple? { val getTuple = getChunkInternal(pos) if (getTuple != null) - return WorldChunkTuple( - world = getTuple.world, - chunk = getTuple.chunk, - top = getTuple.top, - left = getTuple.left, - right = getTuple.right, - bottom = getTuple.bottom, - ) + return WorldChunkTuple(getTuple) return null } - protected open fun computeIfAbsentInternal(pos: ChunkPos): T { + protected open fun computeIfAbsentInternal(pos: ChunkPos): IWorldChunkTuple { if (lastAccessedChunk?.chunk?.pos == pos) { return lastAccessedChunk!! } return chunkMap.computeIfAbsent(pos) lazy@{ - val chunk = Chunk(this, pos) + val chunk = chunkFactory(pos) val top = getChunkInternal(pos.up()) val left = getChunkInternal(pos.left()) val right = getChunkInternal(pos.right()) val bottom = getChunkInternal(pos.down()) - val tuple = tupleFactory( + val tuple = MutableWorldChunkTuple( + world = this as This, chunk = chunk, top = top, left = left, @@ -184,17 +211,8 @@ abstract class World(val seed: Long = 0L) { } } - open fun computeIfAbsent(pos: ChunkPos): IWorldChunkTuple { - val getTuple = computeIfAbsentInternal(pos) - - return WorldChunkTuple( - world = getTuple.world, - chunk = getTuple.chunk, - top = getTuple.top, - left = getTuple.left, - right = getTuple.right, - bottom = getTuple.bottom, - ) + open fun computeIfAbsent(pos: ChunkPos): IWorldChunkTuple { + return WorldChunkTuple(computeIfAbsentInternal(pos)) } open fun getForegroundView(pos: ChunkPos): TileChunkView? { @@ -233,7 +251,7 @@ abstract class World(val seed: Long = 0L) { return getChunkInternal(ChunkPos.fromTilePosition(pos))?.chunk?.foreground?.get(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)) } - fun setTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple { + fun setTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple { val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos)) chunk.chunk.foreground[ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)] = tile return chunk @@ -243,14 +261,14 @@ abstract class World(val seed: Long = 0L) { return getChunkInternal(ChunkPos.fromTilePosition(pos))?.chunk?.background?.get(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)) } - fun setBackgroundTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple { + fun setBackgroundTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple { val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos)) chunk.chunk.background[ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)] = tile return chunk } - protected open fun collectInternal(boundingBox: AABBi): List { - val output = ArrayList() + protected open fun collectInternal(boundingBox: AABBi): List> { + val output = ArrayList>() for (pos in boundingBox.chunkPositions) { val chunk = getChunkInternal(pos) @@ -266,18 +284,11 @@ abstract class World(val seed: Long = 0L) { /** * Возвращает все чанки, которые пересекаются с заданным [boundingBox] */ - open fun collect(boundingBox: AABBi): List { - val output = ArrayList() + open fun collect(boundingBox: AABBi): List> { + val output = ArrayList>() for (chunk in collectInternal(boundingBox)) { - output.add(WorldChunkTuple( - world = chunk.world, - chunk = chunk.chunk, - top = chunk.top, - left = chunk.left, - right = chunk.right, - bottom = chunk.bottom, - )) + output.add(WorldChunkTuple(chunk)) } return output diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt index ca64cdce..0cef5e00 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt @@ -12,7 +12,7 @@ enum class Move { MOVE_RIGHT } -open class AliveEntity(world: World<*>) : Entity(world) { +open class AliveEntity(world: World<*, *>) : Entity(world) { open var maxHealth = 10.0 open var health = 10.0 open val moveDirection = Move.STAND_STILL @@ -20,12 +20,12 @@ open class AliveEntity(world: World<*>) : Entity(world) { open val aabbDucked get() = aabb - override val worldaabb: AABB get() { + override val currentaabb: AABB get() { if (isDucked) { - return aabbDucked + pos + return aabbDucked } - return super.worldaabb + return super.currentaabb } var wantsToDuck = false diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt index 8c20665a..22b0560a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.world.entities import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.client.render.EntityRenderer import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.math.lerp @@ -18,10 +19,10 @@ enum class CollisionResolution { /** * Определяет из себя сущность в мире, которая имеет позицию, скорость и коробку столкновений */ -open class Entity(val world: World<*>) { - var chunk: Chunk? = null +open class Entity(val world: World<*, *>) { + var chunk: Chunk<*, *>? = null set(value) { - if (!spawned) { + if (!isSpawned) { throw IllegalStateException("Trying to set chunk this entity belong to before spawning in world") } @@ -35,17 +36,22 @@ open class Entity(val world: World<*>) { throw IllegalStateException("Set proper position before setting chunk this Entity belongs to") } - val oldChunk = chunk + val oldChunk = field field = value 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) } } - open val worldaabb: AABB get() = aabb + pos + open val currentaabb: AABB get() = aabb + open val worldaabb: AABB get() = currentaabb + pos var pos = Vector2d() set(value) { @@ -55,7 +61,7 @@ open class Entity(val world: World<*>) { val old = field field = value - if (spawned) { + if (isSpawned) { val oldChunkPos = ChunkPos.fromTilePosition(old) val newChunkPos = ChunkPos.fromTilePosition(value) @@ -66,21 +72,36 @@ open class Entity(val world: World<*>) { } var velocity = Vector2d() - private var spawned = false + var isSpawned = false + private set + var isRemoved = false + private set fun spawn() { - if (spawned) + if (isSpawned) throw IllegalStateException("Already spawned") - spawned = true + isSpawned = true world.entities.add(this) - chunk = world.getChunk(ChunkPos.ZERO)?.chunk + chunk = world.getChunk(ChunkPos.fromTilePosition(pos))?.chunk if (chunk == null) { world.orphanedEntities.add(this) } } + fun remove() { + if (isRemoved) + throw IllegalStateException("Already removed") + + isRemoved = true + + if (isSpawned) { + world.entities.remove(this) + chunk?.removeEntity(this) + } + } + /** * Касается ли сущность земли * @@ -170,7 +191,7 @@ open class Entity(val world: World<*>) { * Заставляет сущность "думать". */ fun think(delta: Double) { - if (!spawned) { + if (!isSpawned) { throw IllegalStateException("Tried to think before spawning in world") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt index 9ad06ee7..85a0dd7f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt @@ -7,7 +7,7 @@ import ru.dbotthepony.kstarbound.world.World /** * Физический аватар игрока в мире */ -open class PlayerEntity(world: World<*>) : AliveEntity(world) { +open class PlayerEntity(world: World<*, *>) : AliveEntity(world) { override val aabb = AABB.rectangle(Vector2d.ZERO, 1.8, 3.7) override val aabbDucked: AABB = AABB.rectangle(Vector2d.ZERO, 1.8, 1.8) + Vector2d(y = -0.9) override var moveDirection = Move.STAND_STILL