Undo dynamic chunk dimensions, add render regions instead for solid grid rendering

This commit is contained in:
DBotThePony 2023-09-05 21:12:23 +07:00
parent 94fe3662ad
commit 538f8a9b72
Signed by: DBot
GPG Key ID: DCC23B5715498507
11 changed files with 347 additions and 183 deletions

View File

@ -12,7 +12,6 @@ import ru.dbotthepony.kstarbound.player.Avatar
import ru.dbotthepony.kstarbound.player.QuestDescriptor import ru.dbotthepony.kstarbound.player.QuestDescriptor
import ru.dbotthepony.kstarbound.player.QuestInstance import ru.dbotthepony.kstarbound.player.QuestInstance
import ru.dbotthepony.kstarbound.util.JVMTimeSource import ru.dbotthepony.kstarbound.util.JVMTimeSource
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kstarbound.world.api.IChunkCell
import ru.dbotthepony.kstarbound.world.entities.ItemEntity import ru.dbotthepony.kstarbound.world.entities.ItemEntity
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
@ -90,7 +89,7 @@ fun main() {
for (y in 0 .. 31) { for (y in 0 .. 31) {
for (x in 0 .. 31) { for (x in 0 .. 31) {
val cell = client.world!!.chunkMap.getCellDirect(chunkX * 32 + x, chunkY * 32 + y) val cell = client.world!!.getCellDirect(chunkX * 32 + x, chunkY * 32 + y)
if (cell == null) { if (cell == null) {
IChunkCell.skip(reader) IChunkCell.skip(reader)
@ -160,7 +159,7 @@ fun main() {
client.gl.font.render("${ent.position}", y = 100f, scale = 0.25f) client.gl.font.render("${ent.position}", y = 100f, scale = 0.25f)
client.gl.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f) client.gl.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f)
client.gl.font.render("Camera: ${client.camera.pos} ${client.settings.zoom}", y = 140f, scale = 0.25f) client.gl.font.render("Camera: ${client.camera.pos} ${client.settings.zoom}", y = 140f, scale = 0.25f)
client.gl.font.render("World chunk: ${client.world!!.chunkMap.cellToChunk(client.camera.pos.toDoubleVector())}", y = 160f, scale = 0.25f) client.gl.font.render("World chunk: ${client.world!!.chunkFromCell(client.camera.pos.toDoubleVector())}", y = 160f, scale = 0.25f)
} }
client.onPreDrawWorld { client.onPreDrawWorld {

View File

@ -26,7 +26,7 @@ import java.util.LinkedList
const val Z_LEVEL_BACKGROUND = 60000 const val Z_LEVEL_BACKGROUND = 60000
const val Z_LEVEL_LIQUID = 10000 const val Z_LEVEL_LIQUID = 10000
class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) : Chunk<ClientWorld, ClientChunk>(world, pos, width, height), Closeable { 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 view: ITileAccess, private val isBackground: Boolean) : AutoCloseable { private inner class TileLayerRenderer(private val view: ITileAccess, private val isBackground: Boolean) : AutoCloseable {
@ -48,8 +48,9 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) :
layers.clear() layers.clear()
for (x in 0 until width) { for (x in 0 until CHUNK_SIZE) {
for (y in 0 until height) { for (y in 0 until CHUNK_SIZE) {
if (!world.inBounds(x, y)) continue
val tile = view.getTile(x, y) ?: continue val tile = view.getTile(x, y) ?: continue
val material = tile.material val material = tile.material
@ -97,31 +98,31 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) :
override fun foregroundChanges() { override fun foregroundChanges() {
super.foregroundChanges() super.foregroundChanges()
foregroundRenderer.isDirty = true world.forEachRenderRegion(pos) {
it.foreground.isDirty = true
}
forEachNeighbour { forEachNeighbour {
it.foregroundRenderer.isDirty = true world.forEachRenderRegion(it.pos) {
it.foreground.isDirty = true
}
} }
} }
override fun backgroundChanges() { override fun backgroundChanges() {
super.backgroundChanges() super.backgroundChanges()
backgroundRenderer.isDirty = true world.forEachRenderRegion(pos) {
it.background.isDirty = true
}
forEachNeighbour { forEachNeighbour {
it.backgroundRenderer.isDirty = true world.forEachRenderRegion(it.pos) {
it.background.isDirty = true
}
} }
} }
/**
* Тесселирует "статичную" геометрию в builders (к примеру тайлы), с проверкой, изменилось ли что либо,
* и загружает её в видеопамять.
*
* Может быть вызван вне рендер потока (ибо в любом случае он требует некой "стаитичности" данных в чанке)
* но только если до этого был вызыван loadRenderers() и геометрия чанка не поменялась
*
*/
fun bake() { fun bake() {
backgroundRenderer.bake() backgroundRenderer.bake()
foregroundRenderer.bake() foregroundRenderer.bake()
@ -130,9 +131,6 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) :
upload() upload()
} }
/**
* Загружает в видеопамять всю геометрию напрямую, если есть что загружать
*/
fun upload() { fun upload() {
backgroundRenderer.upload() backgroundRenderer.upload()
foregroundRenderer.upload() foregroundRenderer.upload()
@ -146,8 +144,8 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) :
liquidTypes.clear() liquidTypes.clear()
liquidTypesVer = liquidChangeset liquidTypesVer = liquidChangeset
for (x in 0 until width) { for (x in 0 until CHUNK_SIZE) {
for (y in 0 until height) { for (y in 0 until CHUNK_SIZE) {
getCell(x, y).liquid.def?.let { liquidTypes.add(it) } getCell(x, y).liquid.def?.let { liquidTypes.add(it) }
} }
} }
@ -199,8 +197,8 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) :
for (type in types) { for (type in types) {
builder.builder.begin() builder.builder.begin()
for (x in 0 until width) { for (x in 0 until CHUNK_SIZE) {
for (y in 0 until height) { for (y in 0 until CHUNK_SIZE) {
val state = getCell(x, y) val state = getCell(x, y)
if (state.liquid.def === type) { if (state.liquid.def === type) {

View File

@ -1,14 +1,26 @@
package ru.dbotthepony.kstarbound.client package ru.dbotthepony.kstarbound.client
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import ru.dbotthepony.kstarbound.client.render.ConfiguredStaticMesh
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.math.encasingIntAABB import ru.dbotthepony.kstarbound.client.render.TileLayerList
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.world.* import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2f import ru.dbotthepony.kvector.vector.Vector2f
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import kotlin.math.roundToInt import java.util.function.Function
import kotlin.collections.ArrayList
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
class ClientWorld( class ClientWorld(
val client: StarboundClient, val client: StarboundClient,
@ -21,31 +33,186 @@ class ClientWorld(
physics.debugDraw = client.gl.box2dRenderer physics.debugDraw = client.gl.box2dRenderer
} }
private fun determineChunkSize(cells: Int): Int {
for (i in 16 downTo 1) {
if (cells % i == 0) {
return i
}
}
throw RuntimeException("unreachable code")
}
val renderRegionWidth = if (size == null) 16 else determineChunkSize(size.x)
val renderRegionHeight = if (size == null) 16 else determineChunkSize(size.y)
inner class RenderRegion(val x: Int, val y: Int) {
inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) {
private val state get() = client.gl
private val layers = TileLayerList()
val bakedMeshes = ArrayList<Pair<ConfiguredStaticMesh, Int>>()
var isDirty = true
fun bake() {
if (!isDirty) return
isDirty = false
if (state.isSameThread()) {
for (mesh in bakedMeshes) {
mesh.first.close()
}
}
bakedMeshes.clear()
layers.clear()
for (x in 0 until renderRegionWidth) {
for (y in 0 until renderRegionHeight) {
if (!inBounds(x, y)) continue
val tile = view.getTile(x, y) ?: continue
val material = tile.material
if (material != null) {
client.tileRenderers.getTileRenderer(material.materialName).tesselate(tile, view, layers, Vector2i(x, y), background = isBackground)
}
val modifier = tile.modifier
if (modifier != null) {
client.tileRenderers.getModifierRenderer(modifier.modName).tesselate(tile, view, layers, Vector2i(x, y), background = isBackground, isModifier = true)
}
}
}
if (layers.isNotEmpty) {
for (mesh in bakedMeshes) {
mesh.first.close()
}
bakedMeshes.clear()
for ((baked, builder, zLevel) in layers.buildSortedLayerList()) {
bakedMeshes.add(ConfiguredStaticMesh(baked, builder) to zLevel)
}
layers.clear()
}
}
}
val view = OffsetCellAccess(this@ClientWorld, x * renderRegionWidth, y * renderRegionHeight)
val background = Layer(TileView.Background(view), true)
val foreground = Layer(TileView.Foreground(view), false)
fun addLayers(layers: LayeredRenderer, renderOrigin: Vector2f) {
background.bake()
foreground.bake()
for ((baked, zLevel) in background.bakedMeshes) {
layers.add(zLevel + Z_LEVEL_BACKGROUND) {
it.push().last().translateWithMultiplication(renderOrigin.x, renderOrigin.y)
baked.renderStacked(it)
it.pop()
}
}
for ((baked, zLevel) in foreground.bakedMeshes) {
layers.add(zLevel) {
it.push().last().translateWithMultiplication(renderOrigin.x, renderOrigin.y)
baked.renderStacked(it)
it.pop()
}
}
/*for (renderer in entityRenderers.values) {
layers.add(renderer.layer) {
val relative = renderer.renderPos - posVector2d
it.push().last().translateWithMultiplication(renderOrigin.x + relative.x.toFloat(), renderOrigin.y + relative.y.toFloat())
renderer.render(it)
it.pop()
}
}*/
/*val types = getLiquidTypes()
if (types.isNotEmpty()) {
layers.add(Z_LEVEL_LIQUID) {
it.push().last().translateWithMultiplication(renderOrigin.x, renderOrigin.y)
val program = state.programs.liquid
program.use()
program.transform = it.last()
val builder = program.builder
for (type in types) {
builder.builder.begin()
for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) {
val state = getCell(x, y)
if (state.liquid.def === type) {
builder.builder.quad(x.toFloat(), y.toFloat(), x + 1f, y + state.liquid.level)
}
}
}
program.baselineColor = type.color
builder.upload()
builder.draw()
}
it.pop()
}
}*/
}
}
val renderRegions = Long2ObjectOpenHashMap<RenderRegion>()
inline fun forEachRenderRegion(pos: ChunkPos, action: (RenderRegion) -> Unit) {
var (ix, iy) = pos.tile
ix /= renderRegionWidth
iy /= renderRegionHeight
for (x in ix .. ix + CHUNK_SIZE / renderRegionWidth) {
for (y in iy .. iy + CHUNK_SIZE / renderRegionWidth) {
renderRegions[x.toLong() shl 32 or y.toLong()]?.let(action)
}
}
}
override fun chunkFactory(pos: ChunkPos): ClientChunk { override fun chunkFactory(pos: ChunkPos): ClientChunk {
return ClientChunk(this, pos, chunkWidth, chunkHeight) return ClientChunk(this, pos)
} }
fun addLayers( fun addLayers(
size: AABB, size: AABB,
layers: LayeredRenderer layers: LayeredRenderer
) { ) {
val rx = roundTowardsNegativeInfinity(size.mins.x) / chunkWidth - 1 val rx = roundTowardsNegativeInfinity(size.mins.x) / renderRegionWidth - 1
val ry = roundTowardsNegativeInfinity(size.mins.y) / chunkHeight - 1 val ry = roundTowardsNegativeInfinity(size.mins.y) / renderRegionHeight - 1
val dx = roundTowardsNegativeInfinity(size.maxs.x - size.mins.x) / chunkWidth + 2 val dx = roundTowardsPositiveInfinity(size.maxs.x - size.mins.x) / renderRegionWidth + 2
val dy = roundTowardsNegativeInfinity(size.maxs.y - size.mins.y) / chunkWidth + 2 val dy = roundTowardsPositiveInfinity(size.maxs.y - size.mins.y) / renderRegionHeight + 2
for (x in rx .. rx + dx) { for (x in rx .. rx + dx) {
for (y in ry .. ry + dy) { for (y in ry .. ry + dy) {
val chunk = chunkMap[x, y] ?: continue val renderer = renderRegions.computeIfAbsent(x.toLong() shl 32 or y.toLong(), Long2ObjectFunction {
val renderer = chunk.Renderer(Vector2f(x * chunkWidth.toFloat(), y * chunkHeight.toFloat())) RenderRegion((it ushr 32).toInt(), it.toInt())
})
renderer.addLayers(layers) renderer.addLayers(layers, Vector2f(x * renderRegionWidth.toFloat(), y * renderRegionHeight.toFloat()))
chunk.bake()
} }
} }
val pos = client.screenToWorld(client.mouseCoordinatesF) val pos = client.screenToWorld(client.mouseCoordinatesF).toDoubleVector()
/*layers.add(-999999) { /*layers.add(-999999) {
val lightsize = 16 val lightsize = 16
@ -65,29 +232,30 @@ class ClientWorld(
} }
}*/ }*/
/*
val rayFan = ArrayList<Vector2d>()
for (i in 0 .. 359) { layers.add(-999999) {
rayFan.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI))) val rayFan = ArrayList<Vector2d>()
}
for (ray in rayFan) { for (i in 0 .. 359) {
val trace = castRayNaive(pos, ray, 16.0) rayFan.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI)))
}
client.gl.quadWireframe { for (ray in rayFan) {
for ((tpos, tile) in trace.traversedTiles) { val trace = castRayNaive(pos, ray, 16.0)
if (tile.material != null)
it.quad( client.gl.quadWireframe {
tpos.x.toFloat(), for ((tpos, tile) in trace.traversedTiles) {
tpos.y.toFloat(), if (tile.foreground.material != null)
tpos.x + 1f, it.quad(
tpos.y + 1f tpos.x.toFloat(),
) tpos.y.toFloat(),
tpos.x + 1f,
tpos.y + 1f
)
}
} }
} }
} }
*/
//rayLightCircleNaive(pos, 48.0, falloffByTravel = 1.0, falloffByTile = 3.0) //rayLightCircleNaive(pos, 48.0, falloffByTravel = 1.0, falloffByTile = 3.0)

View File

@ -227,7 +227,7 @@ class StarboundClient(val starbound: Starbound) : Closeable {
val tileRenderers = TileRenderers(this) val tileRenderers = TileRenderers(this)
var world: ClientWorld? = ClientWorld(this, 0L, null, true) var world: ClientWorld? = ClientWorld(this, 0L, Vector2i(3000, 2000), true)
init { init {
putDebugLog("Initialized OpenGL context") putDebugLog("Initialized OpenGL context")

View File

@ -19,8 +19,8 @@ import ru.dbotthepony.kvector.vector.Vector2i
import kotlin.collections.HashMap import kotlin.collections.HashMap
data class TileLayer( data class TileLayer(
val bakedProgramState: ConfiguredShaderProgram<GLTileProgram>, val program: ConfiguredShaderProgram<GLTileProgram>,
val vertexBuilder: VertexBuilder, val vertices: VertexBuilder,
val zPos: Int val zPos: Int
) )
@ -35,7 +35,7 @@ class TileLayerList {
fun computeIfAbsent(program: ConfiguredShaderProgram<GLTileProgram>, zLevel: Int, compute: () -> VertexBuilder): VertexBuilder { fun computeIfAbsent(program: ConfiguredShaderProgram<GLTileProgram>, zLevel: Int, compute: () -> VertexBuilder): VertexBuilder {
return layers.computeIfAbsent(program) { Int2ObjectAVLTreeMap() }.computeIfAbsent(zLevel, Int2ObjectFunction { return layers.computeIfAbsent(program) { Int2ObjectAVLTreeMap() }.computeIfAbsent(zLevel, Int2ObjectFunction {
return@Int2ObjectFunction TileLayer(program, compute.invoke(), zLevel) return@Int2ObjectFunction TileLayer(program, compute.invoke(), zLevel)
}).vertexBuilder }).vertices
} }
fun buildSortedLayerList(): List<TileLayer> { fun buildSortedLayerList(): List<TileLayer> {

View File

@ -58,6 +58,14 @@ fun roundTowardsPositiveInfinity(value: Double): Int {
return value.toInt() return value.toInt()
} }
fun divideUp(a: Int, b: Int): Int {
return if (a % b == 0) {
a / b
} else {
a / b + 1
}
}
private const val EPSILON = 0.00000001 private const val EPSILON = 0.00000001
fun weakCompare(a: Double, b: Double, epsilon: Double = EPSILON): Int { fun weakCompare(a: Double, b: Double, epsilon: Double = EPSILON): Int {

View File

@ -42,8 +42,6 @@ import kotlin.collections.HashSet
abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, This>>( abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, This>>(
val world: WorldType, val world: WorldType,
val pos: ChunkPos, val pos: ChunkPos,
val width: Int,
val height: Int
) : ICellAccess { ) : ICellAccess {
var changeset = 0 var changeset = 0
private set(value) { private set(value) {
@ -72,7 +70,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
private var isEmpty = true private var isEmpty = true
protected val cells by lazy { protected val cells by lazy {
Object2DArray.nulls<Cell>(width, height) Object2DArray.nulls<Cell>(CHUNK_SIZE, CHUNK_SIZE)
} }
override fun getCell(x: Int, y: Int): IChunkCell { override fun getCell(x: Int, y: Int): IChunkCell {
@ -91,7 +89,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
val localForegroundView = TileView.Foreground(this) val localForegroundView = TileView.Foreground(this)
// relative world cells access (accessing 0, 0 will lookup cell in world, relative to this chunk) // relative world cells access (accessing 0, 0 will lookup cell in world, relative to this chunk)
val worldView = OffsetCellAccess(world.chunkMap, pos.x * width, pos.y * height) val worldView = OffsetCellAccess(world, pos.x * CHUNK_SIZE, pos.y * CHUNK_SIZE)
val worldBackgroundView = TileView.Background(worldView) val worldBackgroundView = TileView.Background(worldView)
val worldForegroundView = TileView.Foreground(worldView) val worldForegroundView = TileView.Foreground(worldView)
@ -104,7 +102,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
private val body by lazy { private val body by lazy {
world.physics.createBody(BodyDef( world.physics.createBody(BodyDef(
position = pos.tile(width, height).toDoubleVector(), position = pos.tile.toDoubleVector(),
userData = this userData = this
)) ))
} }
@ -149,8 +147,8 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
inner class Cell(x: Int, y: Int) : IChunkCell { inner class Cell(x: Int, y: Int) : IChunkCell {
override val x: Int = x + pos.x * width override val x: Int = x + pos.x * CHUNK_SIZE
override val y: Int = y + pos.y * width override val y: Int = y + pos.y * CHUNK_SIZE
inner class Tile(private val foreground: Boolean) : ITileState { inner class Tile(private val foreground: Boolean) : ITileState {
private fun change() { private fun change() {
@ -342,7 +340,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
override fun randomLongFor(x: Int, y: Int): Long { override fun randomLongFor(x: Int, y: Int): Long {
return world.chunkMap.randomLongFor(x or pos.x shl CHUNK_SIZE_BITS, y or pos.y shl CHUNK_SIZE_BITS) return world.randomLongFor(x or pos.x shl CHUNK_SIZE_BITS, y or pos.y shl CHUNK_SIZE_BITS)
} }
protected val entities = ReferenceOpenHashSet<Entity>() protected val entities = ReferenceOpenHashSet<Entity>()

View File

@ -29,9 +29,9 @@ private fun circulate(value: Int, bounds: Int): Int {
data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable<ChunkPos> { data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable<ChunkPos> {
constructor(pos: IStruct2i) : this(pos.component1(), pos.component2()) constructor(pos: IStruct2i) : this(pos.component1(), pos.component2())
fun tile(width: Int, height: Int): Vector2i { val tileX = x shl CHUNK_SIZE_BITS
return Vector2i(x * width, y * height) val tileY = y shl CHUNK_SIZE_BITS
} val tile = Vector2i(tileX, tileY)
val top: ChunkPos get() { val top: ChunkPos get() {
return ChunkPos(x, y + 1) return ChunkPos(x, y + 1)

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.math.divideUp
abstract class CoordinateMapper { abstract class CoordinateMapper {
protected fun positiveModulo(a: Int, b: Int): Int { protected fun positiveModulo(a: Int, b: Int): Int {
val result = a % b val result = a % b
@ -16,16 +18,16 @@ abstract class CoordinateMapper {
return if (result < 0f) result + b else result return if (result < 0f) result + b else result
} }
abstract val chunks: Int
abstract fun cell(value: Int): Int abstract fun cell(value: Int): Int
abstract fun cell(value: Double): Double abstract fun cell(value: Double): Double
abstract fun cell(value: Float): Float abstract fun cell(value: Float): Float
abstract fun chunk(value: Int): Int abstract fun chunk(value: Int): Int
abstract fun cellToChunk(value: Int): Int abstract fun chunkFromCell(value: Int): Int
fun cellToChunk(value: Float): Int = cellToChunk(value.toInt()) fun chunkFromCell(value: Float): Int = chunkFromCell(value.toInt())
fun cellToChunk(value: Double): Int = cellToChunk(value.toInt()) fun chunkFromCell(value: Double): Int = chunkFromCell(value.toInt())
abstract fun cellModulus(value: Int): Int
// inside world bounds // inside world bounds
abstract fun inBoundsCell(value: Int): Boolean abstract fun inBoundsCell(value: Int): Boolean
@ -36,25 +38,24 @@ abstract class CoordinateMapper {
open fun isValidChunkIndex(value: Int): Boolean = inBoundsChunk(value) open fun isValidChunkIndex(value: Int): Boolean = inBoundsChunk(value)
object Infinite : CoordinateMapper() { object Infinite : CoordinateMapper() {
override val chunks: Int
get() = Int.MAX_VALUE
override fun cell(value: Int): Int = value override fun cell(value: Int): Int = value
override fun cell(value: Double): Double = value override fun cell(value: Double): Double = value
override fun cell(value: Float): Float = value override fun cell(value: Float): Float = value
override fun chunk(value: Int): Int = value override fun chunk(value: Int): Int = value
override fun cellToChunk(value: Int): Int { override fun chunkFromCell(value: Int): Int {
return value shr CHUNK_SIZE_BITS return value shr CHUNK_SIZE_BITS
} }
override fun cellModulus(value: Int): Int {
return value and CHUNK_SIZE_MASK
}
override fun inBoundsCell(value: Int) = true override fun inBoundsCell(value: Int) = true
override fun inBoundsChunk(value: Int) = true override fun inBoundsChunk(value: Int) = true
} }
class Wrapper(private val cells: Int, private val chunkSize: Int) : CoordinateMapper() { class Wrapper(private val cells: Int) : CoordinateMapper() {
private val chunks = cells / chunkSize override val chunks = divideUp(cells, CHUNK_SIZE)
override fun inBoundsCell(value: Int) = value in 0 until cells override fun inBoundsCell(value: Int) = value in 0 until cells
override fun inBoundsChunk(value: Int) = value in 0 until chunks override fun inBoundsChunk(value: Int) = value in 0 until chunks
@ -62,12 +63,8 @@ abstract class CoordinateMapper {
override fun isValidCellIndex(value: Int) = true override fun isValidCellIndex(value: Int) = true
override fun isValidChunkIndex(value: Int) = true override fun isValidChunkIndex(value: Int) = true
override fun cellToChunk(value: Int): Int { override fun chunkFromCell(value: Int): Int {
return chunk(value / chunkSize) return chunk(value shr CHUNK_SIZE_BITS)
}
override fun cellModulus(value: Int): Int {
return positiveModulo(value, chunkSize)
} }
override fun cell(value: Int): Int = positiveModulo(value, cells) override fun cell(value: Int): Int = positiveModulo(value, cells)
@ -76,19 +73,15 @@ abstract class CoordinateMapper {
override fun chunk(value: Int): Int = positiveModulo(value, chunks) override fun chunk(value: Int): Int = positiveModulo(value, chunks)
} }
class Clamper(private val cells: Int, private val chunkSize: Int) : CoordinateMapper() { class Clamper(private val cells: Int) : CoordinateMapper() {
private val chunks = cells / chunkSize override val chunks = divideUp(cells, CHUNK_SIZE)
override fun inBoundsCell(value: Int): Boolean { override fun inBoundsCell(value: Int): Boolean {
return value in 0 until cells return value in 0 until cells
} }
override fun cellModulus(value: Int): Int { override fun chunkFromCell(value: Int): Int {
return positiveModulo(value, chunkSize) return chunk(value shr CHUNK_SIZE_BITS)
}
override fun cellToChunk(value: Int): Int {
return chunk(value / chunkSize)
} }
override fun inBoundsChunk(value: Int): Boolean { override fun inBoundsChunk(value: Int): Boolean {

View File

@ -22,7 +22,6 @@ import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kvector.arrays.Int2DArray import ru.dbotthepony.kvector.arrays.Int2DArray
import ru.dbotthepony.kvector.arrays.Object2DArray import ru.dbotthepony.kvector.arrays.Object2DArray
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.util2d.AABBi
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import java.lang.ref.ReferenceQueue import java.lang.ref.ReferenceQueue
@ -34,69 +33,55 @@ const val CHUNK_SIZE = 1 shl CHUNK_SIZE_BITS // 32
const val CHUNK_SIZE_FF = CHUNK_SIZE - 1 const val CHUNK_SIZE_FF = CHUNK_SIZE - 1
const val CHUNK_SIZEd = CHUNK_SIZE.toDouble() const val CHUNK_SIZEd = CHUNK_SIZE.toDouble()
@Suppress("UNCHECKED_CAST")
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>( abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(
val seed: Long, val seed: Long,
val size: Vector2i?, val size: Vector2i?,
val loopX: Boolean, loopX: Boolean,
val loopY: Boolean loopY: Boolean
) { ) : ICellAccess {
private fun determineChunkSize(cells: Int): Int { val x: CoordinateMapper = if (size == null) CoordinateMapper.Infinite else if (loopX) CoordinateMapper.Wrapper(size.x) else CoordinateMapper.Clamper(size.x)
for (i in 32 downTo 1) { val y: CoordinateMapper = if (size == null) CoordinateMapper.Infinite else if (loopY) CoordinateMapper.Wrapper(size.y) else CoordinateMapper.Clamper(size.y)
if (cells % i == 0) {
return i
}
}
throw RuntimeException("unreachable code") // whenever provided cell position is within actual world borders, ignoring wrapping
fun inBounds(x: Int, y: Int) = this.x.inBoundsCell(x) && this.y.inBoundsCell(y)
fun inBounds(value: IStruct2i) = this.x.inBoundsCell(value.component1()) && this.y.inBoundsCell(value.component2())
fun chunkFromCell(x: Int, y: Int) = ChunkPos(this.x.chunkFromCell(x), this.y.chunkFromCell(y))
fun chunkFromCell(x: Double, y: Double) = ChunkPos(this.x.chunkFromCell(x.toInt()), this.y.chunkFromCell(y.toInt()))
fun chunkFromCell(value: IStruct2i) = chunkFromCell(value.component1(), value.component2())
fun chunkFromCell(value: IStruct2d) = chunkFromCell(value.component1(), value.component2())
val background = TileView.Background(this)
val foreground = TileView.Foreground(this)
final override fun randomLongFor(x: Int, y: Int) = super.randomLongFor(x, y) xor seed
override fun getCellDirect(x: Int, y: Int): IChunkCell? {
if (!this.x.inBoundsCell(x) || !this.y.inBoundsCell(y)) return null
return getCell(x, y)
} }
val chunkWidth = size?.x?.let(::determineChunkSize) ?: CHUNK_SIZE override fun getCell(x: Int, y: Int): IChunkCell? {
val chunkHeight = size?.y?.let(::determineChunkSize) ?: CHUNK_SIZE return chunkMap.getCell(x, y)
}
abstract inner class ChunkMap : ICellAccess {
abstract val x: CoordinateMapper
abstract val y: CoordinateMapper
val background = TileView.Background(this)
val foreground = TileView.Foreground(this)
fun inBounds(x: Int, y: Int) = this.x.inBoundsCell(x) && this.y.inBoundsCell(y)
fun inBounds(value: IStruct2i) = this.x.inBoundsCell(value.component1()) && this.y.inBoundsCell(value.component2())
fun cellToChunk(x: Int, y: Int) = ChunkPos(this.x.cellToChunk(x), this.y.cellToChunk(y))
fun cellToChunk(x: Double, y: Double) = ChunkPos(this.x.cellToChunk(x.toInt()), this.y.cellToChunk(y.toInt()))
fun cellToChunk(value: IStruct2i) = cellToChunk(value.component1(), value.component2())
fun cellToChunk(value: IStruct2d) = cellToChunk(value.component1(), value.component2())
final override fun randomLongFor(x: Int, y: Int) = super.randomLongFor(x, y) xor seed
abstract inner class ChunkMap {
abstract operator fun get(x: Int, y: Int): ChunkType? abstract operator fun get(x: Int, y: Int): ChunkType?
operator fun get(pos: ChunkPos) = get(pos.x, pos.y)
abstract fun promote(self: ChunkType) abstract fun promote(self: ChunkType)
abstract fun purge()
abstract fun remove(x: Int, y: Int)
abstract fun getCell(x: Int, y: Int): IChunkCell?
protected val queue = ReferenceQueue<ChunkType>() protected val queue = ReferenceQueue<ChunkType>()
operator fun get(pos: ChunkPos) = get(pos.x, pos.y)
abstract fun purge()
protected inner class Ref(chunk: ChunkType) : WeakReference<ChunkType>(chunk, queue) { protected inner class Ref(chunk: ChunkType) : WeakReference<ChunkType>(chunk, queue) {
val pos = chunk.pos val pos = chunk.pos
} }
abstract fun remove(x: Int, y: Int)
override fun getCell(x: Int, y: Int): IChunkCell? {
if (!this.x.isValidCellIndex(x) || !this.y.isValidCellIndex(y)) return null
val ix = this.x.cell(x)
val iy = this.y.cell(y)
return get(this.x.cellToChunk(ix), this.y.cellToChunk(iy))?.getCell(this.x.cellModulus(ix), this.y.cellModulus(iy))
}
override fun getCellDirect(x: Int, y: Int): IChunkCell? {
if (!this.x.inBoundsCell(x) || !this.y.inBoundsCell(y)) return null
return getCell(x, y)
}
protected fun create(x: Int, y: Int): ChunkType { protected fun create(x: Int, y: Int): ChunkType {
purge() purge()
val pos = ChunkPos(x, y) val pos = ChunkPos(x, y)
@ -106,7 +91,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
for (ent in orphanedEntities) { for (ent in orphanedEntities) {
val (ex, ey) = ent.position val (ex, ey) = ent.position
if (this.x.cellToChunk(ex) == x && this.y.cellToChunk(ey) == y) { if (this@World.x.chunkFromCell(ex) == x && this@World.y.chunkFromCell(ey) == y) {
orphanedInThisChunk.add(ent) orphanedInThisChunk.add(ent)
} }
} }
@ -119,33 +104,41 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
} }
// infinite chunk map is around 30% slower than rectangular one // hash chunk map is around 30% slower than rectangular one
inner class InfiniteChunkMap : ChunkMap() { inner class HashChunkMap : ChunkMap() {
private val map = Long2ObjectOpenHashMap<Any>() private val map = Long2ObjectOpenHashMap<Any>()
override val x = CoordinateMapper.Infinite
override val y = CoordinateMapper.Infinite
override fun get(x: Int, y: Int): ChunkType { override fun getCell(x: Int, y: Int): IChunkCell? {
if (!this@World.x.isValidCellIndex(x) || !this@World.y.isValidCellIndex(y)) return null
val ix = this@World.x.cell(x)
val iy = this@World.y.cell(y)
return this[this@World.x.chunkFromCell(ix), this@World.y.chunkFromCell(iy)]?.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK)
}
override fun get(x: Int, y: Int): ChunkType? {
if (!this@World.x.isValidChunkIndex(x) || !this@World.y.isValidChunkIndex(y)) return null
val x = this@World.x.chunk(x)
val y = this@World.y.chunk(y)
return map[ChunkPos.toLong(x, y)]?.let { return map[ChunkPos.toLong(x, y)]?.let {
if (it is World<*, *>.ChunkMap.Ref) { if (it is World<*, *>.ChunkMap.Ref) {
it.get() as ChunkType? it.get() as ChunkType?
} else { } else {
it as ChunkType? it as ChunkType?
} }
} ?: create(x, y).also { } ?: create(x, y).also { map[ChunkPos.toLong(x, y)] = Ref(it) }
map[ChunkPos.toLong(x, y)] = Ref(it)
}
} }
override fun purge() { override fun purge() {
var next = queue.poll() as World<*, *>.ChunkMap.Ref? var next = queue.poll() as World<*, *>.ChunkMap.Ref?
while (next != null) { while (next != null) {
val get = map[ChunkPos.toLong(next.pos.x, next.pos.y)] val k = ChunkPos.toLong(next.pos.x, next.pos.y)
val get = map[k]
if (get === next) { if (get === next)
map.remove(ChunkPos.toLong(next.pos.x, next.pos.y)) map.remove(k)
}
next = queue.poll() as World<*, *>.ChunkMap.Ref? next = queue.poll() as World<*, *>.ChunkMap.Ref?
} }
@ -167,6 +160,9 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
override fun remove(x: Int, y: Int) { override fun remove(x: Int, y: Int) {
val x = this@World.x.chunk(x)
val y = this@World.y.chunk(y)
val ref = map.remove(ChunkPos.toLong(x, y)) val ref = map.remove(ChunkPos.toLong(x, y))
if (ref is World<*, *>.ChunkMap.Ref) { if (ref is World<*, *>.ChunkMap.Ref) {
@ -175,38 +171,44 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
} }
inner class RectChunkMap : ChunkMap() { inner class ArrayChunkMap : ChunkMap() {
val width = size!!.x val width = size!!.x
val height = size!!.y val height = size!!.y
override val x: CoordinateMapper = if (loopX) CoordinateMapper.Wrapper(width, chunkWidth) else CoordinateMapper.Clamper(width, chunkWidth) private val map = Object2DArray.nulls<Any>(divideUp(width, CHUNK_SIZE), divideUp(height, CHUNK_SIZE))
override val y: CoordinateMapper = if (loopY) CoordinateMapper.Wrapper(height, chunkHeight) else CoordinateMapper.Clamper(height, chunkHeight)
private val map = Object2DArray.nulls<Any>(width / chunkWidth, height / chunkHeight) private fun getRaw(x: Int, y: Int): ChunkType {
return map[x, y]?.let {
override fun get(x: Int, y: Int): ChunkType? {
if (!this.x.isValidChunkIndex(x) || !this.y.isValidChunkIndex(y)) return null
val ix = this.x.chunk(x)
val iy = this.x.chunk(y)
return map[ix, iy]?.let {
if (it is World<*, *>.ChunkMap.Ref) { if (it is World<*, *>.ChunkMap.Ref) {
it.get() as ChunkType? it.get() as ChunkType?
} else { } else {
it as ChunkType? it as ChunkType?
} }
} ?: create(ix, iy).also { } ?: create(x, y).also {
map[ix, iy] = Ref(it) map[x, y] = Ref(it)
} }
} }
override fun getCell(x: Int, y: Int): IChunkCell? {
if (!this@World.x.isValidCellIndex(x) || !this@World.y.isValidCellIndex(y)) return null
val ix = this@World.x.cell(x)
val iy = this@World.y.cell(y)
return getRaw(ix ushr CHUNK_SIZE_BITS, iy ushr CHUNK_SIZE_BITS).getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK)
}
override fun get(x: Int, y: Int): ChunkType? {
if (!this@World.x.isValidChunkIndex(x) || !this@World.y.isValidChunkIndex(y)) return null
return getRaw(this@World.x.chunk(x), this@World.y.chunk(y))
}
override fun purge() { override fun purge() {
while (queue.poll() != null) {} while (queue.poll() != null) {}
} }
override fun promote(self: ChunkType) { override fun promote(self: ChunkType) {
val (x, y) = self.pos val (x, y) = self.pos
val ref = map[x, y] val ref = map[x, y]
if (ref !is World<*, *>.ChunkMap.Ref) { if (ref !is World<*, *>.ChunkMap.Ref) {
@ -221,22 +223,20 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
override fun remove(x: Int, y: Int) { override fun remove(x: Int, y: Int) {
if (!this.x.isValidChunkIndex(x) || !this.y.isValidChunkIndex(y)) return val x = this@World.x.chunk(x)
val y = this@World.y.chunk(y)
val ix = this.x.chunk(x) val old = map[x, y]
val iy = this.x.chunk(y)
val old = map[ix, iy]
if (old is World<*, *>.ChunkMap.Ref) { if (old is World<*, *>.ChunkMap.Ref) {
old.clear() old.clear()
} }
map[ix, iy] = null map[x, y] = null
} }
} }
val chunkMap: ChunkMap = if (size != null) RectChunkMap() else InfiniteChunkMap() val chunkMap: ChunkMap = if (size != null) ArrayChunkMap() else HashChunkMap()
/** /**
* Chunks, which have their collision mesh changed * Chunks, which have their collision mesh changed
@ -410,7 +410,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
for (x in tiles.mins.x .. tiles.maxs.x) { for (x in tiles.mins.x .. tiles.maxs.x) {
for (y in tiles.mins.y .. tiles.maxs.y) { for (y in tiles.mins.y .. tiles.maxs.y) {
if (predicate.test(chunkMap.getCell(x, y) ?: continue)) { if (predicate.test(getCell(x, y) ?: continue)) {
return true return true
} }
} }
@ -434,7 +434,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
return 1 return 1
} }
val tile = chunkMap.getCell(worldPosX, worldPosY) val tile = getCell(worldPosX, worldPosY)
var newIntensity: Int var newIntensity: Int

View File

@ -94,7 +94,7 @@ abstract class Entity(override val world: World<*, *>) : IEntity {
return return
} }
val chunkPos = world.chunkMap.cellToChunk(position) val chunkPos = world.chunkFromCell(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 (expected chunk $chunkPos, got chunk ${value.pos})") throw IllegalStateException("Set proper position before setting chunk this Entity belongs to (expected chunk $chunkPos, got chunk ${value.pos})")
@ -120,13 +120,13 @@ abstract class Entity(override val world: World<*, *>) : IEntity {
return return
val old = field val old = field
field = Vector2d(world.chunkMap.x.cell(value.x), world.chunkMap.x.cell(value.y)) field = Vector2d(world.x.cell(value.x), world.y.cell(value.y))
movement.notifyPositionChanged() movement.notifyPositionChanged()
if (isSpawned && !isRemoved) { if (isSpawned && !isRemoved) {
val oldChunkPos = world.chunkMap.cellToChunk(old) val oldChunkPos = world.chunkFromCell(old)
val newChunkPos = world.chunkMap.cellToChunk(field) val newChunkPos = world.chunkFromCell(field)
if (oldChunkPos != newChunkPos) { if (oldChunkPos != newChunkPos) {
chunk = world.chunkMap[newChunkPos] chunk = world.chunkMap[newChunkPos]
@ -154,7 +154,7 @@ abstract class Entity(override val world: World<*, *>) : IEntity {
isSpawned = true isSpawned = true
world.entities.add(this) world.entities.add(this)
chunk = world.chunkMap[world.chunkMap.cellToChunk(position)] chunk = world.chunkMap[world.chunkFromCell(position)]
if (chunk == null) { if (chunk == null) {
world.orphanedEntities.add(this) world.orphanedEntities.add(this)