SAT testcode

This commit is contained in:
DBotThePony 2023-10-14 10:39:20 +07:00
parent 2280882247
commit b9975657d4
Signed by: DBot
GPG Key ID: DCC23B5715498507
15 changed files with 407 additions and 703 deletions

View File

@ -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<List<Vector2d>>("""[ [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<Poly>()
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<Poly.Penetration>()
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)
}

View File

@ -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?
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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) }
}
}

View File

@ -29,24 +29,4 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
it.liquidIsDirty = true
}
}
private val entityRenderers = HashMap<Entity, EntityRenderer>()
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)
}
}

View File

@ -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() {

View File

@ -324,10 +324,10 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
protected val entities = ReferenceOpenHashSet<Entity>()
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)) {

View File

@ -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<Poly> 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

View File

@ -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<T : AliveWalkingEntity>(entity: T) : MovementController<T>(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
}

View File

@ -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<Poly>()
/**
* 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<Poly>()
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<Poly.Penetration>()
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
}
}

View File

@ -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<ItemEntity>(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: деспавнинг?
// просто, как бы, предметы не должны уж так сильно нагружать процессор
}

View File

@ -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<T : Entity>(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>(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)
}
}

View File

@ -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<PlayerEntity>(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() {
}
}

View File

@ -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<Vector2d>): ImmutableList<Poly.Edge> {
require(points.size >= 3) { "Provided poly is invalid (only ${points.size} points are defined)" }
val edges = ImmutableList.Builder<Poly.Edge>()
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<Edge>, val vertices: ImmutableList<Vector2d>) {
constructor(points: List<Vector2d>) : 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<Penetration> {
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<Vector2d>()
val edges = ImmutableList.Builder<Edge>()
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<Vector2d>()
val edges = ImmutableList.Builder<Edge>()
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<Vector2d>()
val edges = ImmutableList.Builder<Edge>()
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<Vector2d>()
val edges = ImmutableList.Builder<Edge>()
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<Vector2d>()
val edges = ImmutableList.Builder<Edge>()
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<Vector2d>()
edges.forEach { normals.add(it.normal) }
other.edges.forEach { normals.add(it.normal) }
val intersections = ArrayList<Penetration>()
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()
}
}