Circular world test

This commit is contained in:
DBotThePony 2022-08-05 11:32:50 +07:00
parent 51a43d70be
commit 2653d043a9
Signed by: DBot
GPG Key ID: DCC23B5715498507
8 changed files with 169 additions and 75 deletions

View File

@ -190,7 +190,7 @@ fun main() {
client.gl.box2dRenderer.drawAABB = false client.gl.box2dRenderer.drawAABB = false
client.gl.box2dRenderer.drawJoints = false client.gl.box2dRenderer.drawJoints = false
ent.spawn() //ent.spawn()
client.input.addScrollCallback { _, x, y -> client.input.addScrollCallback { _, x, y ->
if (y > 0.0) { if (y > 0.0) {
@ -207,11 +207,11 @@ fun main() {
//client.camera.pos.x = ent.position.x.toFloat() //client.camera.pos.x = ent.position.x.toFloat()
//client.camera.pos.y = ent.position.y.toFloat() //client.camera.pos.y = ent.position.y.toFloat()
client.camera.pos.x += if (client.input.KEY_LEFT_DOWN) -client.frameRenderTime.toFloat() * 32f / client.settings.scale else 0f client.camera.pos.x += if (client.input.KEY_LEFT_DOWN || client.input.KEY_A_DOWN) -client.frameRenderTime.toFloat() * 32f / client.settings.scale else 0f
client.camera.pos.x += if (client.input.KEY_RIGHT_DOWN) client.frameRenderTime.toFloat() * 32f / client.settings.scale else 0f client.camera.pos.x += if (client.input.KEY_RIGHT_DOWN || client.input.KEY_D_DOWN) client.frameRenderTime.toFloat() * 32f / client.settings.scale else 0f
client.camera.pos.y += if (client.input.KEY_UP_DOWN) client.frameRenderTime.toFloat() * 32f / client.settings.scale else 0f client.camera.pos.y += if (client.input.KEY_UP_DOWN || client.input.KEY_W_DOWN) client.frameRenderTime.toFloat() * 32f / client.settings.scale else 0f
client.camera.pos.y += if (client.input.KEY_DOWN_DOWN) -client.frameRenderTime.toFloat() * 32f / client.settings.scale else 0f client.camera.pos.y += if (client.input.KEY_DOWN_DOWN || client.input.KEY_S_DOWN) -client.frameRenderTime.toFloat() * 32f / client.settings.scale else 0f
//println(client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1) //println(client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1)

View File

@ -19,7 +19,7 @@ import java.io.Closeable
*/ */
const val Z_LEVEL_BACKGROUND = 60000 const val Z_LEVEL_BACKGROUND = 60000
class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos), Closeable, ILayeredRenderer { class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos), Closeable {
val state: GLStateTracker get() = world.client.gl val state: GLStateTracker get() = world.client.gl
private inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable { private inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable {
@ -221,63 +221,69 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
} }
} }
private val layerQueue = ArrayDeque<Pair<(Matrix4fStack) -> Unit, Int>>() /**
* Хранит состояние отрисовки этого чанка
*
* Должен быть использован только один раз, после выкинут, иначе поведение
* кода невозможно будет предсказать
*/
inner class BakedLayeredRenderer constructor(val origin: ChunkPos = pos) : ILayeredRenderer {
private val layerQueue = ArrayDeque<Pair<(Matrix4fStack) -> Unit, Int>>()
override fun renderLayerFromStack(zPos: Int, stack: Matrix4fStack): Int { init {
if (layerQueue.isEmpty()) for ((baked, zLevel) in backgroundRenderer.bakedMeshes) {
return -1 layerQueue.add(baked::renderStacked to (zLevel + Z_LEVEL_BACKGROUND))
}
stack.push().translateWithMultiplication(x = pos.x * CHUNK_SIZEf, y = pos.y * CHUNK_SIZEf) for ((baked, zLevel) in foregroundRenderer.bakedMeshes) {
var pair = layerQueue.last() layerQueue.add(baked::renderStacked to zLevel)
}
while (pair.second >= zPos) { for (renderer in entityRenderers.values) {
pair.first.invoke(stack) layerQueue.add(lambda@{ it: Matrix4fStack ->
val relative = renderer.renderPos - posVector2d
it.push().translateWithMultiplication(relative.x.toFloat(), relative.y.toFloat())
renderer.render(it)
it.pop()
return@lambda
} to renderer.layer)
}
layerQueue.removeLast() layerQueue.sortBy {
return@sortBy it.second
}
}
override fun renderLayerFromStack(zPos: Int, stack: Matrix4fStack): Int {
if (layerQueue.isEmpty())
return -1
stack.push().translateWithMultiplication(x = origin.x * CHUNK_SIZEf, y = origin.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()) { if (layerQueue.isEmpty()) {
stack.pop()
return -1 return -1
} }
pair = layerQueue.last() return layerQueue.last().second
}
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().translateWithMultiplication(relative.x.toFloat(), relative.y.toFloat())
renderer.render(it)
it.pop()
return@lambda
} to renderer.layer)
}
layerQueue.sortBy {
return@sortBy it.second
} }
} }

View File

@ -3,15 +3,21 @@ package ru.dbotthepony.kstarbound.client
import org.lwjgl.opengl.GL46.* import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.gl.VertexTransformers import ru.dbotthepony.kstarbound.client.gl.VertexTransformers
import ru.dbotthepony.kstarbound.client.render.ILayeredRenderer
import ru.dbotthepony.kstarbound.client.render.renderLayeredList import ru.dbotthepony.kstarbound.client.render.renderLayeredList
import ru.dbotthepony.kstarbound.defs.ParallaxPrototype import ru.dbotthepony.kstarbound.defs.ParallaxPrototype
import ru.dbotthepony.kstarbound.math.encasingChunkPosAABB import ru.dbotthepony.kstarbound.math.encasingChunkPosAABB
import ru.dbotthepony.kstarbound.util.DoubleEdgeProgression import ru.dbotthepony.kstarbound.util.DoubleEdgeProgression
import ru.dbotthepony.kstarbound.world.* import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.matrix.Matrix4fStack
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWorld, ClientChunk>(seed) { class ClientWorld(
val client: StarboundClient,
seed: Long,
widthInChunks: Int,
) : World<ClientWorld, ClientChunk>(seed, widthInChunks) {
init { init {
physics.debugDraw = client.gl.box2dRenderer physics.debugDraw = client.gl.box2dRenderer
} }
@ -94,20 +100,20 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWo
client.gl.matrixStack.pop() client.gl.matrixStack.pop()
} }
val determineRenderers = ArrayList<ClientChunk>() val determineRenderers = ArrayList<ILayeredRenderer>()
for (chunk in collect(size.encasingChunkPosAABB())) { for (chunk in collectPositionAware(size.encasingChunkPosAABB())) {
determineRenderers.add(chunk) determineRenderers.add(chunk.second.BakedLayeredRenderer(chunk.first))
chunk.bake() chunk.second.bake()
} }
renderLayeredList(client.gl.matrixStack, determineRenderers) renderLayeredList(client.gl.matrixStack, determineRenderers)
physics.debugDraw() physics.debugDraw()
for (renderer in determineRenderers) { /*for (renderer in determineRenderers) {
renderer.renderDebug() renderer.renderDebug()
} }*/
} }
override fun thinkInner(delta: Double) { override fun thinkInner(delta: Double) {

View File

@ -125,7 +125,7 @@ class StarboundClient : AutoCloseable {
val gl = GLStateTracker() val gl = GLStateTracker()
var world: ClientWorld? = ClientWorld(this, 0L) var world: ClientWorld? = ClientWorld(this, 0L, 0)
fun ensureSameThread() = gl.ensureSameThread() fun ensureSameThread() = gl.ensureSameThread()

View File

@ -34,13 +34,6 @@ interface ILayeredRenderer {
* если этот объект имеет самый дальний слой * если этот объект имеет самый дальний слой
*/ */
fun bottomMostZLevel(): Int fun bottomMostZLevel(): Int
/**
* Говорит о том, что вот-вот будет начата отрисовка в render pipeline
* и будут вызываться [renderLayerFromStack]. В данном методе должна построиться
* и отсортироваться стопка слоёв
*/
fun prepareForLayeredRender()
} }
fun renderLayeredList(transform: Matrix4fStack, potentialRenderers: List<ILayeredRenderer>): Int { fun renderLayeredList(transform: Matrix4fStack, potentialRenderers: List<ILayeredRenderer>): Int {
@ -48,7 +41,6 @@ fun renderLayeredList(transform: Matrix4fStack, potentialRenderers: List<ILayere
var bottomMost = -1 var bottomMost = -1
for (render in potentialRenderers) { for (render in potentialRenderers) {
render.prepareForLayeredRender()
val zLevel = render.bottomMostZLevel() val zLevel = render.bottomMostZLevel()
if (zLevel >= 0) { if (zLevel >= 0) {

View File

@ -6,6 +6,18 @@ import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kvector.vector.nint.Vector2i import ru.dbotthepony.kvector.vector.nint.Vector2i
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
private fun circulate(value: Int, bounds: Int): Int {
require(bounds > 0) { "Bounds must be positive ($bounds given)" }
if (value >= bounds) {
return value % bounds
} else if (value < 0) {
return bounds + value % bounds
}
return value
}
/** /**
* Сетка чанков идёт как и сетка тайлов. * Сетка чанков идёт как и сетка тайлов.
* *
@ -85,6 +97,16 @@ class ChunkPos(val x: Int, val y: Int) : Comparable<ChunkPos> {
return y.compareTo(other.y) return y.compareTo(other.y)
} }
fun circular(xWrap: Int): ChunkPos {
val x = circulate(x, xWrap)
if (x == this.x) {
return this
}
return ChunkPos(x, y)
}
companion object { companion object {
val ZERO = ChunkPos(0, 0) val ZERO = ChunkPos(0, 0)
@ -93,15 +115,29 @@ class ChunkPos(val x: Int, val y: Int) : Comparable<ChunkPos> {
return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y)) return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y))
} }
fun fromTilePosition(input: IStruct2i, xWrap: Int): ChunkPos {
val (x, y) = input
return ChunkPos(circulate(tileToChunkComponent(x), xWrap), tileToChunkComponent(y))
}
fun fromTilePosition(input: IStruct2d): ChunkPos { fun fromTilePosition(input: IStruct2d): ChunkPos {
val (x, y) = input val (x, y) = input
return fromTilePosition(x, y) return fromTilePosition(x, y)
} }
fun fromTilePosition(input: IStruct2d, xWrap: Int): ChunkPos {
val (x, y) = input
return fromTilePosition(x, y, xWrap)
}
fun fromTilePosition(x: Int, y: Int): ChunkPos { fun fromTilePosition(x: Int, y: Int): ChunkPos {
return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y)) 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 fromTilePosition(x: Double, y: Double): ChunkPos {
return ChunkPos( return ChunkPos(
tileToChunkComponent(roundByAbsoluteValue(x)), tileToChunkComponent(roundByAbsoluteValue(x)),
@ -109,6 +145,13 @@ class ChunkPos(val x: Int, val y: Int) : Comparable<ChunkPos> {
) )
} }
fun fromTilePosition(x: Double, y: Double, xWrap: Int): ChunkPos {
return ChunkPos(
circulate(tileToChunkComponent(roundByAbsoluteValue(x)), xWrap),
tileToChunkComponent(roundByAbsoluteValue(y))
)
}
fun normalizeCoordinate(input: Int): Int { fun normalizeCoordinate(input: Int): Int {
val band = input and CHUNK_SIZE_FF val band = input and CHUNK_SIZE_FF

View File

@ -33,11 +33,23 @@ data class WorldSweepResult(
private const val EPSILON = 0.00001 private const val EPSILON = 0.00001
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val seed: Long = 0L) { abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(
val seed: Long,
val widthInChunks: Int
) {
protected val chunkMap = Object2ObjectAVLTreeMap<ChunkPos, ChunkType> cmp@{ a, b -> protected val chunkMap = Object2ObjectAVLTreeMap<ChunkPos, ChunkType> cmp@{ a, b ->
return@cmp a.compareTo(b) return@cmp a.compareTo(b)
} }
/**
* Является ли мир "сферическим"
*
* Данный флаг говорит о том, что [widthInChunks] имеет осмысленное значение,
* попытка получить чанк с X координатой меньше нуля или больше [widthInChunks]
* приведёт к замыканию на конец/начало мира соответственно
*/
val isCircular = widthInChunks > 0
/** /**
* Chunks, which have their collision mesh changed * Chunks, which have their collision mesh changed
*/ */
@ -213,13 +225,28 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
* Возвращает чанк на указанной позиции если он существует * Возвращает чанк на указанной позиции если он существует
*/ */
open operator fun get(pos: ChunkPos): ChunkType? { open operator fun get(pos: ChunkPos): ChunkType? {
if (!isCircular) {
val lastAccessedChunk = lastAccessedChunk
if (lastAccessedChunk?.pos == pos) {
return lastAccessedChunk
}
return chunkMap[pos]
}
@Suppress("Name_Shadowing")
val pos = pos.circular(widthInChunks)
val lastAccessedChunk = lastAccessedChunk val lastAccessedChunk = lastAccessedChunk
if (lastAccessedChunk?.pos == pos) { if (lastAccessedChunk?.pos == pos) {
return lastAccessedChunk return lastAccessedChunk
} }
return chunkMap[pos] val load = chunkMap[pos]
this.lastAccessedChunk = load
return load
} }
open fun getInstantTuple(pos: ChunkPos): IWorldChunkTuple<This, ChunkType>? { open fun getInstantTuple(pos: ChunkPos): IWorldChunkTuple<This, ChunkType>? {
@ -230,6 +257,9 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
* Возвращает чанк на заданной позиции, создаёт его если он не существует * Возвращает чанк на заданной позиции, создаёт его если он не существует
*/ */
open fun computeIfAbsent(pos: ChunkPos): ChunkType { open fun computeIfAbsent(pos: ChunkPos): ChunkType {
@Suppress("Name_Shadowing")
val pos = pos.circular(widthInChunks)
val _lastAccessedChunk = lastAccessedChunk val _lastAccessedChunk = lastAccessedChunk
if (_lastAccessedChunk?.pos == pos) { if (_lastAccessedChunk?.pos == pos) {
@ -244,7 +274,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val orphanedInThisChunk = ArrayList<Entity>() val orphanedInThisChunk = ArrayList<Entity>()
for (ent in orphanedEntities) { for (ent in orphanedEntities) {
val cPos = ChunkPos.fromTilePosition(ent.position) val cPos = if (isCircular) ChunkPos.fromTilePosition(ent.position, widthInChunks) else ChunkPos.fromTilePosition(ent.position)
if (cPos == pos) { if (cPos == pos) {
orphanedInThisChunk.add(ent) orphanedInThisChunk.add(ent)
@ -330,6 +360,23 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
return output return output
} }
/**
* Возвращает все чанки, которые пересекаются с заданным [boundingBox]
*/
open fun collectPositionAware(boundingBox: AABBi): List<Pair<ChunkPos, ChunkType>> {
val output = ArrayList<Pair<ChunkPos, ChunkType>>()
for (pos in boundingBox.chunkPositions) {
val chunk = get(pos)
if (chunk != null) {
output.add(pos to chunk)
}
}
return output
}
/** /**
* Производит проверку на пересечение [worldaabb] с геометрией мира при попытке пройти в [_deltaMovement] * Производит проверку на пересечение [worldaabb] с геометрией мира при попытке пройти в [_deltaMovement]
*/ */

View File

@ -95,10 +95,10 @@ abstract class Entity(override val world: World<*, *>) : IEntity {
return return
} }
val chunkPos = ChunkPos.fromTilePosition(position) val chunkPos = if (world.isCircular) ChunkPos.fromTilePosition(position, world.widthInChunks) else ChunkPos.fromTilePosition(position)
if (value != null && chunkPos != value.pos) { if (value != null && chunkPos != value.pos) {
throw IllegalStateException("Set proper position before setting chunk this Entity belongs to") throw IllegalStateException("Set proper position before setting chunk this Entity belongs to (expected chunk $chunkPos, got chunk ${value.pos})")
} }
val oldChunk = field val oldChunk = field