KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt
DBotThePony f907124af6
И ещё дальше
Более полные коллизии
Ентити
Приседания
UserInput
Больше отрисовки
Более реалистичные движения
2022-02-06 18:40:23 +07:00

466 lines
14 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.api.IStruct2d
import ru.dbotthepony.kstarbound.api.IStruct2i
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.*
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.absoluteValue
/**
* Представляет из себя класс, который содержит состояние тайла на заданной позиции
*/
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 {
/**
* Относительная проверка находится ли координата вне границ чанка
*/
fun isOutside(x: Int, y: Int): Boolean {
return x !in 0 until CHUNK_SIZE || y !in 0 until CHUNK_SIZE
}
}
/**
* Предоставляет интерфейс для доступа к тайлам в чанке
*/
interface ITileGetter : ITileMap {
/**
* Возвращает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка
*/
operator fun get(x: Int, y: Int): ChunkTile?
/**
* Возвращает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка
*/
operator fun get(pos: Vector2i) = get(pos.x, pos.y)
/**
* Возвращает итератор пар <Vector2i, Тайл?>
*
* Вектор имеет ОТНОСИТЕЛЬНЫЕ значения внутри самого чанка
*/
val posToTile: Iterator<Pair<Vector2i, ChunkTile?>> get() {
return object : Iterator<Pair<Vector2i, ChunkTile?>> {
private var x = 0
private var y = 0
private fun idx() = x + CHUNK_SIZE * y
override fun hasNext(): Boolean {
return idx() < CHUNK_SIZE * CHUNK_SIZE
}
override fun next(): Pair<Vector2i, ChunkTile?> {
if (!hasNext()) {
throw IllegalStateException("Already iterated everything!")
}
val tile = this@ITileGetter[x, y]
val pos = Vector2i(x, y)
x++
if (x >= CHUNK_SIZE) {
y++
x = 0
}
return pos to tile
}
}
}
}
/**
* Интерфейс предоставляет из себя описание класса, который имеет координаты чанка
*/
interface IChunkPositionable : ITileMap {
val pos: ChunkPos
/**
* Возвращает псевдослучайное Long для заданной позиции
*
* Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции
*/
fun randomLongFor(x: Int, y: Int): Long {
var long = (x or (pos.x shl CHUNK_SHIFT)) * 738548L + (y or (pos.y shl CHUNK_SHIFT)) * 2191293543L
long = long xor 8339437585692L
long = (long ushr 4) or (long shl 52)
long *= 7848344324L
long = (long ushr 12) or (long shl 44)
return long
}
/**
* Возвращает псевдослучайное нормализированное Double для заданной позиции
*
* Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции
*/
fun randomDoubleFor(x: Int, y: Int): Double {
return (randomLongFor(x, y) / 9.223372036854776E18) / 2.0 + 0.5
}
/**
* Возвращает псевдослучайное Long для заданной позиции
*
* Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции
*/
fun randomLongFor(pos: Vector2i) = randomLongFor(pos.x, pos.y)
/**
* Возвращает псевдослучайное нормализированное Double для заданной позиции
*
* Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции
*/
fun randomDoubleFor(pos: Vector2i) = randomDoubleFor(pos.x, pos.y)
}
/**
* Предоставляет интерфейс по установке тайлов в чанке
*/
interface ITileSetter : ITileMap {
/**
* Устанавливает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка
*/
operator fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile?
/**
* Устанавливает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка
*/
operator fun set(pos: Vector2i, tile: TileDefinition?) = set(pos.x, pos.y, tile)
}
interface ITileGetterSetter : ITileGetter, ITileSetter
interface ITileChunk : ITileGetter, IChunkPositionable
interface IMutableTileChunk : ITileChunk, ITileSetter
const val CHUNK_SHIFT = 5
const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 32
const val CHUNK_SIZE_FF = CHUNK_SIZE - 1
const val CHUNK_SIZEf = CHUNK_SIZE.toFloat()
const val CHUNK_SIZEd = CHUNK_SIZE.toDouble()
data class ChunkPos(override val x: Int, override val y: Int) : IVector2i<ChunkPos>() {
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 {
val ZERO = ChunkPos(0, 0)
fun fromTilePosition(input: IStruct2i): ChunkPos {
val (x, y) = input
return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y))
}
fun fromTilePosition(input: IStruct2d): ChunkPos {
val (x, y) = input
return fromTilePosition(x, y)
}
fun fromTilePosition(x: Int, y: Int): ChunkPos {
return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y))
}
fun fromTilePosition(x: Double, y: Double): ChunkPos {
return ChunkPos(tileToChunkComponent(roundByAbsoluteValue(x)), tileToChunkComponent(roundByAbsoluteValue(y)))
}
fun normalizeCoordinate(input: Int): Int {
val band = input and CHUNK_SIZE_FF
if (band < 0) {
return band + CHUNK_SIZE_FF
}
return band
}
fun tileToChunkComponent(comp: Int): Int {
if (comp < 0) {
return -(comp.absoluteValue shr CHUNK_SHIFT) - 1
}
return comp shr CHUNK_SHIFT
}
}
}
/**
* Предоставляет доступ к чанку и его соседям
*
* В основном для использования в местах, где нужен не мир, а определённый чанк мира,
* и при этом координаты проверяются относительно чанка и могут спокойно выйти за его пределы,
* с желанием получить тайл из соседнего чанка
*/
open class TileChunkView(
open val center: ITileChunk,
open val right: ITileChunk?,
open val top: ITileChunk?,
open val topRight: ITileChunk?,
open val topLeft: ITileChunk?,
open val left: ITileChunk?,
open val bottom: ITileChunk?,
open val bottomLeft: ITileChunk?,
open val bottomRight: ITileChunk?,
) : ITileChunk {
override fun get(x: Int, y: Int): ChunkTile? {
if (x in 0 .. CHUNK_SIZE_FF) {
if (y in 0 .. CHUNK_SIZE_FF) {
return center[x, y]
}
if (y < 0) {
return bottom?.get(x, y + CHUNK_SIZE)
} else {
return top?.get(x, y - CHUNK_SIZE)
}
}
if (x < 0) {
if (y in 0 .. CHUNK_SIZE_FF) {
return left?.get(x + CHUNK_SIZE, y)
}
if (y < 0) {
return bottomLeft?.get(x + CHUNK_SIZE, y + CHUNK_SIZE)
} else {
return topLeft?.get(x + CHUNK_SIZE, y - CHUNK_SIZE)
}
} else {
if (y in 0 .. CHUNK_SIZE_FF) {
return right?.get(x - CHUNK_SIZE, y)
}
if (y < 0) {
return bottomRight?.get(x - CHUNK_SIZE, y + CHUNK_SIZE)
} else {
return topRight?.get(x - CHUNK_SIZE, y - CHUNK_SIZE)
}
}
}
override val pos: ChunkPos
get() = center.pos
}
class MutableTileChunkView(
override val center: IMutableTileChunk,
override val right: IMutableTileChunk?,
override val top: IMutableTileChunk?,
override val topRight: IMutableTileChunk?,
override val topLeft: IMutableTileChunk?,
override val left: IMutableTileChunk?,
override val bottom: IMutableTileChunk?,
override val bottomLeft: IMutableTileChunk?,
override val bottomRight: IMutableTileChunk?,
) : TileChunkView(center, right, top, topRight, topLeft, left, bottom, bottomLeft, bottomRight), IMutableTileChunk {
override fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? {
if (x in 0 .. CHUNK_SIZE_FF) {
if (y in 0 .. CHUNK_SIZE_FF) {
return center.set(x, y, tile)
}
if (y < 0) {
return bottom?.set(x, y + CHUNK_SIZE, tile)
} else {
return top?.set(x, y - CHUNK_SIZE, tile)
}
}
if (x < 0) {
if (y in 0 .. CHUNK_SIZE_FF) {
return left?.set(x + CHUNK_SIZE, y, tile)
}
if (y < 0) {
return bottomLeft?.set(x + CHUNK_SIZE, y + CHUNK_SIZE, tile)
} else {
return topLeft?.set(x + CHUNK_SIZE, y - CHUNK_SIZE, tile)
}
} else {
if (y in 0 .. CHUNK_SIZE_FF) {
return right?.set(x - CHUNK_SIZE, y, tile)
}
if (y < 0) {
return bottomRight?.set(x - CHUNK_SIZE, y + CHUNK_SIZE, tile)
} else {
return topRight?.set(x - CHUNK_SIZE, y - CHUNK_SIZE, tile)
}
}
}
}
/**
* Чанк мира
*
* Хранит в себе тайлы и ентити внутри себя
*
* Считается, что один тайл имеет форму квадрата и сторона квадрата примерно равна полуметру,
* что будет называться Starbound Unit
*
* Весь игровой мир будет измеряться в Starbound Unit'ах
*/
open class Chunk(val world: World<*>?, val pos: ChunkPos) {
/**
* Возвращает счётчик изменений чанка
*/
var changeset = 0
private set
fun incChangeset() {
changeset++
}
val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble())
inner class TileLayer : IMutableTileChunk {
/**
* Возвращает счётчик изменений этого слоя
*/
var changeset = 0
private set
fun incChangeset() {
changeset++
this@Chunk.changeset++
}
private val collisionCache = ArrayList<AABB>()
private val collisionCacheView = Collections.unmodifiableCollection(collisionCache)
private var collisionChangeset = -1
// максимально грубое комбинирование тайлов в бруски для коллизии
// TODO: https://ru.wikipedia.org/wiki/R-дерево_(структураанных)
private fun bakeCollisions() {
collisionChangeset = changeset
val seen = BooleanArray(tiles.size)
collisionCache.clear()
val xAdd = pos.x * CHUNK_SIZEd
val yAdd = pos.y * CHUNK_SIZEd
for (y in 0 .. CHUNK_SIZE_FF) {
var first: Int? = null
var last = 0
for (x in 0 .. CHUNK_SIZE_FF) {
if (tiles[x or (y shl CHUNK_SHIFT)] != null) {
if (first == null) {
first = x
}
last = x
} else {
if (first != null) {
collisionCache.add(AABB(
Vector2d(x = xAdd + first.toDouble(), y = y.toDouble() + yAdd),
Vector2d(x = xAdd + last.toDouble() + 1.0, y = y.toDouble() + 1.0 + yAdd),
))
first = null
}
}
}
if (first != null) {
collisionCache.add(AABB(
Vector2d(x = first.toDouble() + xAdd, y = y.toDouble() + yAdd),
Vector2d(x = last.toDouble() + 1.0 + xAdd, y = y.toDouble() + 1.0 + yAdd),
))
}
}
}
/**
* Возвращает список AABB тайлов этого слоя
*
* Данный список напрямую указывает на внутреннее состояние и будет изменён при перестройке
* коллизии чанка, поэтому если необходим стабильный список, его необходимо скопировать
*/
fun collisionLayers(): Collection<AABB> {
if (collisionChangeset != changeset) {
bakeCollisions()
}
return collisionCacheView
}
override val pos: ChunkPos
get() = this@Chunk.pos
/**
* Хранит тайлы как x + y * CHUNK_SIZE
*/
private val tiles = arrayOfNulls<ChunkTile>(CHUNK_SIZE * CHUNK_SIZE)
override operator fun get(x: Int, y: Int): ChunkTile? {
if (isOutside(x, y))
return null
return tiles[x or (y shl CHUNK_SHIFT)]
}
operator fun set(x: Int, y: Int, tile: ChunkTile?) {
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
}
override operator fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? {
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(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 ?: 0L)
}
}
val foreground = TileLayer()
val background = TileLayer()
companion object {
val EMPTY = object : IMutableTileChunk {
override val pos = ChunkPos(0, 0)
override fun get(x: Int, y: Int): ChunkTile? = null
override fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? = null
}
private val aabbBase = AABB(
Vector2d.ZERO,
Vector2d(CHUNK_SIZE.toDouble(), CHUNK_SIZE.toDouble()),
)
}
}