Going up the stairs again

This commit is contained in:
DBotThePony 2022-02-21 12:16:42 +07:00
parent 05e21deb57
commit 35e5b64606
Signed by: DBot
GPG Key ID: DCC23B5715498507
12 changed files with 243 additions and 81 deletions

View File

@ -6,6 +6,9 @@ import ru.dbotthepony.kbox2d.collision.DynamicTree
import ru.dbotthepony.kbox2d.collision.BroadPhase
fun interface ProxyQueryCallback {
/**
* Return false to terminate query.
*/
fun invoke(nodeId: Int, userData: Any?): Boolean
}
@ -61,7 +64,7 @@ interface IProxieable {
* Query an AABB for overlapping proxies. The callback class
* is called for each proxy that overlaps the supplied AABB.
*
* @return Whenever callback returned false
* @return Whenever callback terminated query early
*/
fun query(aabb: AABB, callback: ProxyQueryCallback): Boolean

View File

@ -1,3 +1,6 @@
@file:Suppress("unused")
package ru.dbotthepony.kbox2d.api
import ru.dbotthepony.kvector.api.concrete.IMatrix2d
@ -130,8 +133,10 @@ class Rotation(
}
}
/// A transform contains translation and rotation. It is used to represent
/// the position and orientation of rigid frames.
/**
* A transform contains translation and rotation. It is used to represent
* the position and orientation of rigid frames.
*/
class Transform(
position: Vector2d,
val rotation: Rotation,
@ -200,13 +205,17 @@ class Transform(
}
}
/** This describes the motion of a body/shape for TOI computation.
/**
* This describes the motion of a body/shape for TOI computation.
* Shapes are defined with respect to the body origin, which may
* no coincide with the center of mass. However, to support dynamics
* we must interpolate the center of mass position.
*/
class Sweep {
var localCenter: Vector2d = Vector2d.ZERO ///< local center of mass position
/**
* local center of mass position
*/
var localCenter: Vector2d = Vector2d.ZERO
set(value) {
if (!value.isFinite) {
throw IllegalArgumentException("Tried to set illegal local center $value")
@ -215,7 +224,10 @@ class Sweep {
field = value
}
var c0: Vector2d = Vector2d.ZERO ///< center world positions
/**
* center world positions
*/
var c0: Vector2d = Vector2d.ZERO
set(value) {
if (!value.isFinite) {
throw IllegalArgumentException("Tried to set illegal center world position $value")
@ -224,7 +236,10 @@ class Sweep {
field = value
}
var c: Vector2d = Vector2d.ZERO ///< center world positions
/**
* center world positions
*/
var c: Vector2d = Vector2d.ZERO
set(value) {
if (!value.isFinite) {
throw IllegalArgumentException("Tried to set illegal center world position $value")
@ -233,7 +248,13 @@ class Sweep {
field = value
}
var a0: Double = 0.0 ///< world angles
var oldCenter by this::c0
var newCenter by this::c
/**
* world angles
*/
var a0: Double = 0.0
set(value) {
if (!value.isFinite()) {
throw IllegalArgumentException("Tried to set illegal non finite $value")
@ -246,7 +267,10 @@ class Sweep {
field = value
}
var a: Double = 0.0 ///< world angles
/**
* world angles
*/
var a: Double = 0.0
set(value) {
if (!value.isFinite()) {
throw IllegalArgumentException("Tried to set illegal non finite $value")
@ -259,8 +283,13 @@ class Sweep {
field = value
}
/// Fraction of the current time step in the range [0,1]
/// c0 and a0 are the positions at alpha0.
var oldAngles by this::a0
var newAngles by this::a
/**
* Fraction of the current time step in the range [0,1]
* [c0] and [a0] are the positions at alpha0.
*/
var alpha0: Double = 0.0
set(value) {
if (!value.isFinite()) {
@ -276,24 +305,30 @@ class Sweep {
/** Get the interpolated transform at a specific time.
* @param transform the output transform
* @param beta is a factor in [0,1], where 0 indicates alpha0.
* @param beta is a factor in [0,1], where 0 indicates [alpha0].
* https://fgiesen.wordpress.com/2012/08/15/linear-interpolation-past-present-and-future/
*/
fun getTransform(loadInto: Transform, beta: Double) {
loadInto.position = c0 * (1.0 - beta) + c * beta
fun getTransform(transform: Transform, beta: Double) {
transform.position = c0 * (1.0 - beta) + c * beta
val angle = (1.0 - beta) * a0 + beta * a
loadInto.rotation.set(angle)
loadInto.position -= loadInto.position * localCenter
transform.rotation.set(angle)
transform.position -= transform.position * localCenter
}
/** Get the interpolated transform at a specific time.
* @param beta is a factor in [0,1], where 0 indicates [alpha0].
* https://fgiesen.wordpress.com/2012/08/15/linear-interpolation-past-present-and-future/
*/
fun getTransform(beta: Double): Transform {
val v = Transform()
getTransform(v, beta)
return v
}
/// Advance the sweep forward, yielding a new initial state.
/// @param alpha the new initial time.
/**
* Advance the sweep forward, yielding a new initial state.
* @param alpha the new initial time.
*/
fun advance(alpha: Double) {
require(alpha < 1.0) { "Bad advance value $alpha" }
@ -303,7 +338,9 @@ class Sweep {
alpha0 = alpha
}
/// Normalize the angles.
/**
* Normalize the angles.
*/
fun normalize() {
val d = floor(a0 / (PI * 2.0)) * 2.0 * PI
a0 -= d

View File

@ -7,6 +7,7 @@ import ru.dbotthepony.kbox2d.collision.shapes.EdgeShape
import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import ru.dbotthepony.kvector.vector.ndouble.times
import java.util.*
var b2_gjkCalls = 0
private set
@ -21,11 +22,11 @@ var b2_gjkMaxIters = 0
* A distance proxy is used by the GJK algorithm.
* It encapsulates any shape.
*/
class DistanceProxy {
class DistanceProxy(shape: IShape<*>, index: Int) {
val vertices: List<Vector2d>
val radius: Double
constructor(shape: IShape<*>, index: Int) {
init {
when (shape.type) {
IShape.Type.CIRCLE -> {
val circle = shape as CircleShape
@ -41,7 +42,7 @@ class DistanceProxy {
IShape.Type.POLYGON -> {
val polygon = shape as PolygonShape
vertices = polygon.vertices
vertices = Collections.unmodifiableList(polygon.vertices)
radius = polygon.radius
}
@ -62,11 +63,6 @@ class DistanceProxy {
}
}
constructor(vertices: List<Vector2d>, radius: Double) {
this.vertices = vertices
this.radius = radius
}
/**
* Get the supporting vertex index in the given direction.
*/

View File

@ -230,20 +230,22 @@ const val k_maxIterations = 20
fun b2TimeOfImpact(
proxyA: DistanceProxy,
proxyB: DistanceProxy,
_sweepA: Sweep,
_sweepB: Sweep,
sweepA: Sweep,
sweepB: Sweep,
tMax: Double, // defines sweep interval [0, tMax]
): TOIOutput {
var timer = System.nanoTime()
val timer = System.nanoTime()
b2_toiCalls++
var state = TOIOutput.State.UNKNOWN
var t = tMax
// TODO
val sweepA = _sweepA.copy()
val sweepB = _sweepB.copy()
@Suppress("name_shadowing")
val sweepA = sweepA.copy()
@Suppress("name_shadowing")
val sweepB = sweepB.copy()
sweepA.normalize()
sweepB.normalize()

View File

@ -599,8 +599,8 @@ class B2World(
val output = b2TimeOfImpact(
proxyA = DistanceProxy(fA.shape, indexA),
proxyB = DistanceProxy(fB.shape, indexB),
_sweepA = bA.sweep,
_sweepB = bB.sweep,
sweepA = bA.sweep,
sweepB = bB.sweep,
tMax = 1.0
)

View File

@ -44,8 +44,8 @@ class ContactManager {
// Remove from the world.
run {
val prev = contact.prev as AbstractContact?
val next = contact.next as AbstractContact?
val prev = contact.prev
val next = contact.next
prev?.next = next
next?.prev = prev
@ -93,7 +93,6 @@ class ContactManager {
// Update awake contacts.
for (c in contactListIterator) {
c as AbstractContact
val fixtureA = c.fixtureA
val fixtureB = c.fixtureB
val indexA = c.childIndexA
@ -119,8 +118,8 @@ class ContactManager {
c.isFlaggedForFiltering = false
}
val activeA = bodyA.isAwake && bodyA.type != ru.dbotthepony.kbox2d.api.BodyType.STATIC
val activeB = bodyB.isAwake && bodyB.type != ru.dbotthepony.kbox2d.api.BodyType.STATIC
val activeA = bodyA.isAwake && bodyA.type != BodyType.STATIC
val activeB = bodyB.isAwake && bodyB.type != BodyType.STATIC
// At least one body must be awake and it must be dynamic or kinematic.
if (!activeA && !activeB) {
@ -144,7 +143,7 @@ class ContactManager {
broadPhase.updatePairs(this::addPair)
}
fun addPair(proxyUserDataA: Any?, proxyUserDataB: Any?) {
private fun addPair(proxyUserDataA: Any?, proxyUserDataB: Any?) {
val proxyA = proxyUserDataA as FixtureProxy
val proxyB = proxyUserDataB as FixtureProxy
@ -200,7 +199,7 @@ class ContactManager {
// Contact creation may swap fixtures.
// Insert into the world.
c.next = contactList
(contactList as AbstractContact?)?.prev = c
contactList?.prev = c
contactList = c
// Connect to island graph.

View File

@ -4,6 +4,8 @@ import org.apache.logging.log4j.LogManager
import org.lwjgl.Version
import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose
import ru.dbotthepony.kbox2d.api.*
import ru.dbotthepony.kbox2d.collision.DistanceProxy
import ru.dbotthepony.kbox2d.collision.b2TimeOfImpact
import ru.dbotthepony.kbox2d.collision.shapes.CircleShape
import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
import ru.dbotthepony.kstarbound.client.StarboundClient
@ -130,7 +132,7 @@ fun main() {
}
run {
val stripes = 4
val stripes = 0
for (stripe in 0 until stripes) {
for (x in 0 .. (stripes - stripe)) {
@ -178,10 +180,6 @@ fun main() {
client.gl.box2dRenderer.drawAABB = false
client.gl.box2dRenderer.drawJoints = false
client.onPostDrawWorld {
}
ent.spawn()
while (client.renderFrame()) {

View File

@ -8,5 +8,5 @@ data class ClientSettings(
*/
var scale: Float = 2f,
var debugCollisions: Boolean = true,
var debugCollisions: Boolean = false,
)

View File

@ -260,7 +260,7 @@ class StarboundClient : AutoCloseable {
val runtime = Runtime.getRuntime()
gl.font.render("FPS: ${(averageFramesPerSecond * 100f).toInt() / 100f}", scale = 0.4f)
gl.font.render("Mem: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", x = viewportWidth.toFloat(), scale = 0.4f, alignX = TextAlignX.RIGHT)
gl.font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = gl.font.lineHeight * 0.5f, scale = 0.4f)
GLFW.glfwSwapBuffers(window)
GLFW.glfwPollEvents()

View File

@ -11,6 +11,7 @@ fun formatBytesShort(input: Long): String {
in 0 until KIBIBYTE -> "${input}b"
in KIBIBYTE until MEBIBYTE -> "${(((input / KIBIBYTE).toDouble() + (input % KIBIBYTE).toDouble() / KIBIBYTE) * 100.0).toLong().toDouble() / 100.0}KiB"
in MEBIBYTE until GIBIBYTE -> "${(((input / MEBIBYTE).toDouble() + (input % MEBIBYTE).toDouble() / MEBIBYTE) * 100.0).toLong().toDouble() / 100.0}MiB"
in GIBIBYTE until TEBIBYTE -> "${(((input / GIBIBYTE).toDouble() + (input % GIBIBYTE).toDouble() / GIBIBYTE) * 100.0).toLong().toDouble() / 100.0}GiB"
else -> "${input}b"
}
}

View File

@ -1,5 +1,11 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kbox2d.api.ContactEdge
import ru.dbotthepony.kbox2d.api.FixtureDef
import ru.dbotthepony.kbox2d.api.b2_linearSlop
import ru.dbotthepony.kbox2d.api.b2_polygonRadius
import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
import ru.dbotthepony.kbox2d.dynamics.B2Fixture
import ru.dbotthepony.kstarbound.client.ClientWorld
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kvector.util2d.AABB
@ -72,6 +78,32 @@ abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : Move
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
@ -131,6 +163,8 @@ abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : Move
protected abstract fun canUnDuck(): Boolean
protected var previousVelocity = Vector2d.ZERO
protected open fun thinkMovement(delta: Double) {
if (onGround && !isDucked) {
when (moveDirection) {
@ -151,6 +185,54 @@ abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : Move
body.linearVelocity += -delta * 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 = -delta * 18.0 * stepSize * world.gravity.y)
}
}
}
Move.MOVE_RIGHT -> {
@ -166,9 +248,59 @@ abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : Move
body.linearVelocity += -delta * 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 = -delta * 18.0 * stepSize * world.gravity.y)
}
}
}
}
previousVelocity = body.linearVelocity
if (jumpRequested) {
jumpRequested = false
nextJump = world.timer + 0.1
@ -195,9 +327,15 @@ abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : Move
if (wantsToDuck && onGround) {
isDucked = true
recreateBodyFixture()
recreateSensors()
body.isAwake = true
} else if (isDucked) {
if (canUnDuck()) {
isDucked = false
recreateBodyFixture()
recreateSensors()
body.isAwake = true
}
}
}

View File

@ -9,44 +9,32 @@ import ru.dbotthepony.kvector.vector.ndouble.Vector2d
class PlayerMovementController(entity: PlayerEntity) : WalkableMovementController<PlayerEntity>(entity) {
public override var moveDirection = Move.STAND_STILL
private var bodyFixture: B2Fixture
override var isDucked: Boolean = false
set(value) {
if (value == field)
return
override fun recreateBodyFixture() {
bodyFixture?.destroy()
field = value
bodyFixture.destroy()
if (value) {
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,
))
}
body.isAwake = true
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,
))
}
override fun canUnDuck(): Boolean {
return world.isSpaceEmptyFromTiles(STANDING_AABB + position)
}
init {
bodyFixture = body.createFixture(FixtureDef(
shape = STANDING,
friction = 0.4,
density = 1.9,
))
recreateBodyFixture()
recreateSensors()
}
override fun canUnDuck(): Boolean {
return world.isSpaceEmptyFromTiles(STANDING_AABB + position)
}
companion object {