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.drawJoints = false
ent.spawn()
//ent.spawn()
client.input.addScrollCallback { _, x, y ->
if (y > 0.0) {
@ -207,11 +207,11 @@ fun main() {
//client.camera.pos.x = ent.position.x.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_RIGHT_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.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_DOWN_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.input.KEY_S_DOWN) -client.frameRenderTime.toFloat() * 32f / client.settings.scale else 0f
//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
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
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 {
if (layerQueue.isEmpty())
return -1
init {
for ((baked, zLevel) in backgroundRenderer.bakedMeshes) {
layerQueue.add(baked::renderStacked to (zLevel + Z_LEVEL_BACKGROUND))
}
stack.push().translateWithMultiplication(x = pos.x * CHUNK_SIZEf, y = pos.y * CHUNK_SIZEf)
var pair = layerQueue.last()
for ((baked, zLevel) in foregroundRenderer.bakedMeshes) {
layerQueue.add(baked::renderStacked to zLevel)
}
while (pair.second >= zPos) {
pair.first.invoke(stack)
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.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()) {
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().translateWithMultiplication(relative.x.toFloat(), relative.y.toFloat())
renderer.render(it)
it.pop()
return@lambda
} to renderer.layer)
}
layerQueue.sortBy {
return@sortBy it.second
return layerQueue.last().second
}
}

View File

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

View File

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

View File

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

View File

@ -6,6 +6,18 @@ import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kvector.vector.nint.Vector2i
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)
}
fun circular(xWrap: Int): ChunkPos {
val x = circulate(x, xWrap)
if (x == this.x) {
return this
}
return ChunkPos(x, y)
}
companion object {
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))
}
fun fromTilePosition(input: IStruct2i, xWrap: Int): ChunkPos {
val (x, y) = input
return ChunkPos(circulate(tileToChunkComponent(x), xWrap), tileToChunkComponent(y))
}
fun fromTilePosition(input: IStruct2d): ChunkPos {
val (x, y) = input
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 {
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 {
return ChunkPos(
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 {
val band = input and CHUNK_SIZE_FF

View File

@ -33,11 +33,23 @@ data class WorldSweepResult(
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 ->
return@cmp a.compareTo(b)
}
/**
* Является ли мир "сферическим"
*
* Данный флаг говорит о том, что [widthInChunks] имеет осмысленное значение,
* попытка получить чанк с X координатой меньше нуля или больше [widthInChunks]
* приведёт к замыканию на конец/начало мира соответственно
*/
val isCircular = widthInChunks > 0
/**
* 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? {
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
if (lastAccessedChunk?.pos == pos) {
return lastAccessedChunk
}
return chunkMap[pos]
val load = chunkMap[pos]
this.lastAccessedChunk = load
return load
}
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 {
@Suppress("Name_Shadowing")
val pos = pos.circular(widthInChunks)
val _lastAccessedChunk = lastAccessedChunk
if (_lastAccessedChunk?.pos == pos) {
@ -244,7 +274,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val orphanedInThisChunk = ArrayList<Entity>()
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) {
orphanedInThisChunk.add(ent)
@ -330,6 +360,23 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
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]
*/

View File

@ -95,10 +95,10 @@ abstract class Entity(override val world: World<*, *>) : IEntity {
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) {
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