Отрисовка сущностей

This commit is contained in:
DBotThePony 2022-02-07 11:08:54 +07:00
parent f907124af6
commit c3863d8ea2
Signed by: DBot
GPG Key ID: DCC23B5715498507
15 changed files with 533 additions and 445 deletions

View File

@ -30,7 +30,7 @@ fun main() {
Starbound.terminateLoading = true
}
var chunkA: Chunk? = null
var chunkA: Chunk<*, *>? = null
val ent = PlayerEntity(client.world!!)
@ -122,12 +122,6 @@ fun main() {
client.camera.pos.y = ent.pos.y.toFloat()
}
client.onPostDrawWorld {
client.gl.quadWireframe {
it.quad(ent.worldaabb)
}
}
ent.spawn()
while (client.renderFrame()) {

View File

@ -0,0 +1,312 @@
package ru.dbotthepony.kstarbound.client
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.render.BakedStaticMesh
import ru.dbotthepony.kstarbound.client.render.EntityRenderer
import ru.dbotthepony.kstarbound.client.render.ILayeredRenderer
import ru.dbotthepony.kstarbound.client.render.TileLayerList
import ru.dbotthepony.kstarbound.math.Matrix4fStack
import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kstarbound.world.entities.Entity
import java.io.Closeable
/**
* Псевдо zPos у фоновых тайлов
*
* Добавление этого числа к zPos гарантирует, что фоновые тайлы будут отрисованы
* первыми (на самом дальнем плане)
*/
const val Z_LEVEL_BACKGROUND = 60000
class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos), Closeable, ILayeredRenderer {
val state: GLStateTracker get() = world.client.gl
private inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable {
private val layers = TileLayerList()
val bakedMeshes = ArrayList<Pair<BakedStaticMesh, Int>>()
private var changeset = -1
fun bake(view: ITileChunk) {
if (state.isSameThread()) {
for (mesh in bakedMeshes) {
mesh.first.close()
}
bakedMeshes.clear()
} else {
for (mesh in bakedMeshes) {
unloadableBakedMeshes.add(mesh.first)
}
bakedMeshes.clear()
}
layers.clear()
for ((pos, tile) in view.posToTile) {
if (tile != null) {
val renderer = state.tileRenderers.get(tile.def.materialName)
renderer.tesselate(view, layers, pos, background = isBackground)
}
}
}
fun loadRenderers(view: ITileChunk) {
for ((_, tile) in view.posToTile) {
if (tile != null) {
state.tileRenderers.get(tile.def.materialName)
}
}
}
fun uploadStatic(clear: Boolean = true) {
for ((baked, builder, zLevel) in layers.buildList()) {
bakedMeshes.add(BakedStaticMesh(baked, builder) to zLevel)
}
if (clear) {
layers.clear()
}
}
fun render(stack: Matrix4fStack) {
val transform = stack.last
for (mesh in bakedMeshes) {
mesh.first.render(transform)
}
}
fun autoBake(provider: () -> ITileChunk) {
if (changeset != layerChangeset.invoke()) {
this.bake(provider.invoke())
changeset = layerChangeset.invoke()
}
}
fun autoUpload() {
if (layers.isNotEmpty) {
for (mesh in bakedMeshes) {
mesh.first.close()
}
bakedMeshes.clear()
for ((baked, builder, zLevel) in layers.buildList()) {
bakedMeshes.add(BakedStaticMesh(baked, builder) to zLevel)
}
layers.clear()
}
}
fun autoBakeAndUpload(provider: () -> ITileChunk) {
autoBake(provider)
autoUpload()
}
fun bakeAndRender(transform: Matrix4fStack, provider: () -> ITileChunk) {
autoBakeAndUpload(provider)
render(transform)
}
override fun close() {
for (mesh in bakedMeshes) {
mesh.first.close()
}
}
}
val debugCollisions get() = world.client.settings.debugCollisions
val posVector2d = Vector2d(x = pos.x * CHUNK_SIZEd, y = pos.y * CHUNK_SIZEd)
private val unloadableBakedMeshes = ArrayList<BakedStaticMesh>()
private val foregroundRenderer = TileLayerRenderer(foreground::changeset, isBackground = false)
private val backgroundRenderer = TileLayerRenderer(background::changeset, isBackground = true)
private fun getForegroundView(): ITileChunk {
return world.getForegroundView(pos)!!
}
private fun getBackgroundView(): ITileChunk {
return world.getBackgroundView(pos)!!
}
/**
* Принудительно подгружает в GLStateTracker все необходимые рендереры (ибо им нужны текстуры и прочее)
*
* Вызывается перед tesselateStatic()
*/
fun loadRenderers() {
unloadUnused()
foregroundRenderer.loadRenderers(getForegroundView())
backgroundRenderer.loadRenderers(getBackgroundView())
}
private fun unloadUnused() {
if (unloadableBakedMeshes.size != 0) {
for (baked in unloadableBakedMeshes) {
baked.close()
}
unloadableBakedMeshes.clear()
}
}
/**
* Отрисовывает всю геометрию напрямую
*/
fun render(stack: Matrix4fStack) {
unloadUnused()
backgroundRenderer.render(stack)
foregroundRenderer.render(stack)
}
/**
* Отрисовывает всю геометрию напрямую, с проверкой, изменился ли чанк
*/
fun bakeAndRender(stack: Matrix4fStack) {
unloadUnused()
backgroundRenderer.bakeAndRender(stack, this::getBackgroundView)
foregroundRenderer.bakeAndRender(stack, this::getForegroundView)
}
/**
* Тесселирует "статичную" геометрию в builders (к примеру тайлы), с проверкой, изменилось ли что либо,
* и загружает её в видеопамять.
*
* Может быть вызван вне рендер потока (ибо в любом случае он требует некой "стаитичности" данных в чанке)
* но только если до этого был вызыван loadRenderers() и геометрия чанка не поменялась
*
*/
fun bake() {
if (state.isSameThread())
unloadUnused()
backgroundRenderer.autoBake(this::getBackgroundView)
foregroundRenderer.autoBake(this::getForegroundView)
if (state.isSameThread())
upload()
}
/**
* Загружает в видеопамять всю геометрию напрямую, если есть что загружать
*/
fun upload() {
unloadUnused()
backgroundRenderer.autoUpload()
foregroundRenderer.autoUpload()
}
fun renderDebug() {
if (debugCollisions) {
state.quadWireframe {
it.quad(aabb.mins.x.toFloat(), aabb.mins.y.toFloat(), aabb.maxs.x.toFloat(), aabb.maxs.y.toFloat())
for (layer in foreground.collisionLayers()) {
it.quad(layer.mins.x.toFloat(), layer.mins.y.toFloat(), layer.maxs.x.toFloat(), layer.maxs.y.toFloat())
}
}
}
for (renderer in entityRenderers.values) {
renderer.renderDebug()
}
}
private val layerQueue = ArrayDeque<Pair<(Matrix4fStack) -> Unit, Int>>()
override fun renderLayerFromStack(zPos: Int, stack: Matrix4fStack): Int {
if (layerQueue.isEmpty())
return -1
stack.push().translateWithScale(x = pos.x * CHUNK_SIZEf, y = pos.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()) {
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().translateWithScale(relative.x.toFloat(), relative.y.toFloat())
renderer.render(it)
it.pop()
return@lambda
} to renderer.layer)
}
layerQueue.sortBy {
return@sortBy it.second
}
}
private val entityRenderers = HashMap<Entity, EntityRenderer>()
override fun onEntityAdded(entity: Entity) {
entityRenderers[entity] = EntityRenderer(state, entity, this)
}
override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) {
val renderer = otherChunk.entityRenderers[entity] ?: throw IllegalStateException("$otherChunk has no renderer for $entity!")
entityRenderers[entity] = renderer
renderer.chunk = this
}
override fun onEntityTransferedFromThis(entity: Entity, otherChunk: ClientChunk) {
entityRenderers.remove(entity)
}
override fun onEntityRemoved(entity: Entity) {
entityRenderers.remove(entity)!!.close()
}
override fun close() {
backgroundRenderer.close()
foregroundRenderer.close()
for (renderer in entityRenderers.values) {
renderer.close()
}
}
}

View File

@ -1,49 +1,14 @@
package ru.dbotthepony.kstarbound.client
import org.lwjgl.glfw.GLFW.glfwGetTime
import ru.dbotthepony.kstarbound.api.IStruct2d
import ru.dbotthepony.kstarbound.api.IStruct2f
import ru.dbotthepony.kstarbound.client.render.ChunkRenderer
import ru.dbotthepony.kstarbound.client.render.renderLayeredList
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.world.*
class ClientWorldChunkTuple(
world: World<*>,
chunk: Chunk,
top: IWorldChunkTuple?,
left: IWorldChunkTuple?,
right: IWorldChunkTuple?,
bottom: IWorldChunkTuple?,
val renderer: ChunkRenderer
) : MutableWorldChunkTuple(
world,
chunk,
top,
left,
right,
bottom,
)
class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWorldChunkTuple>(seed) {
override fun tupleFactory(
chunk: Chunk,
top: IWorldChunkTuple?,
left: IWorldChunkTuple?,
right: IWorldChunkTuple?,
bottom: IWorldChunkTuple?
): ClientWorldChunkTuple {
return ClientWorldChunkTuple(
class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWorld, ClientChunk>(seed) {
override fun chunkFactory(pos: ChunkPos): ClientChunk {
return ClientChunk(
world = this,
chunk = chunk,
top = top,
left = left,
right = right,
bottom = bottom,
renderer = ChunkRenderer(client.gl, chunk, this)
pos = pos,
)
}
@ -58,14 +23,11 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWo
fun render(
size: AABB,
) {
val determineRenderers = ArrayList<ChunkRenderer>()
val determineRenderers = ArrayList<ClientChunk>()
for (chunk in collectInternal(size.encasingChunkPosAABB())) {
determineRenderers.add(chunk.renderer)
}
for (renderer in determineRenderers) {
renderer.autoBakeStatic()
determineRenderers.add(chunk.chunk)
chunk.chunk.bake()
}
renderLayeredList(client.gl.matrixStack, determineRenderers)

View File

@ -198,9 +198,6 @@ class StarboundClient : AutoCloseable {
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
gl.matrixStack.clear(viewportMatrixGame.toMutableMatrix())
val mins = Vector2f((-viewportWidth / 2f) / settings.scale, (-viewportHeight / 2f) / settings.scale)
val maxs = -mins
gl.matrixStack.push()
.translateWithScale(viewportWidth / 2f, viewportHeight / 2f, 2f) // центр экрана + координаты отрисовки мира
.scale(x = settings.scale * PIXELS_IN_STARBOUND_UNITf, y = settings.scale * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера

View File

@ -111,12 +111,12 @@ class GLStateTracker {
var cleanManual = false
val cleanable = cleaner.register(ref) {
if (!cleanManual)
LOGGER.error("{} with ID {} got leaked.", name, nativeRef)
cleanerHits.add {
fn(nativeRef)
checkForGLError()
if (!cleanManual)
LOGGER.error("{} with ID {} got leaked.", name, nativeRef)
}
}

View File

@ -6,6 +6,7 @@ import ru.dbotthepony.kstarbound.client.gl.GLVertexArrayObject
import ru.dbotthepony.kstarbound.client.gl.DynamicVertexBuilder
import ru.dbotthepony.kstarbound.client.gl.checkForGLError
import ru.dbotthepony.kstarbound.math.FloatMatrix
import ru.dbotthepony.kstarbound.math.Matrix4fStack
/**
* Служит для быстрой настройки состояния для будущей отрисовки
@ -83,6 +84,8 @@ class BakedStaticMesh(
checkForGLError()
}
fun renderStacked(transform: Matrix4fStack) = render(transform.last)
var isValid = true
private set

View File

@ -1,287 +0,0 @@
package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.ClientWorld
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.math.FloatMatrix
import ru.dbotthepony.kstarbound.math.Matrix4f
import ru.dbotthepony.kstarbound.math.Matrix4fStack
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.CHUNK_SIZEf
import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ITileChunk
import kotlin.collections.ArrayList
/**
* Псевдо zPos у фоновых тайлов
*
* Добавление этого числа к zPos гарантирует, что фоновые тайлы будут отрисованы
* первыми (на самом дальнем плане)
*/
const val Z_LEVEL_BACKGROUND = 60000
class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: ClientWorld? = null) : AutoCloseable, ILayeredRenderer {
private inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable {
private val layers = TileLayerList()
val bakedMeshes = ArrayList<Pair<BakedStaticMesh, Int>>()
private var changeset = -1
fun tesselateStatic(view: ITileChunk) {
if (state.isSameThread()) {
for (mesh in bakedMeshes) {
mesh.first.close()
}
bakedMeshes.clear()
} else {
for (mesh in bakedMeshes) {
unloadableBakedMeshes.add(mesh.first)
}
bakedMeshes.clear()
}
layers.clear()
for ((pos, tile) in view.posToTile) {
if (tile != null) {
val renderer = state.tileRenderers.get(tile.def.materialName)
renderer.tesselate(view, layers, pos, background = isBackground)
}
}
}
fun loadRenderers(view: ITileChunk) {
for ((_, tile) in view.posToTile) {
if (tile != null) {
state.tileRenderers.get(tile.def.materialName)
}
}
}
fun uploadStatic(clear: Boolean = true) {
for ((baked, builder, zLevel) in layers.buildList()) {
bakedMeshes.add(BakedStaticMesh(baked, builder) to zLevel)
}
if (clear) {
layers.clear()
}
}
fun render(transform: FloatMatrix<*>) {
for (mesh in bakedMeshes) {
mesh.first.render(transform)
}
}
fun bakeAndRender(transform: FloatMatrix<*>, provider: () -> ITileChunk) {
if (changeset != layerChangeset.invoke()) {
this.tesselateStatic(provider.invoke())
this.uploadStatic()
changeset = layerChangeset.invoke()
}
render(transform)
}
fun autoBake(provider: () -> ITileChunk) {
if (changeset != layerChangeset.invoke()) {
this.tesselateStatic(provider.invoke())
this.uploadStatic()
changeset = layerChangeset.invoke()
}
}
fun autoUpload() {
if (layers.isNotEmpty) {
for (mesh in bakedMeshes) {
mesh.first.close()
}
bakedMeshes.clear()
for ((baked, builder, zLevel) in layers.buildList()) {
bakedMeshes.add(BakedStaticMesh(baked, builder) to zLevel)
}
layers.clear()
}
}
override fun close() {
for (mesh in bakedMeshes) {
mesh.first.close()
}
}
}
val debugCollisions get() = world?.client?.settings?.debugCollisions ?: false
val transform = Matrix4f().translate(x = chunk.pos.x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf, y = chunk.pos.x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf)
private val unloadableBakedMeshes = ArrayList<BakedStaticMesh>()
private val foreground = TileLayerRenderer(chunk.foreground::changeset, isBackground = false)
private val background = TileLayerRenderer(chunk.background::changeset, isBackground = true)
private fun getForeground(): ITileChunk {
return world?.getForegroundView(chunk.pos) ?: chunk.foreground
}
private fun getBackground(): ITileChunk {
return world?.getBackgroundView(chunk.pos) ?: chunk.background
}
/**
* Тесселирует "статичную" геометрию в builders (к примеру тайлы).
*
* Может быть вызван вне рендер потока (ибо в любом случае он требует некой "стаитичности" данных в чанке)
* но только если до этого был вызыван loadRenderers() и геометрия чанка не поменялась
*/
fun tesselateStatic() {
foreground.tesselateStatic(getForeground())
background.tesselateStatic(getBackground())
}
/**
* Принудительно подгружает в GLStateTracker все необходимые рендереры (ибо им нужны текстуры и прочее)
*
* Вызывается перед tesselateStatic()
*/
fun loadRenderers() {
unloadUnused()
foreground.loadRenderers(getForeground())
background.loadRenderers(getBackground())
}
private fun unloadUnused() {
if (unloadableBakedMeshes.size != 0) {
for (baked in unloadableBakedMeshes) {
baked.close()
}
unloadableBakedMeshes.clear()
}
}
fun uploadStatic(clear: Boolean = true) {
unloadUnused()
foreground.uploadStatic(clear)
background.uploadStatic(clear)
}
/**
* Отрисовывает всю геометрию напрямую
*/
fun render(transform: FloatMatrix<*> = state.matrixStack.last) {
unloadUnused()
background.render(transform)
foreground.render(transform)
}
/**
* Отрисовывает всю геометрию напрямую, с проверкой, изменился ли чанк
*/
fun bakeAndRender(transform: FloatMatrix<*> = state.matrixStack.last) {
unloadUnused()
background.bakeAndRender(transform, this::getBackground)
foreground.bakeAndRender(transform, this::getForeground)
}
/**
* Запекает всю геометрию напрямую, с проверкой, изменился ли чанк,
* и загружает её, если вызвано в рендер потоке
*/
fun autoBakeStatic() {
if (state.isSameThread())
unloadUnused()
background.autoBake(this::getBackground)
foreground.autoBake(this::getForeground)
if (state.isSameThread())
autoUploadStatic()
}
/**
* Загружает в видеопамять всю геометрию напрямую, если есть что загружать
*/
fun autoUploadStatic() {
unloadUnused()
background.autoUpload()
foreground.autoUpload()
}
fun renderDebug() {
if (debugCollisions) {
state.quadWireframe {
it.quad(chunk.aabb.mins.x.toFloat(), chunk.aabb.mins.y.toFloat(), chunk.aabb.maxs.x.toFloat(), chunk.aabb.maxs.y.toFloat())
for (layer in chunk.foreground.collisionLayers()) {
it.quad(layer.mins.x.toFloat(), layer.mins.y.toFloat(), layer.maxs.x.toFloat(), layer.maxs.y.toFloat())
}
}
}
}
private val meshDeque = ArrayDeque<Pair<BakedStaticMesh, Int>>()
override fun renderLayerFromStack(zPos: Int, transform: Matrix4fStack): Int {
if (meshDeque.isEmpty())
return -1
transform.push().translateWithScale(x = chunk.pos.x * CHUNK_SIZEf, y = chunk.pos.y * CHUNK_SIZEf)
var pair = meshDeque.last()
while (pair.second >= zPos) {
pair.first.render(transform.last)
meshDeque.removeLast()
if (meshDeque.isEmpty()) {
transform.pop()
return -1
}
pair = meshDeque.last()
}
transform.pop()
return meshDeque.last().second
}
override fun bottomMostZLevel(): Int {
if (meshDeque.isEmpty()) {
return -1
}
return meshDeque.last().second
}
override fun prepareForLayeredRender() {
meshDeque.clear()
for ((baked, zLevel) in background.bakedMeshes) {
meshDeque.add(baked to (zLevel + Z_LEVEL_BACKGROUND))
}
meshDeque.addAll(foreground.bakedMeshes)
meshDeque.sortBy {
return@sortBy it.second
}
}
override fun close() {
background.close()
foreground.close()
}
}

View File

@ -0,0 +1,33 @@
package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kstarbound.client.ClientChunk
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.math.Matrix4fStack
import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.world.entities.Entity
import java.io.Closeable
/**
* Базовый класс, отвечающий за отрисовку определённого ентити в мире
*
* Считается, что процесс отрисовки ограничен лишь одним слоем (т.е. отрисовка происходит в один проход)
*/
open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open var chunk: ClientChunk?) : Closeable {
open val renderPos: Vector2d get() = entity.pos
open fun render(stack: Matrix4fStack) {
}
open fun renderDebug() {
if (chunk?.world?.client?.settings?.debugCollisions == true) {
state.quadWireframe(entity.worldaabb)
}
}
open val layer: Int = 100
override fun close() {
}
}

View File

@ -1,7 +1,5 @@
package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kstarbound.math.FloatMatrix
import ru.dbotthepony.kstarbound.math.Matrix4f
import ru.dbotthepony.kstarbound.math.Matrix4fStack
/**
@ -24,7 +22,7 @@ interface ILayeredRenderer {
* Если следующего слоя нет, вернуть -1, и данный объект
* будет считаться отрисованным.
*/
fun renderLayerFromStack(zPos: Int, transform: Matrix4fStack): Int
fun renderLayerFromStack(zPos: Int, stack: Matrix4fStack): Int
/**
* Возвращает наибольшее zPos в данной стопке.

View File

@ -342,6 +342,7 @@ abstract class IVector2d<T : IVector2d<T>> : IMatrixLike, IMatrixLikeDouble, ISt
}
protected abstract fun make(x: Double, y: Double): T
fun toFloatVector(): Vector2f = Vector2f(x.toFloat(), y.toFloat())
}
data class Vector2d(override val x: Double = 0.0, override val y: Double = 0.0) : IVector2d<Vector2d>() {

View File

@ -4,14 +4,16 @@ import ru.dbotthepony.kstarbound.api.IStruct2d
import ru.dbotthepony.kstarbound.api.IStruct2i
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.world.entities.Entity
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
import kotlin.math.absoluteValue
/**
* Представляет из себя класс, который содержит состояние тайла на заданной позиции
*/
data class ChunkTile(val chunk: Chunk.TileLayer, val def: TileDefinition) {
data class ChunkTile(val chunk: Chunk<*, *>.TileLayer, val def: TileDefinition) {
var color = 0
set(value) {
field = value
@ -322,7 +324,7 @@ class MutableTileChunkView(
*
* Весь игровой мир будет измеряться в Starbound Unit'ах
*/
open class Chunk(val world: World<*>?, val pos: ChunkPos) {
abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, This>>(val world: WorldType, val pos: ChunkPos) {
/**
* Возвращает счётчик изменений чанка
*/
@ -355,8 +357,6 @@ open class Chunk(val world: World<*>?, val pos: ChunkPos) {
// 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
@ -448,6 +448,49 @@ open class Chunk(val world: World<*>?, val pos: ChunkPos) {
val foreground = TileLayer()
val background = TileLayer()
protected val entities = HashSet<Entity>()
val entitiesAccess = Collections.unmodifiableSet(entities)
protected abstract fun onEntityAdded(entity: Entity)
protected abstract fun onEntityTransferedToThis(entity: Entity, otherChunk: This)
protected abstract fun onEntityTransferedFromThis(entity: Entity, otherChunk: This)
protected abstract fun onEntityRemoved(entity: Entity)
fun addEntity(entity: Entity) {
if (!entities.add(entity)) {
throw IllegalArgumentException("Already having having entity $entity")
}
onEntityAdded(entity)
}
fun transferEntity(entity: Entity, otherChunk: Chunk<*, *>) {
if (otherChunk == this)
throw IllegalArgumentException("what?")
if (this::class.java != otherChunk::class.java) {
throw IllegalArgumentException("Incompatible types: $this !is $otherChunk")
}
if (!entities.add(entity)) {
throw IllegalArgumentException("Already containing $entity")
}
onEntityTransferedToThis(entity, otherChunk as This)
otherChunk.onEntityTransferedFromThis(entity, this as This)
if (!otherChunk.entities.remove(entity)) {
throw IllegalStateException("Unable to remove $entity from $otherChunk after transfer")
}
}
fun removeEntity(entity: Entity) {
if (!entities.remove(entity)) {
throw IllegalArgumentException("Already not having entity $entity")
}
onEntityRemoved(entity)
}
companion object {
val EMPTY = object : IMutableTileChunk {

View File

@ -8,41 +8,78 @@ import ru.dbotthepony.kstarbound.world.entities.Entity
import kotlin.math.absoluteValue
/**
* Возвращает кортеж чанка, который содержит родителя (мир) и соседей (кортежи чанков)
* Кортеж чанка, который содержит родителя (мир) и соседей (кортежи чанков)
*/
interface IWorldChunkTuple {
val world: World<*>
val chunk: Chunk
val top: IWorldChunkTuple?
val left: IWorldChunkTuple?
val right: IWorldChunkTuple?
val bottom: IWorldChunkTuple?
interface IWorldChunkTuple<WorldType : World<WorldType, ChunkType>, ChunkType : Chunk<WorldType, ChunkType>> {
val world: WorldType
val chunk: ChunkType
val top: IWorldChunkTuple<WorldType, ChunkType>?
val left: IWorldChunkTuple<WorldType, ChunkType>?
val right: IWorldChunkTuple<WorldType, ChunkType>?
val bottom: IWorldChunkTuple<WorldType, ChunkType>?
}
interface IMutableWorldChunkTuple : IWorldChunkTuple {
override var top: IWorldChunkTuple?
override var left: IWorldChunkTuple?
override var right: IWorldChunkTuple?
override var bottom: IWorldChunkTuple?
interface IMutableWorldChunkTuple<WorldType : World<WorldType, ChunkType>, ChunkType : Chunk<WorldType, ChunkType>> : IWorldChunkTuple<WorldType, ChunkType> {
override var top: IMutableWorldChunkTuple<WorldType, ChunkType>?
override var left: IMutableWorldChunkTuple<WorldType, ChunkType>?
override var right: IMutableWorldChunkTuple<WorldType, ChunkType>?
override var bottom: IMutableWorldChunkTuple<WorldType, ChunkType>?
}
data class WorldChunkTuple(
override val world: World<*>,
override val chunk: Chunk,
override val top: IWorldChunkTuple?,
override val left: IWorldChunkTuple?,
override val right: IWorldChunkTuple?,
override val bottom: IWorldChunkTuple?,
) : IWorldChunkTuple
class WorldChunkTuple<WorldType : World<WorldType, ChunkType>, ChunkType : Chunk<WorldType, ChunkType>>(
private val parent: IWorldChunkTuple<WorldType, ChunkType>
) : IWorldChunkTuple<WorldType, ChunkType> {
override val world get() = parent.world
override val chunk get() = parent.chunk
override val top: IWorldChunkTuple<WorldType, ChunkType>? get() {
val getValue = parent.top
open class MutableWorldChunkTuple(
override val world: World<*>,
override val chunk: Chunk,
override var top: IWorldChunkTuple?,
override var left: IWorldChunkTuple?,
override var right: IWorldChunkTuple?,
override var bottom: IWorldChunkTuple?,
) : IMutableWorldChunkTuple
if (getValue != null) {
return WorldChunkTuple(getValue)
}
return null
}
override val left: IWorldChunkTuple<WorldType, ChunkType>? get() {
val getValue = parent.left
if (getValue != null) {
return WorldChunkTuple(getValue)
}
return null
}
override val right: IWorldChunkTuple<WorldType, ChunkType>? get() {
val getValue = parent.right
if (getValue != null) {
return WorldChunkTuple(getValue)
}
return null
}
override val bottom: IWorldChunkTuple<WorldType, ChunkType>? get() {
val getValue = parent.bottom
if (getValue != null) {
return WorldChunkTuple(getValue)
}
return null
}
}
open class MutableWorldChunkTuple<WorldType : World<WorldType, ChunkType>, ChunkType : Chunk<WorldType, ChunkType>>(
override val world: WorldType,
override val chunk: ChunkType,
override var top: IMutableWorldChunkTuple<WorldType, ChunkType>?,
override var left: IMutableWorldChunkTuple<WorldType, ChunkType>?,
override var right: IMutableWorldChunkTuple<WorldType, ChunkType>?,
override var bottom: IMutableWorldChunkTuple<WorldType, ChunkType>?,
) : IMutableWorldChunkTuple<WorldType, ChunkType>
const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT
@ -55,9 +92,9 @@ data class WorldSweepResult(
private const val EPSILON = 0.00001
abstract class World<T : IMutableWorldChunkTuple>(val seed: Long = 0L) {
protected val chunkMap = HashMap<ChunkPos, T>()
protected var lastAccessedChunk: T? = null
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val seed: Long = 0L) {
protected val chunkMap = HashMap<ChunkPos, IMutableWorldChunkTuple<This, ChunkType>>()
protected var lastAccessedChunk: IMutableWorldChunkTuple<This, ChunkType>? = null
/**
* Таймер этого мира, в секундах.
@ -106,15 +143,11 @@ abstract class World<T : IMutableWorldChunkTuple>(val seed: Long = 0L) {
*/
var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION)
protected abstract fun tupleFactory(
chunk: Chunk,
top: IWorldChunkTuple?,
left: IWorldChunkTuple?,
right: IWorldChunkTuple?,
bottom: IWorldChunkTuple?,
): T
protected abstract fun chunkFactory(
pos: ChunkPos,
): ChunkType
protected fun getChunkInternal(pos: ChunkPos): T? {
protected fun getChunkInternal(pos: ChunkPos): IMutableWorldChunkTuple<This, ChunkType>? {
if (lastAccessedChunk?.chunk?.pos == pos) {
return lastAccessedChunk
}
@ -122,36 +155,30 @@ abstract class World<T : IMutableWorldChunkTuple>(val seed: Long = 0L) {
return chunkMap[pos]
}
open fun getChunk(pos: ChunkPos): IWorldChunkTuple? {
open fun getChunk(pos: ChunkPos): IWorldChunkTuple<This, ChunkType>? {
val getTuple = getChunkInternal(pos)
if (getTuple != null)
return WorldChunkTuple(
world = getTuple.world,
chunk = getTuple.chunk,
top = getTuple.top,
left = getTuple.left,
right = getTuple.right,
bottom = getTuple.bottom,
)
return WorldChunkTuple(getTuple)
return null
}
protected open fun computeIfAbsentInternal(pos: ChunkPos): T {
protected open fun computeIfAbsentInternal(pos: ChunkPos): IWorldChunkTuple<This, ChunkType> {
if (lastAccessedChunk?.chunk?.pos == pos) {
return lastAccessedChunk!!
}
return chunkMap.computeIfAbsent(pos) lazy@{
val chunk = Chunk(this, pos)
val chunk = chunkFactory(pos)
val top = getChunkInternal(pos.up())
val left = getChunkInternal(pos.left())
val right = getChunkInternal(pos.right())
val bottom = getChunkInternal(pos.down())
val tuple = tupleFactory(
val tuple = MutableWorldChunkTuple(
world = this as This,
chunk = chunk,
top = top,
left = left,
@ -184,17 +211,8 @@ abstract class World<T : IMutableWorldChunkTuple>(val seed: Long = 0L) {
}
}
open fun computeIfAbsent(pos: ChunkPos): IWorldChunkTuple {
val getTuple = computeIfAbsentInternal(pos)
return WorldChunkTuple(
world = getTuple.world,
chunk = getTuple.chunk,
top = getTuple.top,
left = getTuple.left,
right = getTuple.right,
bottom = getTuple.bottom,
)
open fun computeIfAbsent(pos: ChunkPos): IWorldChunkTuple<This, ChunkType> {
return WorldChunkTuple(computeIfAbsentInternal(pos))
}
open fun getForegroundView(pos: ChunkPos): TileChunkView? {
@ -233,7 +251,7 @@ abstract class World<T : IMutableWorldChunkTuple>(val seed: Long = 0L) {
return getChunkInternal(ChunkPos.fromTilePosition(pos))?.chunk?.foreground?.get(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y))
}
fun setTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple {
fun setTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple<This, ChunkType> {
val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos))
chunk.chunk.foreground[ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)] = tile
return chunk
@ -243,14 +261,14 @@ abstract class World<T : IMutableWorldChunkTuple>(val seed: Long = 0L) {
return getChunkInternal(ChunkPos.fromTilePosition(pos))?.chunk?.background?.get(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y))
}
fun setBackgroundTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple {
fun setBackgroundTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple<This, ChunkType> {
val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos))
chunk.chunk.background[ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)] = tile
return chunk
}
protected open fun collectInternal(boundingBox: AABBi): List<T> {
val output = ArrayList<T>()
protected open fun collectInternal(boundingBox: AABBi): List<IMutableWorldChunkTuple<This, ChunkType>> {
val output = ArrayList<IMutableWorldChunkTuple<This, ChunkType>>()
for (pos in boundingBox.chunkPositions) {
val chunk = getChunkInternal(pos)
@ -266,18 +284,11 @@ abstract class World<T : IMutableWorldChunkTuple>(val seed: Long = 0L) {
/**
* Возвращает все чанки, которые пересекаются с заданным [boundingBox]
*/
open fun collect(boundingBox: AABBi): List<IWorldChunkTuple> {
val output = ArrayList<IWorldChunkTuple>()
open fun collect(boundingBox: AABBi): List<IWorldChunkTuple<This, ChunkType>> {
val output = ArrayList<IWorldChunkTuple<This, ChunkType>>()
for (chunk in collectInternal(boundingBox)) {
output.add(WorldChunkTuple(
world = chunk.world,
chunk = chunk.chunk,
top = chunk.top,
left = chunk.left,
right = chunk.right,
bottom = chunk.bottom,
))
output.add(WorldChunkTuple(chunk))
}
return output

View File

@ -12,7 +12,7 @@ enum class Move {
MOVE_RIGHT
}
open class AliveEntity(world: World<*>) : Entity(world) {
open class AliveEntity(world: World<*, *>) : Entity(world) {
open var maxHealth = 10.0
open var health = 10.0
open val moveDirection = Move.STAND_STILL
@ -20,12 +20,12 @@ open class AliveEntity(world: World<*>) : Entity(world) {
open val aabbDucked get() = aabb
override val worldaabb: AABB get() {
override val currentaabb: AABB get() {
if (isDucked) {
return aabbDucked + pos
return aabbDucked
}
return super.worldaabb
return super.currentaabb
}
var wantsToDuck = false

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.world.entities
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.client.render.EntityRenderer
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.math.lerp
@ -18,10 +19,10 @@ enum class CollisionResolution {
/**
* Определяет из себя сущность в мире, которая имеет позицию, скорость и коробку столкновений
*/
open class Entity(val world: World<*>) {
var chunk: Chunk? = null
open class Entity(val world: World<*, *>) {
var chunk: Chunk<*, *>? = null
set(value) {
if (!spawned) {
if (!isSpawned) {
throw IllegalStateException("Trying to set chunk this entity belong to before spawning in world")
}
@ -35,17 +36,22 @@ open class Entity(val world: World<*>) {
throw IllegalStateException("Set proper position before setting chunk this Entity belongs to")
}
val oldChunk = chunk
val oldChunk = field
field = value
if (oldChunk == null && value != null) {
world.orphanedEntities.remove(this)
value.addEntity(this)
} else if (oldChunk != null && value == null) {
world.orphanedEntities.add(this)
oldChunk.removeEntity(this)
} else if (oldChunk != null && value != null) {
value.transferEntity(this, oldChunk)
}
}
open val worldaabb: AABB get() = aabb + pos
open val currentaabb: AABB get() = aabb
open val worldaabb: AABB get() = currentaabb + pos
var pos = Vector2d()
set(value) {
@ -55,7 +61,7 @@ open class Entity(val world: World<*>) {
val old = field
field = value
if (spawned) {
if (isSpawned) {
val oldChunkPos = ChunkPos.fromTilePosition(old)
val newChunkPos = ChunkPos.fromTilePosition(value)
@ -66,21 +72,36 @@ open class Entity(val world: World<*>) {
}
var velocity = Vector2d()
private var spawned = false
var isSpawned = false
private set
var isRemoved = false
private set
fun spawn() {
if (spawned)
if (isSpawned)
throw IllegalStateException("Already spawned")
spawned = true
isSpawned = true
world.entities.add(this)
chunk = world.getChunk(ChunkPos.ZERO)?.chunk
chunk = world.getChunk(ChunkPos.fromTilePosition(pos))?.chunk
if (chunk == null) {
world.orphanedEntities.add(this)
}
}
fun remove() {
if (isRemoved)
throw IllegalStateException("Already removed")
isRemoved = true
if (isSpawned) {
world.entities.remove(this)
chunk?.removeEntity(this)
}
}
/**
* Касается ли сущность земли
*
@ -170,7 +191,7 @@ open class Entity(val world: World<*>) {
* Заставляет сущность "думать".
*/
fun think(delta: Double) {
if (!spawned) {
if (!isSpawned) {
throw IllegalStateException("Tried to think before spawning in world")
}

View File

@ -7,7 +7,7 @@ import ru.dbotthepony.kstarbound.world.World
/**
* Физический аватар игрока в мире
*/
open class PlayerEntity(world: World<*>) : AliveEntity(world) {
open class PlayerEntity(world: World<*, *>) : AliveEntity(world) {
override val aabb = AABB.rectangle(Vector2d.ZERO, 1.8, 3.7)
override val aabbDucked: AABB = AABB.rectangle(Vector2d.ZERO, 1.8, 1.8) + Vector2d(y = -0.9)
override var moveDirection = Move.STAND_STILL