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
src
kbox2d/kotlin/ru/dbotthepony/kbox2d
main/kotlin/ru/dbotthepony/kstarbound

View File

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

View File

@ -1,3 +1,6 @@
@file:Suppress("unused")
package ru.dbotthepony.kbox2d.api package ru.dbotthepony.kbox2d.api
import ru.dbotthepony.kvector.api.concrete.IMatrix2d 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( class Transform(
position: Vector2d, position: Vector2d,
val rotation: Rotation, 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 * Shapes are defined with respect to the body origin, which may
* no coincide with the center of mass. However, to support dynamics * no coincide with the center of mass. However, to support dynamics
* we must interpolate the center of mass position. * we must interpolate the center of mass position.
*/ */
class Sweep { class Sweep {
var localCenter: Vector2d = Vector2d.ZERO ///< local center of mass position /**
* local center of mass position
*/
var localCenter: Vector2d = Vector2d.ZERO
set(value) { set(value) {
if (!value.isFinite) { if (!value.isFinite) {
throw IllegalArgumentException("Tried to set illegal local center $value") throw IllegalArgumentException("Tried to set illegal local center $value")
@ -215,7 +224,10 @@ class Sweep {
field = value field = value
} }
var c0: Vector2d = Vector2d.ZERO ///< center world positions /**
* center world positions
*/
var c0: Vector2d = Vector2d.ZERO
set(value) { set(value) {
if (!value.isFinite) { if (!value.isFinite) {
throw IllegalArgumentException("Tried to set illegal center world position $value") throw IllegalArgumentException("Tried to set illegal center world position $value")
@ -224,7 +236,10 @@ class Sweep {
field = value field = value
} }
var c: Vector2d = Vector2d.ZERO ///< center world positions /**
* center world positions
*/
var c: Vector2d = Vector2d.ZERO
set(value) { set(value) {
if (!value.isFinite) { if (!value.isFinite) {
throw IllegalArgumentException("Tried to set illegal center world position $value") throw IllegalArgumentException("Tried to set illegal center world position $value")
@ -233,7 +248,13 @@ class Sweep {
field = value 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) { set(value) {
if (!value.isFinite()) { if (!value.isFinite()) {
throw IllegalArgumentException("Tried to set illegal non finite $value") throw IllegalArgumentException("Tried to set illegal non finite $value")
@ -246,7 +267,10 @@ class Sweep {
field = value field = value
} }
var a: Double = 0.0 ///< world angles /**
* world angles
*/
var a: Double = 0.0
set(value) { set(value) {
if (!value.isFinite()) { if (!value.isFinite()) {
throw IllegalArgumentException("Tried to set illegal non finite $value") throw IllegalArgumentException("Tried to set illegal non finite $value")
@ -259,8 +283,13 @@ class Sweep {
field = value field = value
} }
/// Fraction of the current time step in the range [0,1] var oldAngles by this::a0
/// c0 and a0 are the positions at alpha0. 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 var alpha0: Double = 0.0
set(value) { set(value) {
if (!value.isFinite()) { if (!value.isFinite()) {
@ -276,24 +305,30 @@ class Sweep {
/** Get the interpolated transform at a specific time. /** Get the interpolated transform at a specific time.
* @param transform the output transform * @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/ * https://fgiesen.wordpress.com/2012/08/15/linear-interpolation-past-present-and-future/
*/ */
fun getTransform(loadInto: Transform, beta: Double) { fun getTransform(transform: Transform, beta: Double) {
loadInto.position = c0 * (1.0 - beta) + c * beta transform.position = c0 * (1.0 - beta) + c * beta
val angle = (1.0 - beta) * a0 + beta * a val angle = (1.0 - beta) * a0 + beta * a
loadInto.rotation.set(angle) transform.rotation.set(angle)
loadInto.position -= loadInto.position * localCenter 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 { fun getTransform(beta: Double): Transform {
val v = Transform() val v = Transform()
getTransform(v, beta) getTransform(v, beta)
return v 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) { fun advance(alpha: Double) {
require(alpha < 1.0) { "Bad advance value $alpha" } require(alpha < 1.0) { "Bad advance value $alpha" }
@ -303,7 +338,9 @@ class Sweep {
alpha0 = alpha alpha0 = alpha
} }
/// Normalize the angles. /**
* Normalize the angles.
*/
fun normalize() { fun normalize() {
val d = floor(a0 / (PI * 2.0)) * 2.0 * PI val d = floor(a0 / (PI * 2.0)) * 2.0 * PI
a0 -= d 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.kbox2d.collision.shapes.PolygonShape
import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import ru.dbotthepony.kvector.vector.ndouble.times import ru.dbotthepony.kvector.vector.ndouble.times
import java.util.*
var b2_gjkCalls = 0 var b2_gjkCalls = 0
private set private set
@ -21,11 +22,11 @@ var b2_gjkMaxIters = 0
* A distance proxy is used by the GJK algorithm. * A distance proxy is used by the GJK algorithm.
* It encapsulates any shape. * It encapsulates any shape.
*/ */
class DistanceProxy { class DistanceProxy(shape: IShape<*>, index: Int) {
val vertices: List<Vector2d> val vertices: List<Vector2d>
val radius: Double val radius: Double
constructor(shape: IShape<*>, index: Int) { init {
when (shape.type) { when (shape.type) {
IShape.Type.CIRCLE -> { IShape.Type.CIRCLE -> {
val circle = shape as CircleShape val circle = shape as CircleShape
@ -41,7 +42,7 @@ class DistanceProxy {
IShape.Type.POLYGON -> { IShape.Type.POLYGON -> {
val polygon = shape as PolygonShape val polygon = shape as PolygonShape
vertices = polygon.vertices vertices = Collections.unmodifiableList(polygon.vertices)
radius = polygon.radius 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. * Get the supporting vertex index in the given direction.
*/ */

View File

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

View File

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

View File

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

View File

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

View File

@ -8,5 +8,5 @@ data class ClientSettings(
*/ */
var scale: Float = 2f, 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() val runtime = Runtime.getRuntime()
gl.font.render("FPS: ${(averageFramesPerSecond * 100f).toInt() / 100f}", scale = 0.4f) 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.glfwSwapBuffers(window)
GLFW.glfwPollEvents() GLFW.glfwPollEvents()

View File

@ -11,6 +11,7 @@ fun formatBytesShort(input: Long): String {
in 0 until KIBIBYTE -> "${input}b" 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 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 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" else -> "${input}b"
} }
} }

View File

@ -1,5 +1,11 @@
package ru.dbotthepony.kstarbound.world.entities 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.client.ClientWorld
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
@ -72,6 +78,32 @@ abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : Move
protected abstract val moveDirection: 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 var wantsToDuck = false
open var isDucked = false open var isDucked = false
protected set protected set
@ -131,6 +163,8 @@ abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : Move
protected abstract fun canUnDuck(): Boolean protected abstract fun canUnDuck(): Boolean
protected var previousVelocity = Vector2d.ZERO
protected open fun thinkMovement(delta: Double) { protected open fun thinkMovement(delta: Double) {
if (onGround && !isDucked) { if (onGround && !isDucked) {
when (moveDirection) { when (moveDirection) {
@ -151,6 +185,54 @@ abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : Move
body.linearVelocity += -delta * world.gravity * 2.0 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 -> { Move.MOVE_RIGHT -> {
@ -166,9 +248,59 @@ abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : Move
body.linearVelocity += -delta * world.gravity * 2.0 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) { if (jumpRequested) {
jumpRequested = false jumpRequested = false
nextJump = world.timer + 0.1 nextJump = world.timer + 0.1
@ -195,9 +327,15 @@ abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : Move
if (wantsToDuck && onGround) { if (wantsToDuck && onGround) {
isDucked = true isDucked = true
recreateBodyFixture()
recreateSensors()
body.isAwake = true
} else if (isDucked) { } else if (isDucked) {
if (canUnDuck()) { if (canUnDuck()) {
isDucked = false 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) { class PlayerMovementController(entity: PlayerEntity) : WalkableMovementController<PlayerEntity>(entity) {
public override var moveDirection = Move.STAND_STILL public override var moveDirection = Move.STAND_STILL
private var bodyFixture: B2Fixture
override var isDucked: Boolean = false override fun recreateBodyFixture() {
set(value) { bodyFixture?.destroy()
if (value == field)
return
field = value if (isDucked) {
bodyFixture = body.createFixture(FixtureDef(
bodyFixture.destroy() shape = DUCKING,
friction = 0.4,
if (value) { density = 1.9,
bodyFixture = body.createFixture(FixtureDef( ))
shape = DUCKING, } else {
friction = 0.4, bodyFixture = body.createFixture(FixtureDef(
density = 1.9, shape = STANDING,
)) friction = 0.4,
} else { density = 1.9,
bodyFixture = body.createFixture(FixtureDef( ))
shape = STANDING,
friction = 0.4,
density = 1.9,
))
}
body.isAwake = true
} }
override fun canUnDuck(): Boolean {
return world.isSpaceEmptyFromTiles(STANDING_AABB + position)
} }
init { init {
bodyFixture = body.createFixture(FixtureDef( recreateBodyFixture()
shape = STANDING, recreateSensors()
friction = 0.4, }
density = 1.9,
)) override fun canUnDuck(): Boolean {
return world.isSpaceEmptyFromTiles(STANDING_AABB + position)
} }
companion object { companion object {