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

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 Starbound.terminateLoading = true
} }
var chunkA: Chunk? = null var chunkA: Chunk<*, *>? = null
val ent = PlayerEntity(client.world!!) val ent = PlayerEntity(client.world!!)
@ -122,12 +122,6 @@ fun main() {
client.camera.pos.y = ent.pos.y.toFloat() client.camera.pos.y = ent.pos.y.toFloat()
} }
client.onPostDrawWorld {
client.gl.quadWireframe {
it.quad(ent.worldaabb)
}
}
ent.spawn() ent.spawn()
while (client.renderFrame()) { 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 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.client.render.renderLayeredList
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.world.* import ru.dbotthepony.kstarbound.world.*
class ClientWorldChunkTuple( class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWorld, ClientChunk>(seed) {
world: World<*>, override fun chunkFactory(pos: ChunkPos): ClientChunk {
chunk: Chunk, return ClientChunk(
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(
world = this, world = this,
chunk = chunk, pos = pos,
top = top,
left = left,
right = right,
bottom = bottom,
renderer = ChunkRenderer(client.gl, chunk, this)
) )
} }
@ -58,14 +23,11 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWo
fun render( fun render(
size: AABB, size: AABB,
) { ) {
val determineRenderers = ArrayList<ChunkRenderer>() val determineRenderers = ArrayList<ClientChunk>()
for (chunk in collectInternal(size.encasingChunkPosAABB())) { for (chunk in collectInternal(size.encasingChunkPosAABB())) {
determineRenderers.add(chunk.renderer) determineRenderers.add(chunk.chunk)
} chunk.chunk.bake()
for (renderer in determineRenderers) {
renderer.autoBakeStatic()
} }
renderLayeredList(client.gl.matrixStack, determineRenderers) renderLayeredList(client.gl.matrixStack, determineRenderers)

View File

@ -198,9 +198,6 @@ class StarboundClient : AutoCloseable {
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
gl.matrixStack.clear(viewportMatrixGame.toMutableMatrix()) gl.matrixStack.clear(viewportMatrixGame.toMutableMatrix())
val mins = Vector2f((-viewportWidth / 2f) / settings.scale, (-viewportHeight / 2f) / settings.scale)
val maxs = -mins
gl.matrixStack.push() gl.matrixStack.push()
.translateWithScale(viewportWidth / 2f, viewportHeight / 2f, 2f) // центр экрана + координаты отрисовки мира .translateWithScale(viewportWidth / 2f, viewportHeight / 2f, 2f) // центр экрана + координаты отрисовки мира
.scale(x = settings.scale * PIXELS_IN_STARBOUND_UNITf, y = settings.scale * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера .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 var cleanManual = false
val cleanable = cleaner.register(ref) { val cleanable = cleaner.register(ref) {
if (!cleanManual)
LOGGER.error("{} with ID {} got leaked.", name, nativeRef)
cleanerHits.add { cleanerHits.add {
fn(nativeRef) fn(nativeRef)
checkForGLError() 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.DynamicVertexBuilder
import ru.dbotthepony.kstarbound.client.gl.checkForGLError import ru.dbotthepony.kstarbound.client.gl.checkForGLError
import ru.dbotthepony.kstarbound.math.FloatMatrix import ru.dbotthepony.kstarbound.math.FloatMatrix
import ru.dbotthepony.kstarbound.math.Matrix4fStack
/** /**
* Служит для быстрой настройки состояния для будущей отрисовки * Служит для быстрой настройки состояния для будущей отрисовки
@ -83,6 +84,8 @@ class BakedStaticMesh(
checkForGLError() checkForGLError()
} }
fun renderStacked(transform: Matrix4fStack) = render(transform.last)
var isValid = true var isValid = true
private set 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 package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kstarbound.math.FloatMatrix
import ru.dbotthepony.kstarbound.math.Matrix4f
import ru.dbotthepony.kstarbound.math.Matrix4fStack import ru.dbotthepony.kstarbound.math.Matrix4fStack
/** /**
@ -24,7 +22,7 @@ interface ILayeredRenderer {
* Если следующего слоя нет, вернуть -1, и данный объект * Если следующего слоя нет, вернуть -1, и данный объект
* будет считаться отрисованным. * будет считаться отрисованным.
*/ */
fun renderLayerFromStack(zPos: Int, transform: Matrix4fStack): Int fun renderLayerFromStack(zPos: Int, stack: Matrix4fStack): Int
/** /**
* Возвращает наибольшее zPos в данной стопке. * Возвращает наибольшее 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 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>() { 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.api.IStruct2i
import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.world.entities.Entity
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashSet
import kotlin.math.absoluteValue 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 var color = 0
set(value) { set(value) {
field = value field = value
@ -322,7 +324,7 @@ class MutableTileChunkView(
* *
* Весь игровой мир будет измеряться в Starbound Unit'ах * Весь игровой мир будет измеряться в 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-дерево_(структураанных) // TODO: https://ru.wikipedia.org/wiki/R-дерево_(структураанных)
private fun bakeCollisions() { private fun bakeCollisions() {
collisionChangeset = changeset collisionChangeset = changeset
val seen = BooleanArray(tiles.size)
collisionCache.clear() collisionCache.clear()
val xAdd = pos.x * CHUNK_SIZEd val xAdd = pos.x * CHUNK_SIZEd
@ -448,6 +448,49 @@ open class Chunk(val world: World<*>?, val pos: ChunkPos) {
val foreground = TileLayer() val foreground = TileLayer()
val background = 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 { companion object {
val EMPTY = object : IMutableTileChunk { val EMPTY = object : IMutableTileChunk {

View File

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

View File

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

View File

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