From ff6dba143e6ca09b9fee9439a9056251a6c6f3a6 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 4 Feb 2022 19:34:48 +0700 Subject: [PATCH] =?UTF-8?q?=D0=91=D0=BE=D0=BB=D1=8C=D1=88=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2?= =?UTF-8?q?=20=D1=80=D0=B5=D0=BD=D0=B4=D0=B5=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 161 +++--------- .../ru/dbotthepony/kstarbound/Starbound.kt | 6 + .../kstarbound/client/ClientSettings.kt | 12 + .../kstarbound/client/ClientWorld.kt | 75 ++++++ .../kstarbound/client/StarboundClient.kt | 40 ++- .../kstarbound/client/gl/GLStateTracker.kt | 55 +++++ .../kstarbound/client/gl/GLTexture.kt | 9 +- .../client/gl/GLVertexArrayObject.kt | 12 +- .../client/gl/GLVertexBufferObject.kt | 10 +- .../client/render/BakedProgramState.kt | 4 +- .../kstarbound/client/render/Camera.kt | 44 +++- .../kstarbound/client/render/ChunkRenderer.kt | 165 +++++++------ .../kstarbound/client/render/Font.kt | 6 +- .../kstarbound/client/render/TileRenderer.kt | 29 ++- .../ru/dbotthepony/kstarbound/math/Vector.kt | 97 +++++++- .../ru/dbotthepony/kstarbound/world/Chunk.kt | 72 +++++- .../ru/dbotthepony/kstarbound/world/World.kt | 233 ++++++++++-------- 17 files changed, 687 insertions(+), 343 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientSettings.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index d2e48b56..6693ac79 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -10,35 +10,17 @@ import ru.dbotthepony.kstarbound.client.render.Camera import ru.dbotthepony.kstarbound.client.render.ChunkRenderer import ru.dbotthepony.kstarbound.client.render.TextAlignX import ru.dbotthepony.kstarbound.client.render.TextAlignY +import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.util.Color import ru.dbotthepony.kstarbound.util.formatBytesShort +import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF +import ru.dbotthepony.kstarbound.world.Chunk +import ru.dbotthepony.kstarbound.world.ChunkPos import java.io.File +import java.util.* private val LOGGER = LogManager.getLogger() -var viewportWidth = 800 - private set - -var viewportHeight = 600 - private set - -var viewportMatrixGUI = updateViewportMatrixA() - private set - -var viewportMatrixGame = updateViewportMatrixB() - private set - -private fun updateViewportMatrixA(): Matrix4f { - return Matrix4f.ortho(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 0.1f, 100f) -} - -private fun updateViewportMatrixB(): Matrix4f { - return Matrix4f.orthoDirect(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 1f, 100f) -} - -var window = 0L - private set - fun main() { LOGGER.info("Running LWJGL ${Version.getVersion()}") @@ -54,128 +36,61 @@ fun main() { Starbound.terminateLoading = true } - while (client.renderFrame()) { - Starbound.pollCallbacks() - } -} + var chunkA: Chunk? = null -private var camera: Camera? = null -private val startupTextList = ArrayList() -private var finishStartupRendering = Long.MAX_VALUE - -var frameRenderTime = 1.0 - private set - -val framesPerSecond get() = 1.0 / frameRenderTime - -private fun loop() { - val client = StarboundClient() - val state = client.gl - startupTextList.add("Initialized OpenGL context") - camera = Camera() - - // Set the clear color - glClearColor(0.2f, 0.2f, 0.2f, 0.2f) - - state.blend = true - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - - var chunkRenderer: ChunkRenderer? = null - - /*Starbound.onInitialize { - val chunk = Starbound.world.getOrMakeChunk(Vector2i(2, 2)) + Starbound.onInitialize { + chunkA = client.world!!.computeIfAbsent(ChunkPos(0, 0)).chunk + val chunkB = client.world!!.computeIfAbsent(ChunkPos(-1, 0)).chunk var x = 0 var y = 0 for (tile in Starbound.tilesAccess.values) { - chunk.background[x, y + 1] = tile - chunk.background[x++, y] = tile - chunk.background[x, y + 1] = tile - chunk.background[x++, y] = tile - chunk.background[x, y + 1] = tile - chunk.background[x++, y] = tile - chunk.background[x, y + 1] = tile - chunk.background[x++, y] = tile - chunk.background[x, y + 1] = tile - chunk.background[x++, y] = tile - chunk.background[x, y + 1] = tile + chunkA!!.background[x, y + 1] = tile + chunkA!!.background[x++, y] = tile - if (x >= 32) { + if (x >= 31) { x = 0 y += 2 } } - val tile = Starbound.getTileDefinition("glass") + x = 0 + y = 0 - for (x in 0 .. 32) { - for (y in 0 .. 32) { - chunk.foreground[x, y] = tile + for (tile in Starbound.tilesAccess.values) { + chunkB.foreground[x, y + 1] = tile + chunkB.foreground[x++, y] = tile + + if (x > 31) { + x = 0 + y += 2 + } + } + + val tile = Starbound.getTileDefinition("alienrock") + + for (x in 0 .. 31) { + for (y in 0 .. 31) { + chunkA!!.foreground[x, y] = tile } } for (x in 4 .. 8) { for (y in 4 .. 8) { - chunk.foreground[x, y] = null as TileDefinition? + chunkA!!.foreground[x, y] = null as TileDefinition? } } + } - chunkRenderer = ChunkRenderer(state, chunk, Starbound.world) - chunkRenderer!!.tesselateStatic() - chunkRenderer!!.uploadStatic() - }*/ + val rand = Random() - val runtime = Runtime.getRuntime() - - // Run the rendering loop until the user has attempted to close - // the window or has pressed the ESCAPE key. - while (!glfwWindowShouldClose(window)) { - val measure = glfwGetTime() - glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) // clear the framebuffer - - state.matrixStack.clear(viewportMatrixGame.toMutableMatrix()) - camera?.translate(state.matrixStack.last) - - state.matrixStack.push().scale(x = 20f, y = 20f).translateWithScale(0f, 0f) - chunkRenderer?.render() - - state.matrixStack.clear(viewportMatrixGUI.toMutableMatrix().translate(z = 2f)) - - state.font.render("FPS: %.2f".format(framesPerSecond), scale = 0.4f) - state.font.render("Mem: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", x = viewportWidth.toFloat(), scale = 0.4f, alignX = TextAlignX.RIGHT) - - val thisTime = System.currentTimeMillis() - - if (startupTextList.isNotEmpty() && thisTime <= finishStartupRendering) { - var alpha = 1f - - if (finishStartupRendering - thisTime < 1000L) { - alpha = (finishStartupRendering - thisTime) / 1000f - } - - state.matrixStack.push() - state.matrixStack.translateWithScale(y = viewportHeight.toFloat()) - var shade = 255 - - for (i in startupTextList.size - 1 downTo 0) { - val size = state.font.render(startupTextList[i], alignY = TextAlignY.BOTTOM, scale = 0.4f, color = Color.SHADES_OF_GRAY[shade].copy(alpha = alpha)) - state.matrixStack.translateWithScale(y = -size.height * 1.2f) - - if (shade > 120) { - shade -= 10 - } - } - - state.matrixStack.pop() - } - - glfwSwapBuffers(window) // swap the color buffers - - // Poll for window events. The key callback above will only be - // invoked during this call. - glfwPollEvents() + while (client.renderFrame()) { Starbound.pollCallbacks() - frameRenderTime = glfwGetTime() - measure + + if (chunkA != null && glfwGetTime() < 10.0) { + val tile = Starbound.getTileDefinition("alienrock") + //chunkA!!.foreground[rand.nextInt(0, CHUNK_SIZE_FF), rand.nextInt(0, CHUNK_SIZE_FF)] = tile + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 69cbcaba..9723c35b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -9,6 +9,12 @@ import ru.dbotthepony.kstarbound.world.World import java.io.File import java.io.FileNotFoundException +const val METRES_IN_STARBOUND_UNIT = 0.5 +const val METRES_IN_STARBOUND_UNITf = 0.5f + +const val PIXELS_IN_STARBOUND_UNIT = 8.0 +const val PIXELS_IN_STARBOUND_UNITf = 8.0f + class TileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause) object Starbound { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientSettings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientSettings.kt new file mode 100644 index 00000000..b34ca519 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientSettings.kt @@ -0,0 +1,12 @@ +package ru.dbotthepony.kstarbound.client + +data class ClientSettings( + /** + * Масштаб игрового мира + * + * Масштаб в единицу означает что один Starbound Unit будет равен 8 пикселям на экране + */ + var scale: Float = 2f +) { + +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt new file mode 100644 index 00000000..900973e9 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt @@ -0,0 +1,75 @@ +package ru.dbotthepony.kstarbound.client + +import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf +import ru.dbotthepony.kstarbound.api.IStruct2f +import ru.dbotthepony.kstarbound.client.render.ChunkRenderer +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( + world = this, + chunk = chunk, + top = top, + left = left, + right = right, + bottom = bottom, + + renderer = ChunkRenderer(client.gl, chunk) + ) + } + + /** + * Отрисовывает этот мир с точки зрения [pos] в Starbound Units + * + * Все координаты "местности" сохраняются, поэтому, если отрисовывать слишком далеко от 0, 0 + * то геометрия может начать искажаться из-за погрешности плавающей запятой + * + * Обрезает всю заведомо невидимую геометрию на основе аргументов mins и maxs (в пикселях) + */ + fun render( + pos: IStruct2f, + scale: Float = 1f, + + mins: IStruct2f, + maxs: IStruct2f, + ) { + val determineRenderers = ArrayList() + + for (chunk in chunkMap.values) { + determineRenderers.add(chunk.renderer) + } + + for (renderer in determineRenderers) { + val (x, y) = renderer.chunk.pos + + client.gl.matrixStack.push().translateWithScale(x = x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf, y = y * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf) + renderer.bakeAndRender() + client.gl.matrixStack.pop() + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index c63d84a5..526894da 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -12,13 +12,16 @@ import ru.dbotthepony.kstarbound.math.Matrix4f import ru.dbotthepony.kstarbound.client.render.Camera import ru.dbotthepony.kstarbound.client.render.TextAlignX import ru.dbotthepony.kstarbound.client.render.TextAlignY +import ru.dbotthepony.kstarbound.math.Vector2f import ru.dbotthepony.kstarbound.util.Color import ru.dbotthepony.kstarbound.util.formatBytesShort -import ru.dbotthepony.kstarbound.world.World +import kotlin.math.cos +import kotlin.math.sin class StarboundClient : AutoCloseable { val window: Long val camera = Camera() + var world: ClientWorld? = ClientWorld(this, 0L) var gameTerminated = false private set @@ -140,7 +143,20 @@ class StarboundClient : AutoCloseable { val framesPerSecond get() = 1.0 / frameRenderTime - var world: World? = World() + private val frameRenderTimes = DoubleArray(60) { 1.0 } + private var frameRenderIndex = 0 + + val averageFramesPerSecond: Double get() { + var sum = 0.0 + + for (value in frameRenderTimes) { + sum += value + } + + return frameRenderTimes.size / sum + } + + val settings = ClientSettings() fun renderFrame(): Boolean { ensureSameThread() @@ -153,9 +169,18 @@ class StarboundClient : AutoCloseable { val measure = GLFW.glfwGetTime() glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) - gl.matrixStack.clear(viewportMatrixGame.toMutableMatrix()) - camera.translate(gl.matrixStack.last) + + val mins = Vector2f((-viewportWidth / 2f) / settings.scale, (-viewportHeight / 2f) / settings.scale) + val maxs = -mins + + gl.matrixStack.push() + .translateWithScale(viewportWidth / 2f - camera.pos.x, viewportHeight / 2f - camera.pos.y) // центр экрана + координаты отрисовки мира + .scale(x = settings.scale, y = settings.scale) // масштабируем до нужного размера + + world?.render(Vector2f.ZERO, mins = mins, maxs = maxs) + + gl.matrixStack.pop() gl.matrixStack.clear(viewportMatrixGUI.toMutableMatrix().translate(z = 2f)) @@ -186,13 +211,18 @@ class StarboundClient : AutoCloseable { val runtime = Runtime.getRuntime() - gl.font.render("FPS: %.2f".format(framesPerSecond), scale = 0.4f) + gl.font.render("FPS: %.2f".format(averageFramesPerSecond), scale = 0.4f) gl.font.render("Mem: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", x = viewportWidth.toFloat(), scale = 0.4f, alignX = TextAlignX.RIGHT) GLFW.glfwSwapBuffers(window) GLFW.glfwPollEvents() + camera.tick(GLFW.glfwGetTime() - measure) + + gl.cleanup() + frameRenderTime = GLFW.glfwGetTime() - measure + frameRenderTimes[++frameRenderIndex % frameRenderTimes.size] = frameRenderTime return true } 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 0c8cbfea..e3d7b584 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.client.gl +import org.apache.logging.log4j.LogManager import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.Starbound @@ -12,6 +13,8 @@ import ru.dbotthepony.kstarbound.client.render.TileRenderer import ru.dbotthepony.kstarbound.client.render.TileRenderers import ru.dbotthepony.kstarbound.util.Color import java.io.File +import java.lang.ref.Cleaner +import java.util.concurrent.ThreadFactory import kotlin.reflect.KProperty private class GLStateSwitchTracker(private val enum: Int, private var value: Boolean = false) { @@ -73,6 +76,13 @@ open class GLTransformableColorableProgram(state: GLStateTracker, vararg shaders } } +interface GLCleanable : Cleaner.Cleanable { + /** + * Выставляет флаг на то, что объект был удалён вручную и вызывает clean() + */ + fun cleanManual(): Unit +} + class GLStateTracker { init { // This line is critical for LWJGL's interoperation with GLFW's @@ -83,6 +93,47 @@ class GLStateTracker { GL.createCapabilities() } + private var cleanerHits = ArrayList<() -> Unit>() + private val cleaner = Cleaner.create(object : ThreadFactory { + override fun newThread(r: Runnable): Thread { + val thread = Thread(r, "OpenGL Object Cleaner@" + System.identityHashCode(this)) + thread.priority = 2 + return thread + } + }) + + fun registerCleanable(ref: Any, fn: (Int) -> Unit, name: String, nativeRef: Int): GLCleanable { + var cleanManual = false + + val cleanable = cleaner.register(ref) { + cleanerHits.add { + fn(nativeRef) + checkForGLError() + + if (!cleanManual) + LOGGER.error("{} with ID {} got leaked.", name, nativeRef) + } + } + + return object : GLCleanable { + override fun cleanManual() { + cleanManual = true + clean() + } + + override fun clean() = cleanable.clean() + } + } + + fun cleanup() { + val copy = cleanerHits + cleanerHits = ArrayList() + + for (lambda in copy) { + lambda.invoke() + } + } + var blend by GLStateSwitchTracker(GL_BLEND) var depthTest by GLStateSwitchTracker(GL_DEPTH_TEST) @@ -282,4 +333,8 @@ class GLStateTracker { val freeType = FreeType() val font = Font(this) + + companion object { + private val LOGGER = LogManager.getLogger(GLStateTracker::class.java) + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt index a78fc691..f950e8c3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt @@ -31,6 +31,12 @@ class GLTexturePropertyTracker(private val flag: Int, var value: Int) { class GLTexture2D(val state: GLStateTracker, val name: String = "") : AutoCloseable { val pointer = glGenTextures() + init { + checkForGLError() + } + + private val cleanable = state.registerCleanable(this, ::glDeleteTextures, "2D Texture", pointer) + var width = 0 private set @@ -154,8 +160,7 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "") : A state.texture2D = null } - glDeleteTextures(pointer) - checkForGLError() + cleanable.cleanManual() isValid = false } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexArrayObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexArrayObject.kt index 1670f4ad..5634a6ec 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexArrayObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexArrayObject.kt @@ -6,6 +6,12 @@ import java.io.Closeable class GLVertexArrayObject(val state: GLStateTracker) : Closeable { val pointer = glGenVertexArrays() + init { + checkForGLError() + } + + private val cleanable = state.registerCleanable(this, ::glDeleteVertexArrays, "Vertex Array Object", pointer) + fun bind(): GLVertexArrayObject { check(isValid) { "Tried to use NULL GLVertexArrayObject" } return state.bind(this) @@ -38,13 +44,15 @@ class GLVertexArrayObject(val state: GLStateTracker) : Closeable { override fun close() { state.ensureSameThread() - if (isValid) return + + if (!isValid) return if (state.VAO == this) { state.VAO = null } - glDeleteVertexArrays(pointer) + cleanable.cleanManual() + isValid = false } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexBufferObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexBufferObject.kt index e8b9bf18..4043ddb5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexBufferObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexBufferObject.kt @@ -12,6 +12,12 @@ enum class VBOType(val value: Int) { class GLVertexBufferObject(val state: GLStateTracker, val type: VBOType = VBOType.ARRAY) : Closeable { val pointer = glGenBuffers() + init { + checkForGLError() + } + + private val cleanable = state.registerCleanable(this, ::glDeleteBuffers, "Vertex Buffer Object", pointer) + val isArray get() = type == VBOType.ARRAY val isElementArray get() = type == VBOType.ELEMENT_ARRAY @@ -72,13 +78,15 @@ class GLVertexBufferObject(val state: GLStateTracker, val type: VBOType = VBOTyp override fun close() { state.ensureSameThread() + if (!isValid) return if (state.VBO == this) { state.VBO = null } - glDeleteBuffers(pointer) + cleanable.cleanManual() + isValid = false } } 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 ded559ef..cec83cac 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt @@ -1,14 +1,11 @@ package ru.dbotthepony.kstarbound.client.render -import org.lwjgl.opengl.GL11 import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.client.gl.GLShaderProgram import ru.dbotthepony.kstarbound.client.gl.GLVertexArrayObject import ru.dbotthepony.kstarbound.client.gl.VertexBuilder import ru.dbotthepony.kstarbound.client.gl.checkForGLError -import ru.dbotthepony.kstarbound.gl.* import ru.dbotthepony.kstarbound.math.FloatMatrix -import ru.dbotthepony.kstarbound.math.Matrix4f /** * Служит для быстрой настройки состояния для будущей отрисовки @@ -47,6 +44,7 @@ class BakedStaticMesh( val vao: GLVertexArrayObject, ) : AutoCloseable { private var onClose = {} + constructor(programState: BakedProgramState, builder: VertexBuilder) : this( programState, builder.indexCount, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Camera.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Camera.kt index c87f7159..0883585f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Camera.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Camera.kt @@ -1,21 +1,55 @@ package ru.dbotthepony.kstarbound.client.render +import org.lwjgl.glfw.GLFW.* import ru.dbotthepony.kstarbound.math.* class Camera { + /** + * Позиция этой камеры в Starbound Unit'ах + */ val pos = MutableVector3f() - var zoom = 1f - fun translate(stack: FloatMatrix<*>) { - stack.translateWithScale(pos) - stack.scale(x = zoom, y = zoom) - } + var pressedLeft = false + private set + + var pressedRight = false + private set + + var pressedUp = false + private set + + var pressedDown = false + private set fun userInput(key: Int, scancode: Int, action: Int, mods: Int) { + when (key) { + GLFW_KEY_LEFT -> pressedLeft = action > 0 + GLFW_KEY_RIGHT -> pressedRight = action > 0 + GLFW_KEY_UP -> pressedUp = action > 0 + GLFW_KEY_DOWN -> pressedDown = action > 0 + } + } + fun tick(delta: Double) { + if (pressedLeft) { + pos.x -= (delta * FREEVIEW_SENS).toFloat() + } + + if (pressedRight) { + pos.x += (delta * FREEVIEW_SENS).toFloat() + } + + if (pressedUp) { + pos.y += (delta * FREEVIEW_SENS).toFloat() + } + + if (pressedDown) { + pos.y -= (delta * FREEVIEW_SENS).toFloat() + } } companion object { + const val FREEVIEW_SENS = 800.0 const val MAX_ZOOM = 4f const val MIN_ZOOM = 0.1f const val ZOOM_STEP = 0.1f diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt index 859eb56e..dc1afb9c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt @@ -1,19 +1,88 @@ package ru.dbotthepony.kstarbound.client.render +import ru.dbotthepony.kstarbound.client.ClientWorld import ru.dbotthepony.kstarbound.client.gl.GLStateTracker -import ru.dbotthepony.kstarbound.gl.* import ru.dbotthepony.kstarbound.math.FloatMatrix import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ITileChunk -import ru.dbotthepony.kstarbound.world.World import kotlin.collections.ArrayList -class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: World? = null) : AutoCloseable { - private val foregroundLayers = TileLayerList() - private val backgroundLayers = TileLayerList() - private val bakedMeshes = ArrayList() +class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: ClientWorld? = null) : AutoCloseable { + private inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable { + private val layers = TileLayerList() + private val bakedMeshes = ArrayList() + private var changeset = -1 + + fun tesselateStatic(view: ITileChunk) { + if (state.isSameThread()) { + for (mesh in bakedMeshes) { + mesh.close() + } + + bakedMeshes.clear() + } else { + unloadableBakedMeshes.addAll(bakedMeshes) + 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) in layers.buildList()) { + bakedMeshes.add(BakedStaticMesh(baked, builder)) + } + + if (clear) { + layers.clear() + } + } + + fun render(transform: FloatMatrix<*>) { + for (mesh in bakedMeshes) { + mesh.render(transform) + } + } + + fun bakeAndRender(transform: FloatMatrix<*>, provider: () -> ITileChunk) { + if (changeset != layerChangeset.invoke()) { + this.tesselateStatic(provider.invoke()) + this.uploadStatic() + + changeset = layerChangeset.invoke() + } + + render(transform) + } + + override fun close() { + for (mesh in bakedMeshes) { + mesh.close() + } + } + } + + private val bakedMeshesForeground = ArrayList() 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 } @@ -29,39 +98,8 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Worl * но только если до этого был вызыван loadRenderers() и геометрия чанка не поменялась */ fun tesselateStatic() { - if (state.isSameThread()) { - for (mesh in bakedMeshes) { - mesh.close() - } - - bakedMeshes.clear() - } else { - unloadableBakedMeshes.addAll(bakedMeshes) - bakedMeshes.clear() - } - - foregroundLayers.clear() - - val foreground = getForeground() - - // TODO: Синхронизация (ибо обновления игровой логики будут в потоке вне рендер потока) - for ((pos, tile) in foreground.posToTile) { - if (tile != null) { - val renderer = state.tileRenderers.get(tile.def.materialName) - renderer.tesselate(foreground, foregroundLayers, pos) - } - } - - backgroundLayers.clear() - - val background = getBackground() - - for ((pos, tile) in background.posToTile) { - if (tile != null) { - val renderer = state.tileRenderers.get(tile.def.materialName) - renderer.tesselate(background, backgroundLayers, pos, background = true) - } - } + foreground.tesselateStatic(getForeground()) + background.tesselateStatic(getBackground()) } /** @@ -72,18 +110,8 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Worl fun loadRenderers() { unloadUnused() - // TODO: Синхронизация (ибо обновления игровой логики будут в потоке вне рендер потока) - for ((_, tile) in getForeground().posToTile) { - if (tile != null) { - state.tileRenderers.get(tile.def.materialName) - } - } - - for ((_, tile) in getBackground().posToTile) { - if (tile != null) { - state.tileRenderers.get(tile.def.materialName) - } - } + foreground.loadRenderers(getForeground()) + background.loadRenderers(getBackground()) } private fun unloadUnused() { @@ -99,35 +127,26 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Worl fun uploadStatic(clear: Boolean = true) { unloadUnused() - for ((baked, builder) in backgroundLayers.buildList()) { - bakedMeshes.add(BakedStaticMesh(baked, builder)) - } - - for ((baked, builder) in foregroundLayers.buildList()) { - bakedMeshes.add(BakedStaticMesh(baked, builder)) - } - - if (clear) { - backgroundLayers.clear() - foregroundLayers.clear() - } + foreground.uploadStatic(clear) + background.uploadStatic(clear) } fun render(transform: FloatMatrix<*> = state.matrixStack.last) { unloadUnused() - for (mesh in bakedMeshes) { - mesh.render(transform) - } + background.render(transform) + foreground.render(transform) + } + + fun bakeAndRender(transform: FloatMatrix<*> = state.matrixStack.last) { + unloadUnused() + + background.bakeAndRender(transform, this::getBackground) + foreground.bakeAndRender(transform, this::getForeground) } override fun close() { - for (mesh in bakedMeshes) { - mesh.close() - } - - for (mesh in unloadableBakedMeshes) { - mesh.close() - } + background.close() + foreground.close() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt index 3f58e047..43fd64f3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt @@ -253,9 +253,9 @@ class Font( val advanceX: Float val advanceY: Float - private val vbo: GLVertexBufferObject? - private val ebo: GLVertexBufferObject? - private val vao: GLVertexArrayObject? + private val vbo: GLVertexBufferObject? // все три указателя должны хранится во избежание утечки + private val ebo: GLVertexBufferObject? // все три указателя должны хранится во избежание утечки + private val vao: GLVertexArrayObject? // все три указателя должны хранится во избежание утечки private val indexCount: Int 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 d720154f..2ebb14e3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt @@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.client.render import org.apache.logging.log4j.LogManager import org.lwjgl.opengl.GL46.* +import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.gl.* import ru.dbotthepony.kstarbound.defs.TileDefinition @@ -169,34 +170,36 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { if (offset != Vector2i.ZERO) { a += offset.x / BASELINE_TEXTURE_SIZE - // в json файлах y указан как положительный вверх + // в json файлах y указан как положительный вверх, + // что соответствует нашему миру b += offset.y / BASELINE_TEXTURE_SIZE c += offset.x / BASELINE_TEXTURE_SIZE d += offset.y / BASELINE_TEXTURE_SIZE } - /* - if (!notifiedDepth && tile.render.zLevel >= 5900) { - LOGGER.warn("Tile {} has out of bounds zLevel of {}", tile.materialName, tile.render.zLevel) - notifiedDepth = true - } - */ - if (tile.render.variants == 0 || piece.texture != null || piece.variantStride == null) { val (u0, v0) = texture.pixelToUV(piece.texturePosition) val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize) - //builder.quadZ(a, b, c, d, tile.render.zLevel.toFloat() + 200f, VertexTransformers.uv(u0, v1, u1, v0)) - builder.quadZ(a, b, c, d, Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) + builder.quadZ( + a * PIXELS_IN_STARBOUND_UNITf, + b * PIXELS_IN_STARBOUND_UNITf, + c * PIXELS_IN_STARBOUND_UNITf, + d * PIXELS_IN_STARBOUND_UNITf, + Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) } else { val variant = (getter.randomDoubleFor(pos) * tile.render.variants).toInt() val (u0, v0) = texture.pixelToUV(piece.texturePosition + piece.variantStride * variant) val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize + piece.variantStride * variant) - //builder.quadZ(a, b, c, d, tile.render.zLevel.toFloat() + 200f, VertexTransformers.uv(u0, v1, u1, v0)) - builder.quadZ(a, b, c, d, Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) + builder.quadZ( + a * PIXELS_IN_STARBOUND_UNITf, + b * PIXELS_IN_STARBOUND_UNITf, + c * PIXELS_IN_STARBOUND_UNITf, + d * PIXELS_IN_STARBOUND_UNITf, + Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) } } @@ -245,7 +248,7 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { * * [layers] содержит текущие программы и их билдеры и их zPos * - * Тесселирует тайлы в границы -1f .. CHUNK_SIZEf + 1f на основе [pos] + * Тесселирует тайлы в нужный VertexBuilder с масштабом согласно константе [PIXELS_IN_STARBOUND_UNITf] */ fun tesselate(getter: ITileChunk, layers: TileLayerList, pos: Vector2i, background: Boolean = false) { // если у нас нет renderTemplate diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt index 4923344e..6ab73611 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.math import com.google.gson.JsonArray +import ru.dbotthepony.kstarbound.api.IStruct2f import ru.dbotthepony.kstarbound.api.IStruct2i import ru.dbotthepony.kstarbound.api.IStruct3f import ru.dbotthepony.kstarbound.api.IStruct4f @@ -15,13 +16,16 @@ abstract class IVector2i> : IMatrixLike, IMatrixLikeInt, IStruc abstract val y: Int operator fun plus(other: IVector2i<*>) = make(x + other.x, y + other.y) - operator fun plus(other: Int) = make(x + other, y + other) operator fun minus(other: IVector2i<*>) = make(x - other.x, y - other.y) - operator fun minus(other: Int) = make(x - other, y - other) operator fun times(other: IVector2i<*>) = make(x * other.x, y * other.y) - operator fun times(other: Int) = make(x * other, y * other) operator fun div(other: IVector2i<*>) = make(x / other.x, y / other.y) + operator fun div(other: Int) = make(x / other, y / other) + operator fun times(other: Int) = make(x * other, y * other) + operator fun minus(other: Int) = make(x - other, y - other) + operator fun plus(other: Int) = make(x + other, y + other) + + operator fun unaryMinus() = make(-x, -y) fun left() = make(x - 1, y) fun right() = make(x + 1, y) @@ -59,6 +63,89 @@ data class Vector2i(override val x: Int = 0, override val y: Int = 0) : IVector2 } } +data class MutableVector2i(override var x: Int = 0, override var y: Int = 0) : IVector2i() { + override fun make(x: Int, y: Int): MutableVector2i { + this.x = x + this.y = y + return this + } + + companion object { + fun fromJson(input: JsonArray): MutableVector2i { + return MutableVector2i(input[0].asInt, input[1].asInt) + } + } +} + +abstract class IVector2f> : IMatrixLike, IMatrixLikeFloat, IStruct2f { + override val columns = 1 + override val rows = 2 + + abstract val x: Float + abstract val y: Float + + operator fun plus(other: IVector2f<*>) = make(x + other.x, y + other.y) + operator fun minus(other: IVector2f<*>) = make(x - other.x, y - other.y) + operator fun times(other: IVector2f<*>) = make(x * other.x, y * other.y) + operator fun div(other: IVector2f<*>) = make(x / other.x, y / other.y) + + operator fun plus(other: Float) = make(x + other, y + other) + operator fun minus(other: Float) = make(x - other, y - other) + operator fun times(other: Float) = make(x * other, y * other) + operator fun div(other: Float) = make(x / other, y / other) + + operator fun unaryMinus() = make(-x, -y) + + fun left() = make(x - 1, y) + fun right() = make(x + 1, y) + fun up() = make(x, y + 1) + fun down() = make(x, y - 1) + + override fun get(row: Int, column: Int): Float { + if (column != 0) { + throw IndexOutOfBoundsException("Column must be 0 ($column given)") + } + + return when (row) { + 0 -> x + 1 -> y + else -> throw IndexOutOfBoundsException("Row out of bounds: $row") + } + } + + protected abstract fun make(x: Float, y: Float): T +} + +data class Vector2f(override val x: Float = 0f, override val y: Float = 0f) : IVector2f() { + override fun make(x: Float, y: Float) = Vector2f(x, y) + + companion object { + fun fromJson(input: JsonArray): Vector2f { + return Vector2f(input[0].asFloat, input[1].asFloat) + } + + val ZERO = Vector2f() + val LEFT = Vector2f().left() + val RIGHT = Vector2f().right() + val UP = Vector2f().up() + val DOWN = Vector2f().down() + } +} + +data class MutableVector2f(override var x: Float = 0f, override var y: Float = 0f) : IVector2f() { + override fun make(x: Float, y: Float): MutableVector2f { + this.x = x + this.y = y + return this + } + + companion object { + fun fromJson(input: JsonArray): MutableVector2f { + return MutableVector2f(input[0].asFloat, input[1].asFloat) + } + } +} + abstract class IVector3f> : IMatrixLike, IMatrixLikeFloat, IStruct3f { override val columns = 1 override val rows = 3 @@ -77,6 +164,8 @@ abstract class IVector3f> : IMatrixLike, IMatrixLikeFloat, IStr operator fun times(other: Float) = make(x * other, y * other, z * other) operator fun div(other: Float) = make(x / other, y / other, z / other) + operator fun unaryMinus() = make(-x, -y, -z) + override fun get(row: Int, column: Int): Float { if (column != 0) { throw IndexOutOfBoundsException("Column must be 0 ($column given)") @@ -190,6 +279,8 @@ abstract class IVector4f> : IMatrixLike, IMatrixLikeFloat, IStr operator fun times(other: Float) = make(x * other, y * other, z * other, w * other) operator fun div(other: Float) = make(x / other, y / other, z / other, w / other) + operator fun unaryMinus() = make(-x, -y, -z, -w) + override val columns = 1 override val rows = 4 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index ef968f75..3aa2958d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.world +import ru.dbotthepony.kstarbound.api.IStruct2i import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.math.IVector2i import ru.dbotthepony.kstarbound.math.Vector2i @@ -7,9 +8,18 @@ import ru.dbotthepony.kstarbound.math.Vector2i /** * Представляет из себя класс, который содержит состояние тайла на заданной позиции */ -data class ChunkTile(val def: TileDefinition) { - var color = -1 +data class ChunkTile(val chunk: Chunk.TileLayer, val def: TileDefinition) { + var color = 0 + set(value) { + field = value + chunk.incChangeset() + } + var forceVariant = -1 + set(value) { + field = value + chunk.incChangeset() + } } interface ITileMap { @@ -134,16 +144,23 @@ interface ITileGetterSetter : ITileGetter, ITileSetter interface ITileChunk : ITileGetter, IChunkPositionable interface IMutableTileChunk : ITileChunk, ITileSetter -const val CHUNK_SHIFT = 6 -const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 64 +const val CHUNK_SHIFT = 5 +const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 32 const val CHUNK_SIZE_FF = CHUNK_SIZE - 1 data class ChunkPos(override val x: Int, override val y: Int) : IVector2i() { - constructor(pos: Vector2i) : this(pos.x shr CHUNK_SHIFT, pos.y shr CHUNK_SHIFT) + constructor(pos: IStruct2i) : this(pos.component1(), pos.component2()) override fun make(x: Int, y: Int) = ChunkPos(x, y) val firstBlock get() = Vector2i(x shl CHUNK_SHIFT, y shl CHUNK_SHIFT) val lastBlock get() = Vector2i(((x + 1) shl CHUNK_SHIFT) - 1, ((y + 1) shl CHUNK_SHIFT) - 1) + + companion object { + fun fromTilePosition(input: IStruct2i): ChunkPos { + val (x, y) = input + return ChunkPos(x shr CHUNK_SHIFT, y shr CHUNK_SHIFT) + } + } } /** @@ -256,8 +273,39 @@ class MutableTileChunkView( } } -open class Chunk(val world: World, val pos: ChunkPos) { - inner class ChunkTileLayer : IMutableTileChunk { +/** + * Чанк мира + * + * Хранит в себе тайлы и ентити внутри себя + * + * Считается, что один тайл имеет форму квадрата и сторона квадрата примерно равна полуметру, + * что будет называться Starbound Unit + * + * Весь игровой мир будет измеряться в Starbound Unit'ах + */ +open class Chunk(val world: World<*>?, val pos: ChunkPos) { + /** + * Возвращает счётчик изменений чанка + */ + var changeset = 0 + private set + + fun incChangeset() { + changeset++ + } + + inner class TileLayer : IMutableTileChunk { + /** + * Возвращает счётчик изменений этого слоя + */ + var changeset = 0 + private set + + fun incChangeset() { + changeset++ + this@Chunk.changeset++ + } + override val pos: ChunkPos get() = this@Chunk.pos @@ -277,6 +325,7 @@ open class Chunk(val world: World, val pos: ChunkPos) { if (isOutside(x, y)) throw IndexOutOfBoundsException("Trying to set tile ${tile?.def?.materialName} at $x $y, but that is outside of chunk's range") + changeset++ tiles[x or (y shl CHUNK_SHIFT)] = tile } @@ -284,18 +333,19 @@ open class Chunk(val world: World, val pos: ChunkPos) { if (isOutside(x, y)) throw IndexOutOfBoundsException("Trying to set tile ${tile?.materialName} at $x $y, but that is outside of chunk's range") - val chunkTile = if (tile != null) ChunkTile(tile) else null + val chunkTile = if (tile != null) ChunkTile(this, tile) else null this[x, y] = chunkTile + changeset++ return chunkTile } override fun randomLongFor(x: Int, y: Int): Long { - return super.randomLongFor(x, y) xor world.seed + return super.randomLongFor(x, y) xor (world?.seed ?: 0L) } } - val foreground = ChunkTileLayer() - val background = ChunkTileLayer() + val foreground = TileLayer() + val background = TileLayer() 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 6fac7904..eb08aad1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -3,139 +3,174 @@ package ru.dbotthepony.kstarbound.world import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.math.Vector2i -class World(val seed: Long = 0L) { - private val chunkMap = ArrayList>() - private var lastAccessedChunk: Chunk? = null +/** + * Возвращает кортеж чанка, который содержит родителя (мир) и соседей (кортежи чанков) + */ +interface IWorldChunkTuple { + val world: World<*> + val chunk: Chunk + val top: IWorldChunkTuple? + val left: IWorldChunkTuple? + val right: IWorldChunkTuple? + val bottom: IWorldChunkTuple? +} - fun getChunk(pos: ChunkPos): Chunk? { - if (lastAccessedChunk?.pos == pos) { +interface IMutableWorldChunkTuple : IWorldChunkTuple { + override var top: IWorldChunkTuple? + override var left: IWorldChunkTuple? + override var right: IWorldChunkTuple? + override var bottom: IWorldChunkTuple? +} + +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 + +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 + +@Suppress("WeakerAccess") +abstract class World(val seed: Long = 0L) { + protected val chunkMap = HashMap() + protected var lastAccessedChunk: T? = null + + protected abstract fun tupleFactory( + chunk: Chunk, + top: IWorldChunkTuple?, + left: IWorldChunkTuple?, + right: IWorldChunkTuple?, + bottom: IWorldChunkTuple?, + ): T + + protected fun getChunkInternal(pos: ChunkPos): T? { + if (lastAccessedChunk?.chunk?.pos == pos) { return lastAccessedChunk } - for ((k, v) in chunkMap) { - if (k == pos) { - lastAccessedChunk = v - return v - } - } + return chunkMap[pos] + } + + 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 null } - fun getOrMakeChunk(pos: ChunkPos): Chunk { - if (lastAccessedChunk?.pos == pos) { + protected open fun computeIfAbsentInternal(pos: ChunkPos): T { + if (lastAccessedChunk?.chunk?.pos == pos) { return lastAccessedChunk!! } - for ((k, v) in chunkMap) { - if (k == pos) { - return v - } + return chunkMap.computeIfAbsent(pos) lazy@{ + val chunk = Chunk(this, pos) + + val top = getChunkInternal(pos.up()) + val left = getChunkInternal(pos.left()) + val right = getChunkInternal(pos.right()) + val bottom = getChunkInternal(pos.down()) + + val tuple = tupleFactory( + chunk = chunk, + top = top, + left = left, + right = right, + bottom = bottom, + ) + + top?.bottom = tuple + left?.right = tuple + right?.left = tuple + bottom?.top = tuple + + lastAccessedChunk = tuple + + return@lazy tuple } - - val chunk = Chunk(this, pos) - lastAccessedChunk = chunk - chunkMap.add(pos to chunk) - return chunk } - fun getForegroundView(pos: ChunkPos): TileChunkView? { - val get = getChunk(pos) ?: return null + open fun computeIfAbsent(pos: ChunkPos): IWorldChunkTuple { + val getTuple = computeIfAbsentInternal(pos) - return TileChunkView( - center = get.foreground, - left = getChunk(pos.left())?.foreground, - top = getChunk(pos.up())?.foreground, - topLeft = getChunk(pos.up().left())?.foreground, - topRight = getChunk(pos.up().right())?.foreground, - right = getChunk(pos.right())?.foreground, - bottom = getChunk(pos.down())?.foreground, - bottomLeft = getChunk(pos.down().left())?.foreground, - bottomRight = getChunk(pos.down().right())?.foreground, + return WorldChunkTuple( + world = getTuple.world, + chunk = getTuple.chunk, + top = getTuple.top, + left = getTuple.left, + right = getTuple.right, + bottom = getTuple.bottom, ) } - fun getBackgroundView(pos: ChunkPos): TileChunkView? { - val get = getChunk(pos) ?: return null + open fun getForegroundView(pos: ChunkPos): TileChunkView? { + val get = getChunkInternal(pos) ?: return null return TileChunkView( - center = get.background, - left = getChunk(pos.left())?.background, - top = getChunk(pos.up())?.background, - topLeft = getChunk(pos.up().left())?.background, - topRight = getChunk(pos.up().right())?.background, - right = getChunk(pos.right())?.background, - bottom = getChunk(pos.down())?.background, - bottomLeft = getChunk(pos.down().left())?.background, - bottomRight = getChunk(pos.down().right())?.background, + center = get.chunk.foreground, + left = get.left?.chunk?.foreground, + top = get.top?.chunk?.foreground, + topLeft = getChunkInternal(pos.up().left())?.chunk?.foreground, + topRight = getChunkInternal(pos.up().right())?.chunk?.foreground, + right = get.right?.chunk?.foreground, + bottom = get.bottom?.chunk?.foreground, + bottomLeft = getChunkInternal(pos.down().left())?.chunk?.foreground, + bottomRight = getChunkInternal(pos.down().right())?.chunk?.foreground, ) } - /** - * Считается, что [pos] это абсолютные координаты ТАЙЛА в мире, поэтому они - * трансформируются в координаты чанка - */ - fun getChunk(pos: Vector2i) = getChunk(ChunkPos(pos)) + open fun getBackgroundView(pos: ChunkPos): TileChunkView? { + val get = getChunkInternal(pos) ?: return null - /** - * Считается, что [pos] это абсолютные координаты ТАЙЛА в мире, поэтому они - * трансформируются в координаты чанка - */ - fun getOrMakeChunk(pos: Vector2i) = getOrMakeChunk(ChunkPos(pos)) - - /** - * Считается, что [pos] это абсолютные координаты ТАЙЛА в мире, поэтому они - * трансформируются в координаты чанка - */ - fun getForegroundView(pos: Vector2i) = getForegroundView(ChunkPos(pos)) - - /** - * Считается, что [pos] это абсолютные координаты ТАЙЛА в мире, поэтому они - * трансформируются в координаты чанка - */ - fun getBackgroundView(pos: Vector2i) = getBackgroundView(ChunkPos(pos)) - - /** - * Считается, что [x] и [y] это абсолютные координаты ЧАНКА в мире, поэтому они - * НЕ трансформируются в координаты чанка, а используются напрямую - */ - fun getChunk(x: Int, y: Int) = getChunk(ChunkPos(x, y)) - - /** - * Считается, что [x] и [y] это абсолютные координаты ЧАНКА в мире, поэтому они - * НЕ трансформируются в координаты чанка, а используются напрямую - */ - fun getOrMakeChunk(x: Int, y: Int) = getOrMakeChunk(ChunkPos(x, y)) - - /** - * Считается, что [x] и [y] это абсолютные координаты ЧАНКА в мире, поэтому они - * НЕ трансформируются в координаты чанка, а используются напрямую - */ - fun getForegroundView(x: Int, y: Int) = getForegroundView(ChunkPos(x, y)) - - /** - * Считается, что [x] и [y] это абсолютные координаты ЧАНКА в мире, поэтому они - * НЕ трансформируются в координаты чанка, а используются напрямую - */ - fun getBackgroundView(x: Int, y: Int) = getBackgroundView(ChunkPos(x, y)) + return TileChunkView( + center = get.chunk.background, + left = get.left?.chunk?.background, + top = get.top?.chunk?.background, + topLeft = getChunkInternal(pos.up().left())?.chunk?.background, + topRight = getChunkInternal(pos.up().right())?.chunk?.background, + right = get.right?.chunk?.background, + bottom = get.bottom?.chunk?.background, + bottomLeft = getChunkInternal(pos.down().left())?.chunk?.background, + bottomRight = getChunkInternal(pos.down().right())?.chunk?.background, + ) + } fun getTile(pos: Vector2i): ChunkTile? { - return getChunk(pos)?.foreground?.get(pos.x, pos.y) + return getChunkInternal(ChunkPos.fromTilePosition(pos))?.chunk?.foreground?.get(pos.x, pos.y) } - fun setTile(pos: Vector2i, tile: TileDefinition?): Chunk { - val chunk = getOrMakeChunk(pos) - chunk.foreground[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile + fun setTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple { + val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos)) + chunk.chunk.foreground[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile return chunk } fun getBackgroundTile(pos: Vector2i): ChunkTile? { - return getChunk(pos)?.background?.get(pos.x, pos.y) + return getChunkInternal(ChunkPos.fromTilePosition(pos))?.chunk?.background?.get(pos.x, pos.y) } - fun setBackgroundTile(pos: Vector2i, tile: TileDefinition?): Chunk { - val chunk = getOrMakeChunk(pos) - chunk.background[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile + fun setBackgroundTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple { + val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos)) + chunk.chunk.background[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile return chunk } }