diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 71ff8ef0..dfc934d9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -1,6 +1,8 @@ package ru.dbotthepony.kstarbound +import com.google.common.collect.ImmutableList +import com.google.gson.reflect.TypeToken import org.apache.logging.log4j.LogManager import org.lwjgl.Version import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose @@ -13,20 +15,21 @@ import ru.dbotthepony.kstarbound.player.QuestInstance import ru.dbotthepony.kstarbound.util.JVMTimeSource import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kstarbound.world.entities.ItemEntity -import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import ru.dbotthepony.kstarbound.io.json.VersionedJson import ru.dbotthepony.kstarbound.io.readVarInt import ru.dbotthepony.kstarbound.util.AssetPathStack -import ru.dbotthepony.kstarbound.world.entities.Move import ru.dbotthepony.kstarbound.world.entities.WorldObject +import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kvector.vector.Vector2d import java.io.BufferedInputStream import java.io.ByteArrayInputStream import java.io.DataInputStream import java.io.File import java.util.* +import java.util.concurrent.TimeUnit import java.util.zip.Inflater import java.util.zip.InflaterInputStream +import kotlin.collections.ArrayList private val LOGGER = LogManager.getLogger() @@ -64,8 +67,6 @@ fun main() { Starbound.terminateLoading = true } - val ent = PlayerEntity(client.world!!) - Starbound.onInitialize { //for (chunkX in 17 .. 18) { //for (chunkX in 14 .. 24) { @@ -124,10 +125,10 @@ fun main() { for (i in 0 .. 0) { val item = ItemEntity(client.world!!, item.value) - item.position = Vector2d(7.0 + i, 685.0) + item.position = Vector2d(225.0 + i, 685.0) item.spawn() + item.velocity = Vector2d(0.01, -0.08) //item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0)) - item.movement.applyVelocity(Vector2d(-1.0, 0.0)) } // println(Starbound.statusEffects["firecharge"]) @@ -154,21 +155,55 @@ fun main() { } //ent.position += Vector2d(y = 14.0, x = -10.0) - ent.position = Vector2d(238.0, 685.0) client.camera.pos = Vector2d(238.0, 685.0) //client.camera.pos = Vector2f(0f, 0f) client.onDrawGUI { - client.font.render("${ent.position}", y = 100f, scale = 0.25f) - client.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f) client.font.render("Camera: ${client.camera.pos} ${client.settings.zoom}", y = 140f, scale = 0.25f) client.font.render("Cursor: ${client.mouseCoordinates} -> ${client.screenToWorld(client.mouseCoordinates)}", y = 160f, scale = 0.25f) client.font.render("World chunk: ${client.world!!.chunkFromCell(client.camera.pos)}", y = 180f, scale = 0.25f) } - client.onPreDrawWorld { + var p = Poly(Starbound.gson.fromJson>("""[ [1.75, 2.55], [2.25, 2.05], [2.75, -3.55], [2.25, -3.95], [-2.25, -3.95], [-2.75, -3.55], [-2.25, 2.05], [-1.75, 2.55] ]""", TypeToken.getParameterized(ImmutableList::class.java, Vector2d::class.java).type)) + val polies = ArrayList() + + val box = Poly(listOf(Vector2d(-0.5, -0.5), Vector2d(-0.5, 0.5), Vector2d(0.5, 0.5), Vector2d(0.5, -0.5))) + p = box + val triangle = Poly(listOf(Vector2d(-0.5, -0.5), Vector2d(0.5, 0.5), Vector2d(0.5, -0.5))) + + //polies.add(triangle + Vector2d(0.0, -1.5)) + polies.add(box) + polies.add(box + Vector2d(1.0)) + //polies.add(box + Vector2d(1.0, -1.0)) + //polies.add(box + Vector2d(1.0, -2.0)) + //polies.add(box + Vector2d(1.0, -3.0)) + + //client.foregroundExecutor.scheduleAtFixedRate({ p.intersect(p2)?.let { p += it; println(it) } }, 1, 1, TimeUnit.SECONDS) + + p += client.camera.pos + + for (i in polies.indices) + polies[i] = polies[i] + client.camera.pos + + client.onPostDrawWorld { //client.camera.pos.x = ent.pos.x.toFloat() //client.camera.pos.y = ent.pos.y.toFloat() + + p += client.screenToWorld(client.mouseCoordinates) - p.aabb.centre + + for (i in 0 until 10) { + val intersects = ArrayList() + + polies.forEach { p.intersect(it)?.let { intersects.add(it) } } + + if (intersects.isEmpty()) + break + else + p += intersects.max() + } + + p.render() + polies.forEach { it.render() } } client.box2dRenderer.drawShapes = false @@ -200,23 +235,6 @@ fun main() { //println(client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1) - //if (ent.onGround) - //ent.velocity += client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1 - - if (client.input.KEY_LEFT_DOWN) { - ent.movement.moveDirection = Move.MOVE_LEFT - } else if (client.input.KEY_RIGHT_DOWN) { - ent.movement.moveDirection = Move.MOVE_RIGHT - } else { - ent.movement.moveDirection = Move.STAND_STILL - } - - if (client.input.KEY_SPACE_PRESSED && ent.movement.onGround) { - ent.movement.requestJump() - } else if (client.input.KEY_SPACE_RELEASED) { - ent.movement.recallJump() - } - if (client.input.KEY_ESCAPE_PRESSED) { glfwSetWindowShouldClose(client.window, true) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 4c1f6b4d..eb06a766 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -74,7 +74,6 @@ import java.nio.ByteBuffer import java.nio.ByteOrder import java.time.Duration import java.util.* -import java.util.concurrent.CompletableFuture import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinWorkerThread import java.util.concurrent.atomic.AtomicInteger @@ -326,23 +325,23 @@ class StarboundClient : Closeable { private fun executeQueuedTasks() { foregroundExecutor.executeQueuedTasks() - var next3 = openglCleanQueue.poll() as CleanRef? + var next = openglCleanQueue.poll() as CleanRef? - while (next3 != null) { + while (next != null) { openglObjectsCleaned++ - next3.fn.accept(next3.value) + next.fn.accept(next.value) checkForGLError("Removing unreachable OpenGL object") val head = openglCleanQueueHead - if (next3 === head) { + if (next === head) { openglCleanQueueHead = head.next } else { - next3.prev?.next = next3.next - next3.next?.prev = next3.prev + next.prev?.next = next.next + next.next?.prev = next.prev } - next3 = openglCleanQueue.poll() as CleanRef? + next = openglCleanQueue.poll() as CleanRef? } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt index c0de4a3f..ce1ba841 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt @@ -5,6 +5,7 @@ import org.lwjgl.opengl.GL45.GL_UNSIGNED_INT import org.lwjgl.opengl.GL45.GL_UNSIGNED_SHORT import org.lwjgl.opengl.GL45.GL_UNSIGNED_BYTE import ru.dbotthepony.kstarbound.client.gl.BufferObject +import ru.dbotthepony.kvector.api.IStruct2d import ru.dbotthepony.kvector.api.IStruct2f import ru.dbotthepony.kvector.api.IStruct4f import ru.dbotthepony.kvector.arrays.Matrix3f @@ -279,12 +280,36 @@ class VertexBuilder( return this } + fun vertex(value: IStruct2f): VertexBuilder { + vertex() + position(value.component1(), value.component2()) + return this + } + + fun vertex(value: IStruct2d): VertexBuilder { + vertex() + position(value.component1().toFloat(), value.component2().toFloat()) + return this + } + fun vertex(transform: Matrix3f, x: Float, y: Float): VertexBuilder { vertex() position(transform, x, y) return this } + fun vertex(transform: Matrix3f, value: IStruct2f): VertexBuilder { + vertex() + position(transform, value) + return this + } + + fun vertex(transform: Matrix3f, value: IStruct2d): VertexBuilder { + vertex() + position(transform, value) + return this + } + private fun pushFloat(attr: VertexAttributes.Attribute, x: Float): VertexBuilder { vertexMemory.position(vertexCount * attributes.vertexStride + attr.relativeOffset) vertexMemory.putFloat(x) @@ -352,6 +377,7 @@ class VertexBuilder( } fun position(transform: Matrix3f, value: IStruct2f) = position(transform, value.component1(), value.component2()) + fun position(transform: Matrix3f, value: IStruct2d) = position(transform, value.component1().toFloat(), value.component2().toFloat()) fun uv(x: Float, y: Float): VertexBuilder { return pushFloat2(requireNotNull(uv) { "Vertex format does not have texture UV attribute" }, x, y) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt index cc6519ae..76ce24cc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt @@ -56,8 +56,8 @@ class Box2DRenderer : IDebugDraw { for (i in vertices.indices) { val current = vertices[i] val next = vertices[(i + 1) % vertices.size] - lines.builder.vertex(state.stack.last(), current.x.toFloat(), current.y.toFloat()).color(color) - lines.builder.vertex(state.stack.last(), next.x.toFloat(), next.y.toFloat()).color(color) + lines.builder.vertex(state.stack.last(), current).color(color) + lines.builder.vertex(state.stack.last(), next).color(color) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt index dbc3e84c..ab025bfb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt @@ -7,8 +7,16 @@ import ru.dbotthepony.kvector.arrays.Matrix4fStack class ItemRenderer(client: StarboundClient, entity: ItemEntity, chunk: ClientChunk?) : EntityRenderer(client, entity, chunk) { private val def = entity.def + private val textures = def.inventoryIcon?.stream()?.map { it.image }?.toList() ?: listOf() override fun render(stack: Matrix4fStack) { + //client.programs.positionTexture.use() + //client.programs.positionTexture.texture0 = 0 + //for (texture in textures) { + // client.textures2D[0] = texture.image?.texture ?: continue + //} + + entity.hitboxes.forEach { it.render(client) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt index e2cfa54b..055b6e1a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt @@ -29,24 +29,4 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk() - - override fun onEntityAdded(entity: Entity) { - entityRenderers[entity] = EntityRenderer.getRender(world.client, 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) - } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index f6a512ac..0e35ee90 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -311,6 +311,14 @@ class ClientWorld( obj.addLights(client.viewportLighting, client.viewportCellX, client.viewportCellY) } } + + for (ent in entities) { + if (ent.position.x.toInt() in client.viewportCellX .. client.viewportCellX + client.viewportCellWidth && ent.position.y.toInt() in client.viewportCellY .. client.viewportCellY + client.viewportCellHeight) { + layers.add(RenderLayer.Overlay.point()) { + ent.render(client) + } + } + } } override fun thinkInner() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 3df15be3..b95e8dd2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -324,10 +324,10 @@ abstract class Chunk, This : Chunk() - 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) + protected open fun onEntityAdded(entity: Entity) { } + protected open fun onEntityTransferedToThis(entity: Entity, otherChunk: This) { } + protected open fun onEntityTransferedFromThis(entity: Entity, otherChunk: This) { } + protected open fun onEntityRemoved(entity: Entity) { } fun addEntity(entity: Entity) { if (!entities.add(entity)) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/IChunkCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/IChunkCell.kt index b7ca691f..71d7a6af 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/IChunkCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/IChunkCell.kt @@ -4,9 +4,14 @@ import ru.dbotthepony.kstarbound.RegistryObject import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.TileDefinition +import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kvector.api.IStruct2i +import ru.dbotthepony.kvector.util2d.AABB +import ru.dbotthepony.kvector.vector.Vector2d import java.io.DataInputStream +private val rect = Poly(listOf(Vector2d.ZERO, Vector2d(0.0, 1.0), Vector2d(1.0, 1.0), Vector2d(1.0, 0.0))) + interface IChunkCell : IStruct2i { /** * absolute (in world) @@ -26,6 +31,13 @@ interface IChunkCell : IStruct2i { return y } + val polies: Collection get() { + if (foreground.material.isMeta) + return emptyList() + + return listOf(rect + Vector2d(this.x.toDouble(), this.y.toDouble())) + } + val foreground: ITileState val background: ITileState val liquid: ILiquidState diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt deleted file mode 100644 index 864f6baa..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt +++ /dev/null @@ -1,353 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities - -import ru.dbotthepony.kbox2d.api.ContactEdge -import ru.dbotthepony.kbox2d.api.FixtureDef -import ru.dbotthepony.kbox2d.api.b2_polygonRadius -import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape -import ru.dbotthepony.kbox2d.dynamics.B2Fixture -import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.world.World -import ru.dbotthepony.kvector.util2d.AABB -import ru.dbotthepony.kvector.vector.Vector2d -import ru.dbotthepony.kvector.vector.times -import kotlin.math.absoluteValue - -enum class Move { - STAND_STILL, - MOVE_LEFT, - MOVE_RIGHT -} - -/** - * Базовый абстрактный класс, реализующий сущность, которая ходит по земле - */ -abstract class WalkableMovementController(entity: T) : MovementController(entity) { - init { - body.isFixedRotation = true - } - - protected abstract val moveDirection: Move - - protected var sensorA: B2Fixture? = null - protected var sensorB: B2Fixture? = null - protected var bodyFixture: B2Fixture? = null - - protected abstract fun recreateBodyFixture() - - protected open fun recreateSensors() { - sensorA?.destroy() - sensorB?.destroy() - - val bodyFixture = bodyFixture ?: return - val aabb = bodyFixture.shape.computeAABB(0) - - val sensorheight = (aabb.height - stepSize) / 2.0 - - sensorA = body.createFixture(FixtureDef( - shape = PolygonShape().also { it.setAsBox(0.2, sensorheight, Vector2d(-aabb.width / 2.0 - 0.2, aabb.height / 2.0 - sensorheight), 0.0) }, - isSensor = true, - )) - - sensorB = body.createFixture(FixtureDef( - shape = PolygonShape().also { it.setAsBox(0.2, sensorheight, Vector2d(aabb.width / 2.0 + 0.2, aabb.height / 2.0 - sensorheight), 0.0) }, - isSensor = true, - )) - } - - var wantsToDuck = false - open var isDucked = false - protected set - - override fun think() { - super.think() - thinkMovement() - } - - /** - * See [IWalkableEntity.topSpeed] - */ - open val topSpeed by entity::topSpeed - - /** - * See [IWalkableEntity.moveSpeed] - */ - open val moveSpeed by entity::moveSpeed - - /** - * See [IWalkableEntity.freeFallMoveSpeed] - */ - open val freeFallMoveSpeed by entity::freeFallMoveSpeed - - /** - * See [IWalkableEntity.brakeForce] - */ - open val brakeForce by entity::brakeForce - - /** - * See [IWalkableEntity.stepSize] - */ - open val stepSize by entity::stepSize - - /** - * See [IWalkableEntity.jumpForce] - */ - open val jumpForce by entity::jumpForce - - protected var jumpRequested = false - protected var nextJump = 0 - - open fun requestJump() { - if (jumpRequested || nextJump > world.ticks) - return - - jumpRequested = true - nextJump = world.ticks + 15 - } - - open fun recallJump() { - if (jumpRequested) { - jumpRequested = false - nextJump = 0 - } - } - - protected abstract fun canUnDuck(): Boolean - - protected var previousVelocity = Vector2d.ZERO - - protected open fun thinkMovement() { - if (onGround && !isDucked) { - when (moveDirection) { - Move.STAND_STILL -> { - body.linearVelocity += Vector2d(x = -body.linearVelocity.x * Starbound.TICK_TIME_ADVANCE * brakeForce) - } - - Move.MOVE_LEFT -> { - if (body.linearVelocity.x > 0.0) { - body.linearVelocity += Vector2d(x = -body.linearVelocity.x * Starbound.TICK_TIME_ADVANCE * brakeForce) - } - - if (body.linearVelocity.x > -topSpeed) { - body.linearVelocity += Vector2d(x = -moveSpeed * Starbound.TICK_TIME_ADVANCE) - - // Ghost collision prevention - if (body.linearVelocity.x.absoluteValue < 1) { - body.linearVelocity += -Starbound.TICK_TIME_ADVANCE * world.gravity * 2.0 - } - } - - var wantToStepUp = false - var foundContact: ContactEdge? = null - - for (contact in body.contactEdgeIterator) { - if (contact.contact.manifold.localNormal.dot(Vector2d.NEGATIVE_X) >= 0.95) { - // we hit something to our left - wantToStepUp = true - foundContact = contact - break - } - } - - if (wantToStepUp) { - // make sure something we hit is actually a staircase of sort, and not geometry edges - val aabbFound: AABB - - if (foundContact!!.contact.fixtureB == sensorA) { - aabbFound = foundContact.contact.fixtureA.getAABB(foundContact.contact.childIndexA) - } else { - aabbFound = foundContact.contact.fixtureB.getAABB(foundContact.contact.childIndexB) - } - - if (aabbFound.maxs.y - b2_polygonRadius * 2.0 <= body.worldSpaceAABB.mins.y) { - // just a crack on sidewalk - wantToStepUp = false - } - } - - if (wantToStepUp) { - var stepHeightClear = true - - for (contact in body.contactEdgeIterator) { - if (contact.contact.fixtureA == sensorA || contact.contact.fixtureB == sensorA) { - if (contact.contact.isTouching) { - stepHeightClear = false - break - } - } - } - - if (stepHeightClear) { - val velocity = if (previousVelocity.length > body.linearVelocity.length) previousVelocity else body.linearVelocity - body.setTransform(body.position + Vector2d(x = -0.05, y = stepSize), body.angle) - body.linearVelocity = velocity - //body.linearVelocity += Vector2d(y = -Starbound.SECONDS_IN_FRAME * 18.0 * stepSize * world.gravity.y) - } - } - } - - Move.MOVE_RIGHT -> { - if (body.linearVelocity.x < 0.0) { - body.linearVelocity += Vector2d(x = -body.linearVelocity.x * Starbound.TICK_TIME_ADVANCE * brakeForce) - } - - if (body.linearVelocity.x < topSpeed) { - body.linearVelocity += Vector2d(x = moveSpeed * Starbound.TICK_TIME_ADVANCE) - - // Ghost collision prevention - if (body.linearVelocity.x.absoluteValue < 1) { - body.linearVelocity += -Starbound.TICK_TIME_ADVANCE * world.gravity * 2.0 - } - } - - var wantToStepUp = false - var foundContact: ContactEdge? = null - - for (contact in body.contactEdgeIterator) { - if (contact.contact.manifold.localNormal.dot(Vector2d.POSITIVE_X) >= 0.95) { - // we hit something to our right - wantToStepUp = true - foundContact = contact - break - } - } - - if (wantToStepUp) { - // make sure something we hit is actually a staircase of sort, and not geometry edges - val aabbFound: AABB - - if (foundContact!!.contact.fixtureB == sensorB) { - aabbFound = foundContact.contact.fixtureA.getAABB(foundContact.contact.childIndexA) - } else { - aabbFound = foundContact.contact.fixtureB.getAABB(foundContact.contact.childIndexB) - } - - if (aabbFound.maxs.y - b2_polygonRadius * 2.0 <= body.worldSpaceAABB.mins.y) { - // just a crack on sidewalk - wantToStepUp = false - } - } - - if (wantToStepUp) { - var stepHeightClear = true - - for (contact in body.contactEdgeIterator) { - if (contact.contact.fixtureA == sensorB || contact.contact.fixtureB == sensorB) { - if (contact.contact.isTouching) { - stepHeightClear = false - break - } - } - } - - if (stepHeightClear) { - val velocity = if (previousVelocity.length > body.linearVelocity.length) previousVelocity else body.linearVelocity - body.setTransform(body.position + Vector2d(x = 0.05, y = stepSize), body.angle) - body.linearVelocity = velocity - //body.linearVelocity += Vector2d(y = -Starbound.SECONDS_IN_FRAME * 18.0 * stepSize * world.gravity.y) - } - } - } - } - - previousVelocity = body.linearVelocity - - if (jumpRequested) { - jumpRequested = false - nextJump = world.ticks + 15 - - body.linearVelocity += Vector2d(y = jumpForce) - } - } else if (!onGround && !isDucked && freeFallMoveSpeed != 0.0) { - when (moveDirection) { - Move.STAND_STILL -> { - // do nothing - } - - Move.MOVE_LEFT -> { - body.linearVelocity += Vector2d(x = -freeFallMoveSpeed * Starbound.TICK_TIME_ADVANCE) - } - - Move.MOVE_RIGHT -> { - body.linearVelocity += Vector2d(x = freeFallMoveSpeed * Starbound.TICK_TIME_ADVANCE) - } - } - } else if (onGround && isDucked) { - body.linearVelocity += Vector2d(x = -body.linearVelocity.x * Starbound.TICK_TIME_ADVANCE * brakeForce) - } - - if (wantsToDuck && onGround) { - isDucked = true - recreateBodyFixture() - recreateSensors() - body.isAwake = true - } else if (isDucked) { - if (canUnDuck()) { - isDucked = false - recreateBodyFixture() - recreateSensors() - body.isAwake = true - } - } - } -} - -abstract class AliveEntity(world: World<*, *>) : Entity(world) { - open var maxHealth = 10.0 - open var health = 10.0 -} - -abstract class AliveWalkingEntity(world: World<*, *>) : AliveEntity(world) { - abstract override val movement: WalkableMovementController<*> - - /** - * Максимальная скорость передвижения этого AliveMovementController в Starbound Units/секунда - * - * Скорость передвижения: Это скорость вдоль земли (или в воздухе, если парит) при ходьбе. - * - * Если вектор скорости вдоль поверхности (или в воздухе, если парит) больше заданного значения, - * то сущность быстро тормозит (учитывая силу трения) - */ - open val topSpeed: Double = 20.0 - - /** - * Скорость ускорения сущности в Starbound Units/секунда^2 - * - * Если сущность хочет двигаться вправо или влево, - * то она разгоняется с данной скоростью. - */ - open val moveSpeed: Double = 64.0 - - /** - * То, как сущность может влиять на свою скорость в Starbound Units/секунда^2 - * когда находится в свободном падении или в состоянии невесомости. - * - * Позволяет в т.ч. игрокам изменять свою траекторию полёта в стиле Quake. - */ - open val freeFallMoveSpeed: Double = 8.0 - - /** - * "Сила", с которой сущность останавливается, если не хочет двигаться. - * - * Зависит от текущего трения, так как технически является множителем трения поверхности, - * на которой стоит сущность. - */ - open val brakeForce: Double = 16.0 - - /** - * Высота шага данной сущности. Данное значение отвечает за то, на сколько блоков - * сможет подняться сущность просто двигаясь в одном направлении без необходимости прыгнуть. - */ - open val jumpForce: Double = 20.0 - - /** - * Импульс прыжка данной сущности. Если сущность парит, то данное значение не несёт никакой - * полезной нагрузки. - */ - open val stepSize: Double = 1.1 - - open var wantsToDuck - get() = movement.wantsToDuck - set(value) { movement.wantsToDuck = value } - - open val isDucked get() = movement.isDucked -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt index c657a947..85ee373e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt @@ -1,7 +1,9 @@ package ru.dbotthepony.kstarbound.world.entities +import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kvector.vector.Vector2d abstract class Entity(val world: World<*, *>) { @@ -56,8 +58,6 @@ abstract class Entity(val world: World<*, *>) { val old = field field = Vector2d(world.x.cell(value.x), world.y.cell(value.y)) - movement.notifyPositionChanged() - if (isSpawned && !isRemoved) { val oldChunkPos = world.chunkFromCell(old) val newChunkPos = world.chunkFromCell(field) @@ -85,9 +85,10 @@ abstract class Entity(val world: World<*, *>) { return field = value - movement.notifyPositionChanged() } + var velocity = Vector2d.ZERO + val hitboxes = ArrayList() /** * Whenever is this entity spawned in world ([spawn] called). @@ -113,8 +114,6 @@ abstract class Entity(val world: World<*, *>) { if (chunk == null) { world.orphanedEntities.add(this) } - - movement.onSpawnedInWorld() } open fun remove() { @@ -127,24 +126,8 @@ abstract class Entity(val world: World<*, *>) { world.entities.remove(this) chunk?.removeEntity(this) } - - movement.destroy() } - /** - * This entity's movement controller. Even logical entities have one, but they have - * dummy movement controller, which does nothing. - * - * If entity is physical, this controller handle interaction with Box2D world, update angles and - * position and other stuff. - */ - abstract val movement: MovementController<*> - - /** - * Внутренний блок "раздумья" сущности, вызывается на каждом тике мира - */ - protected abstract fun thinkAI() - /** * Заставляет сущность "думать". */ @@ -153,11 +136,57 @@ abstract class Entity(val world: World<*, *>) { throw IllegalStateException("Tried to think before spawning in world") } - movement.think() - thinkAI() + move() + thinkInner() + } + + protected abstract fun thinkInner() + + protected open fun move() { + position += velocity + + val polies = ArrayList() + + for (x in -1 .. 1) { + for (y in -1 .. 1) { + world.chunkMap.getCell(position.x.toInt() + x, position.y.toInt() + y)?.let { + polies.addAll(it.polies) + } + } + } + + for (i in 0 until 10) { + val intersects = ArrayList() + + this.hitboxes.forEach { hitbox0 -> + val hitbox = hitbox0 + position + polies.forEach { poly -> hitbox.intersect(poly)?.let { intersects.add(it) } } + } + + if (intersects.isEmpty()) + break + else { + val max = intersects.max() + // resolve collision + position += max.vector + // collision response + velocity -= max.axis * velocity.dot(max.axis * 1.1) + } + } } open fun onTouchSurface(velocity: Vector2d, normal: Vector2d) { } + + open fun render(client: StarboundClient = StarboundClient.current()) { + hitboxes.forEach { (it + position).render(client) } + } + + open var maxHealth = 0.0 + open var health = 0.0 + + open fun hurt(amount: Double): Boolean { + return false + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt index 0e6e3b68..3c0a7b39 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt @@ -5,49 +5,19 @@ import ru.dbotthepony.kbox2d.api.FixtureDef import ru.dbotthepony.kbox2d.api.Manifold import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact +import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.physics.Poly +import ru.dbotthepony.kvector.util2d.AABB +import ru.dbotthepony.kvector.vector.Vector2d class ItemEntity(world: World<*, *>, val def: IItemDefinition) : Entity(world) { - override val movement = object : MovementController(this) { - override fun beginContact(contact: AbstractContact) { - // тут надо код подбора предмета игроком, если мы начинаем коллизию с окружностью подбора - } - - override fun endContact(contact: AbstractContact) { - - } - - override fun postSolve(contact: AbstractContact, impulse: ContactImpulse) { - - } - - override fun preSolve(contact: AbstractContact, oldManifold: Manifold) { - if (contact.fixtureA.userData is ItemEntity && contact.fixtureB.userData is ItemEntity) - contact.isEnabled = false - } - - override fun onSpawnedInWorld() { - super.onSpawnedInWorld() - - // все предметы в мире являются коробками - val fixture = FixtureDef( - shape = PolygonShape().also { it.setAsBox(0.75, 0.75) }, - restitution = 0.0, - friction = 1.0, - density = 0.3, - ) - - fixture.userData = this@ItemEntity - - body.createFixture(fixture) - - // предметы не могут поворачиваться в мире, всегда падают плашмя - body.isFixedRotation = true - } + init { + hitboxes.add(Poly(AABB.rectangle(Vector2d.ZERO, 0.75, 0.75))) } - override fun thinkAI() { + override fun thinkInner() { // TODO: деспавнинг? // просто, как бы, предметы не должны уж так сильно нагружать процессор } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt deleted file mode 100644 index 2c27d78f..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt +++ /dev/null @@ -1,155 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities - -import ru.dbotthepony.kbox2d.api.* -import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact -import ru.dbotthepony.kvector.util2d.AABB -import ru.dbotthepony.kvector.vector.Vector2d - -enum class CollisionResolution { - STOP, - BOUNCE, - PUSH, - SLIDE, -} - -abstract class MovementController(val entity: T) : IContactListener { - val world = entity.world - open var position by entity::position - open var angle by entity::angle - - /** - * Уничтожает данный movement controller. - * - * Вызывается изнутри [Entity.remove] - */ - open fun destroy() { - body.world.destroyBody(body) - } - - private val bodyInit = lazy { - world.physics.createBody(BodyDef( - position = position, - angle = angle, - type = BodyType.DYNAMIC, - userData = this - )) - } - - /** - * Было ли создано физическое тело - */ - val bodyInitialized: Boolean - get() = bodyInit.isInitialized() - - /** - * Физическое тело данного контроллера перемещения - * - * Не создаётся в мире пока к нему не обратятся - */ - protected val body by bodyInit - - /** - * Вызывается изнутри [Entity], когда оно спавнится в самом мире. - * - * Так как никто не запрещает нам создавать физические тела в физическом мире - * до того, как мы появимся в самом мире, это негативно сказывается на производительности И не является корректным - * поведением. - * - * Поэтому, прикреплять фигуры к физическому телу лучше всего из данной функции. - */ - open fun onSpawnedInWorld() { - - } - - open val velocity get() = body.linearVelocity - - open fun applyVelocity(velocity: Vector2d) { - if (velocity != Vector2d.ZERO) { - body.applyLinearImpulseToCenter(velocity) - } - } - - /** - * Проверяет, находится ли что-либо под нами - */ - open val onGround: Boolean get() { - for (contact in body.contactEdgeIterator) { - if (contact.contact.manifold.localNormal.dot(world.gravity.unitVector) >= 0.97) { - return true - } - } - - return false - } - - /** - * World space AABB, by default returning combined AABB of physical body. - */ - open val worldAABB: AABB get() { - return body.worldSpaceAABB - } - - /** - * This is called on each world step to update variables and account of changes of - * physics world and this physics body. - */ - open fun think() { - mutePositionChanged = true - position = body.position - angle = body.angle - mutePositionChanged = false - } - - protected open fun onTouchSurface(velocity: Vector2d, normal: Vector2d) { - entity.onTouchSurface(velocity, normal) - } - - protected var mutePositionChanged = false - - open fun notifyPositionChanged() { - if (mutePositionChanged) { - return - } - - body.setTransform(entity.position, entity.angle) - } -} - -/** - * Movement controller for logical entities, which does nothing. - */ -class LogicalMovementController(entity: Entity) : MovementController(entity) { - override val worldAABB: AABB get() = AABB(position, position) - - // Dummies never touch anything, since they don't have Box2D body - override val onGround: Boolean = false - override val velocity: Vector2d = Vector2d.ZERO - - override fun think() { - // no-op - } - - override fun notifyPositionChanged() { - // no-op - } - - override fun beginContact(contact: AbstractContact) { - // no-op - } - - override fun endContact(contact: AbstractContact) { - // no-op - } - - override fun preSolve(contact: AbstractContact, oldManifold: Manifold) { - // no-op - } - - override fun postSolve(contact: AbstractContact, impulse: ContactImpulse) { - // no-op - } - - companion object { - private val DUMMY_AABB = AABB.rectangle(Vector2d.ZERO, 0.1, 0.1) - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt deleted file mode 100644 index b8843707..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt +++ /dev/null @@ -1,74 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities - -import ru.dbotthepony.kbox2d.api.ContactImpulse -import ru.dbotthepony.kbox2d.api.FixtureDef -import ru.dbotthepony.kbox2d.api.Manifold -import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape -import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact -import ru.dbotthepony.kstarbound.world.World -import ru.dbotthepony.kvector.vector.Vector2d - -class PlayerMovementController(entity: PlayerEntity) : WalkableMovementController(entity) { - public override var moveDirection = Move.STAND_STILL - - override fun recreateBodyFixture() { - bodyFixture?.destroy() - - if (isDucked) { - bodyFixture = body.createFixture(FixtureDef( - shape = DUCKING, - friction = 0.4, - density = 1.9, - )) - } else { - bodyFixture = body.createFixture(FixtureDef( - shape = STANDING, - friction = 0.4, - density = 1.9, - )) - } - } - - init { - recreateBodyFixture() - recreateSensors() - } - - override fun beginContact(contact: AbstractContact) { - - } - - override fun endContact(contact: AbstractContact) { - - } - - override fun preSolve(contact: AbstractContact, oldManifold: Manifold) { - - } - - override fun postSolve(contact: AbstractContact, impulse: ContactImpulse) { - - } - - override fun canUnDuck(): Boolean { - return !world.testSpace(STANDING_AABB + position) - } - - companion object { - private val STANDING = PolygonShape().also { it.setAsBox(0.9, 1.8) } - private val STANDING_AABB = STANDING.computeAABB(0) - private val DUCKING = PolygonShape().also { it.setAsBox(0.9, 0.9, Vector2d(y = -0.9), 0.0) } - private val DUCKING_AABB = DUCKING.computeAABB(0) - } -} - -/** - * Физический аватар игрока в мире - */ -open class PlayerEntity(world: World<*, *>) : AliveWalkingEntity(world) { - override val movement = PlayerMovementController(this) - - override fun thinkAI() { - - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt new file mode 100644 index 00000000..2cd821c2 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt @@ -0,0 +1,236 @@ +package ru.dbotthepony.kstarbound.world.physics + +import com.google.common.collect.ImmutableList +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import org.lwjgl.opengl.GL11.GL_LINES +import ru.dbotthepony.kstarbound.client.StarboundClient +import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType +import ru.dbotthepony.kvector.api.IStruct2d +import ru.dbotthepony.kvector.arrays.Matrix3f +import ru.dbotthepony.kvector.util2d.AABB +import ru.dbotthepony.kvector.util2d.intersectSegments +import ru.dbotthepony.kvector.vector.RGBAColor +import ru.dbotthepony.kvector.vector.Vector2d +import kotlin.math.absoluteValue + +private fun calculateEdges(points: List): ImmutableList { + require(points.size >= 3) { "Provided poly is invalid (only ${points.size} points are defined)" } + + val edges = ImmutableList.Builder() + + for (i in points.indices) { + val p0 = points[i] + val p1 = points[(i + 1) % points.size] + + val diff = (p1 - p0).unitVector + val normal = Vector2d(-diff.y, diff.x) + + edges.add(Poly.Edge(p0, p1, normal)) + } + + return edges.build() +} + +/** + * edges are built in clockwise winding + * + * If poly shape is not convex behavior of SAT is undefined + */ +class Poly private constructor(val edges: ImmutableList, val vertices: ImmutableList) { + constructor(points: List) : this(calculateEdges(points), ImmutableList.copyOf(points)) + constructor(aabb: AABB) : this(listOf(aabb.bottomLeft, aabb.topLeft, aabb.topRight, aabb.bottomRight)) + + val aabb = AABB( + Vector2d(vertices.minOf { it.x }, vertices.minOf { it.y }), + Vector2d(vertices.maxOf { it.x }, vertices.maxOf { it.y }), + ) + + data class Edge(val p0: Vector2d, val p1: Vector2d, val normal: Vector2d) { + operator fun plus(other: IStruct2d): Edge { + return Edge(p0 + other, p1 + other, normal) + } + + operator fun minus(other: IStruct2d): Edge { + return Edge(p0 - other, p1 - other, normal) + } + + operator fun times(other: IStruct2d): Edge { + return Edge(p0 * other, p1 * other, (normal * other).unitVector) + } + + operator fun times(other: Double): Edge { + return Edge(p0 * other, p1 * other, (normal * other).unitVector) + } + } + + data class Penetration(val axis: Vector2d, val penetration: Double) : Comparable { + val vector = axis * penetration + + override fun compareTo(other: Penetration): Int { + return penetration.absoluteValue.compareTo(other.penetration.absoluteValue) + } + } + + operator fun plus(value: Penetration): Poly { + val vertices = ImmutableList.Builder() + val edges = ImmutableList.Builder() + + for (v in this.vertices) vertices.add(v + value.vector) + for (v in this.edges) edges.add(v + value.vector) + + return Poly(edges.build(), vertices.build()) + } + + operator fun plus(value: IStruct2d): Poly { + val vertices = ImmutableList.Builder() + val edges = ImmutableList.Builder() + + for (v in this.vertices) vertices.add(v + value) + for (v in this.edges) edges.add(v + value) + + return Poly(edges.build(), vertices.build()) + } + + operator fun minus(value: IStruct2d): Poly { + val vertices = ImmutableList.Builder() + val edges = ImmutableList.Builder() + + for (v in this.vertices) vertices.add(v - value) + for (v in this.edges) edges.add(v - value) + + return Poly(edges.build(), vertices.build()) + } + + operator fun times(value: IStruct2d): Poly { + val vertices = ImmutableList.Builder() + val edges = ImmutableList.Builder() + + for (v in this.vertices) vertices.add(v * value) + for (v in this.edges) edges.add(v * value) + + return Poly(edges.build(), vertices.build()) + } + + operator fun times(value: Double): Poly { + val vertices = ImmutableList.Builder() + val edges = ImmutableList.Builder() + + for (v in this.vertices) vertices.add(v * value) + for (v in this.edges) edges.add(v * value) + + return Poly(edges.build(), vertices.build()) + } + + // min / max + fun project(normal: Vector2d): IStruct2d { + var min = vertices.first().dot(normal) + var max = min + + for (vertex in vertices) { + val dist = vertex.dot(normal) + + min = min.coerceAtMost(dist) + max = max.coerceAtLeast(dist) + } + + return Vector2d(min, max) + } + + fun intersect(other: Poly): Penetration? { + if (!aabb.intersectWeak(other.aabb)) + return null + + val normals = ObjectOpenHashSet() + edges.forEach { normals.add(it.normal) } + other.edges.forEach { normals.add(it.normal) } + + val intersections = ArrayList() + + for (normal in normals) { + val projectThis = project(normal) + val projectOther = other.project(normal) + + if (!intersectSegments(projectThis.component1(), projectThis.component2(), projectOther.component1(), projectOther.component2())) { + return null + } else { + val width = projectThis.component2() - projectThis.component1() + + if ( + projectOther.component1() in projectThis.component1() .. projectThis.component2() && + projectOther.component2() in projectThis.component1() .. projectThis.component2() + ) { + // other inside this + val minMin = projectThis.component1() - projectOther.component1() + val maxMax = projectThis.component2() - projectOther.component2() + + if (minMin <= maxMax) { + // push to left + intersections.add(Penetration(normal, -minMin - width)) + } else { + // push to right + intersections.add(Penetration(normal, maxMax + width)) + } + } else if ( + projectThis.component1() in projectOther.component1() .. projectOther.component2() && + projectThis.component2() in projectOther.component1() .. projectOther.component2() + ) { + // this inside other + val minMin = projectThis.component1() - projectOther.component1() + val maxMax = projectOther.component2() - projectThis.component2() + + if (minMin <= maxMax) { + // push to left + intersections.add(Penetration(normal, -minMin - width)) + } else { + // push to right + intersections.add(Penetration(normal, maxMax + width)) + } + } else if (projectOther.component1() in projectThis.component1() .. projectThis.component2()) { + // other's min point is within this + intersections.add(Penetration(normal, projectOther.component1() - projectThis.component2())) + } else { + // other's max point in within this + intersections.add(Penetration(normal, projectOther.component2() - projectThis.component1())) + } + + if (intersections.last().penetration == 0.0) { + return null + } + } + } + + if (intersections.isEmpty()) + return null + + return intersections.min() + } + + fun render(client: StarboundClient = StarboundClient.current(), color: RGBAColor = RGBAColor.LIGHT_GREEN) { + val program = client.programs.position + val lines = program.builder.builder + program.use() + lines.begin(GeometryType.LINES) + + for (edge in edges) { + val current = edge.p0 + val next = edge.p1 + lines.vertex(client.stack.last(), current) + lines.vertex(client.stack.last(), next) + lines.vertex(client.stack.last(), (next + current) / 2.0) + lines.vertex(client.stack.last(), (next + current) / 2.0 + edge.normal * 3.0) + } + + program.modelMatrix = identity + program.colorMultiplier = color + program.builder.upload() + program.builder.draw(GL_LINES) + } + + override fun toString(): String { + return "Poly[edges = $edges]" + } + + companion object { + private val identity = Matrix3f.identity() + } +}