Более полные коллизии Ентити Приседания UserInput Больше отрисовки Более реалистичные движения
466 lines
14 KiB
Kotlin
466 lines
14 KiB
Kotlin
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()),
|
||
)
|
||
}
|
||
}
|