From 4a02a0e0de0808c5e68856eaa3663c5b67ba13ab Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sun, 20 Feb 2022 17:20:42 +0700 Subject: [PATCH] Box2d integration test --- .../kotlin/ru/dbotthepony/kbox2d/api/Body.kt | 10 +- .../kotlin/ru/dbotthepony/kbox2d/api/Shape.kt | 9 +- .../kbox2d/collision/shapes/ChainShape.kt | 21 + .../kbox2d/collision/shapes/CircleShape.kt | 7 + .../kbox2d/collision/shapes/EdgeShape.kt | 15 + .../kbox2d/collision/shapes/PolygonShape.kt | 18 + .../ru/dbotthepony/kbox2d/dynamics/B2Body.kt | 74 ++- .../dbotthepony/kbox2d/dynamics/B2Fixture.kt | 13 + .../kbox2d/dynamics/internal/Island.kt | 2 +- .../ru/dbotthepony/kvector/util2d/AABB.kt | 2 + .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 480 ++---------------- .../kstarbound/client/ClientWorld.kt | 6 + .../kstarbound/client/StarboundClient.kt | 3 +- .../client/render/EntityRenderer.kt | 2 +- .../ru/dbotthepony/kstarbound/world/Chunk.kt | 61 ++- .../ru/dbotthepony/kstarbound/world/World.kt | 21 +- .../kstarbound/world/entities/AliveEntity.kt | 149 ++---- .../kstarbound/world/entities/Entity.kt | 65 ++- .../world/entities/MovementController.kt | 144 ++---- .../kstarbound/world/entities/PlayerEntity.kt | 51 +- .../kstarbound/world/entities/Projectile.kt | 2 +- 21 files changed, 463 insertions(+), 692 deletions(-) diff --git a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/api/Body.kt b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/api/Body.kt index 24a17738..f1ece611 100644 --- a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/api/Body.kt +++ b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/api/Body.kt @@ -2,10 +2,12 @@ package ru.dbotthepony.kbox2d.api import ru.dbotthepony.kvector.vector.ndouble.Vector2d -/// The body type. -/// static: zero mass, zero velocity, may be manually moved -/// kinematic: zero mass, non-zero velocity set by user, moved by solver -/// dynamic: positive mass, non-zero velocity determined by forces, moved by solver +/** + * The body type. + * - static: zero mass, zero velocity, may be manually moved + * - kinematic: zero mass, non-zero velocity set by user, moved by solver + * - dynamic: positive mass, non-zero velocity determined by forces, moved by solver + */ enum class BodyType { STATIC, KINEMATIC, diff --git a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/api/Shape.kt b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/api/Shape.kt index 40a8a479..e3cb0ddf 100644 --- a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/api/Shape.kt +++ b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/api/Shape.kt @@ -58,12 +58,19 @@ interface IShape> { /** * Given a transform, compute the associated axis aligned bounding box for a child shape. - * @param aabb returns the axis aligned box. * @param xf the world transform of the shape. * @param childIndex the child shape */ fun computeAABB(transform: Transform, childIndex: Int): AABB + /** + * **KBox2D extension.** + * + * Computes full AABB in local coordinate space with zero rotation. + * @param childIndex the child shape + */ + fun computeAABB(childIndex: Int): AABB + /** * Compute the mass properties of this shape using its dimensions and density. * The inertia tensor is computed about the local origin. diff --git a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/ChainShape.kt b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/ChainShape.kt index a70e03bb..1fd4989f 100644 --- a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/ChainShape.kt +++ b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/ChainShape.kt @@ -172,6 +172,27 @@ class ChainShape : IShape { ) } + override fun computeAABB(childIndex: Int): AABB { + val i1 = childIndex + var i2 = childIndex + 1 + + if (i2 == vertices.size) { + i2 = 0 + } + + val v1 = vertices[i1] + val v2 = vertices[i2] + val lower = b2Min(v1, v2) + val upper = b2Max(v1, v2) + + val r = Vector2d(radius, radius) + + return AABB( + mins = lower - r, + maxs = upper + r + ) + } + override fun computeMass(density: Double): MassData { return MASS } diff --git a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/CircleShape.kt b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/CircleShape.kt index 0cd8bf7a..61c10497 100644 --- a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/CircleShape.kt +++ b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/CircleShape.kt @@ -67,6 +67,13 @@ class CircleShape( ) } + override fun computeAABB(childIndex: Int): AABB { + return AABB( + mins = Vector2d(p.x - radius, p.y - radius), + maxs = Vector2d(p.x + radius, p.y + radius) + ) + } + override fun computeMass(density: Double): MassData { val mass = density * PI * radius * radius diff --git a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/EdgeShape.kt b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/EdgeShape.kt index 55795f0f..97012e06 100644 --- a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/EdgeShape.kt +++ b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/EdgeShape.kt @@ -105,6 +105,21 @@ class EdgeShape : IShape { ) } + override fun computeAABB(childIndex: Int): AABB { + val v1 = vertex1 + val v2 = vertex2 + + val lower = b2Min(v1, v2) + val upper = b2Max(v1, v2) + + val r = Vector2d(radius, radius) + + return AABB( + mins = lower - r, + maxs = upper + r + ) + } + override fun computeMass(density: Double): MassData { return MassData( center = 0.5 * (vertex1 + vertex2), diff --git a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/PolygonShape.kt b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/PolygonShape.kt index 4ad461e4..c89ddc84 100644 --- a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/PolygonShape.kt +++ b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/collision/shapes/PolygonShape.kt @@ -306,6 +306,24 @@ class PolygonShape : IShape { ) } + override fun computeAABB(childIndex: Int): AABB { + var lower = vertices[0] + var upper = lower + + for (i in 1 until vertices.size) { + val v = vertices[i] + lower = b2Min(lower, v) + upper = b2Max(upper, v) + } + + val r = Vector2d(radius, radius) + + return AABB( + mins = lower - r, + maxs = upper + r + ) + } + override fun computeMass(density: Double): MassData { // Polygon mass, centroid, and inertia. // Let rho be the polygon density in mass per unit area. diff --git a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/dynamics/B2Body.kt b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/dynamics/B2Body.kt index baf9f335..8bb9c75b 100644 --- a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/dynamics/B2Body.kt +++ b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/dynamics/B2Body.kt @@ -4,6 +4,7 @@ package ru.dbotthepony.kbox2d.dynamics import ru.dbotthepony.kbox2d.api.* +import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.ndouble.Vector2d class B2Body(def: BodyDef, world: B2World) { @@ -395,6 +396,7 @@ class B2Body(def: BodyDef, world: B2World) { } fixture.next = fixtureList + fixtureList?.prev = fixture fixtureList = fixture fixtureCount++ @@ -444,25 +446,17 @@ class B2Body(def: BodyDef, world: B2World) { require(fixture.body == this) { "$fixture does not belong to $this (belongs to ${fixture.body})" } check(fixtureCount > 0) { "Having no tracked fixtures, but $fixture belongs to us" } - var node: B2Fixture? = fixtureList - var found = false - var previous: B2Fixture? = null + val prev = fixture.prev + val next = fixture.next - while (node != null) { - if (node == fixture) { - // TODO: Это должно работать - previous?.next = node.next - found = true - break - } else { - previous = node - node = node.next - } + prev?.next = next + next?.prev = prev + + if (fixture == fixtureList) { + fixtureList = next ?: prev } - check(found) { "Can't find $fixture in linked list of fixtures" } - - val density = node!!.density + val density = fixture.density // Destroy any contacts associated with the fixture. var edge = contactEdge @@ -556,6 +550,42 @@ class B2Body(def: BodyDef, world: B2World) { linearVelocity += b2Cross(angularVelocity, sweep.c - oldCenter) } + /** + * **KBox2D extension.** + * + * Computes full local space AABB of all fixtures attached, with zero rotation. + * If no fixtures are attached, returns zero sized AABB. + */ + val localSpaceAABB: AABB get() { + var combined = AABB.ZERO + + for (fixture in fixtureIterator) { + for (i in 0 until fixture.shape.childCount) { + combined = combined.combine(fixture.shape.computeAABB(i)) + } + } + + return combined + } + + /** + * **KBox2D extension.** + * + * Computes full world space AABB of all fixtures attached, with zero rotation. + * If no fixtures are attached, returns zero sized AABB positioned at [position]. + */ + val worldSpaceAABB: AABB get() { + var combined = AABB(position, position) + + for (fixture in fixtureIterator) { + for (i in 0 until fixture.shape.childCount) { + combined = combined.combine(fixture.shape.computeAABB(xf, i)) + } + } + + return combined + } + /** * Get the mass data of the body. * @return a struct containing the mass, inertia and center of the body. @@ -743,7 +773,7 @@ class B2Body(def: BodyDef, world: B2World) { * @param point the world position of the point of application. * @param wake also wake up the body */ - fun applyForce(force: Vector2d, point: Vector2d, wake: Boolean) { + fun applyForce(force: Vector2d, point: Vector2d, wake: Boolean = true) { if (type != BodyType.DYNAMIC) return @@ -762,7 +792,7 @@ class B2Body(def: BodyDef, world: B2World) { * @param force the world force vector, usually in Newtons (N). * @param wake also wake up the body */ - fun applyForceToCenter(force: Vector2d, wake: Boolean) { + fun applyForceToCenter(force: Vector2d, wake: Boolean = true) { if (type != BodyType.DYNAMIC) return @@ -781,7 +811,7 @@ class B2Body(def: BodyDef, world: B2World) { * @param torque about the z-axis (out of the screen), usually in N-m. * @param wake also wake up the body */ - fun applyTorque(torque: Double, wake: Boolean) { + fun applyTorque(torque: Double, wake: Boolean = true) { if (type != BodyType.DYNAMIC) return @@ -801,7 +831,7 @@ class B2Body(def: BodyDef, world: B2World) { * @param point the world position of the point of application. * @param wake also wake up the body */ - fun applyLinearImpulse(impulse: Vector2d, point: Vector2d, wake: Boolean) { + fun applyLinearImpulse(impulse: Vector2d, point: Vector2d, wake: Boolean = true) { if (type != BodyType.DYNAMIC) return @@ -819,7 +849,7 @@ class B2Body(def: BodyDef, world: B2World) { * @param impulse the world impulse vector, usually in N-seconds or kg-m/s. * @param wake also wake up the body */ - fun applyLinearImpulseToCenter(impulse: Vector2d, wake: Boolean) { + fun applyLinearImpulseToCenter(impulse: Vector2d, wake: Boolean = true) { if (type != BodyType.DYNAMIC) return @@ -836,7 +866,7 @@ class B2Body(def: BodyDef, world: B2World) { * @param impulse the angular impulse in units of kg*m*m/s * @param wake also wake up the body */ - fun applyAngularImpulse(impulse: Double, wake: Boolean) { + fun applyAngularImpulse(impulse: Double, wake: Boolean = true) { if (type != BodyType.DYNAMIC) return diff --git a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/dynamics/B2Fixture.kt b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/dynamics/B2Fixture.kt index 4571737a..3b8a869d 100644 --- a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/dynamics/B2Fixture.kt +++ b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/dynamics/B2Fixture.kt @@ -31,6 +31,14 @@ class B2Fixture( * @return the next shape. */ var next: B2Fixture? = null + internal set + + /** + * Get the previous fixture in the parent body's fixture list. + * @return the next shape. + */ + var prev: B2Fixture? = null + internal set /** * Get the user data that was assigned in the fixture definition. Use this to @@ -126,6 +134,11 @@ class B2Fixture( refilter() } + /** + * **KBox2D extension**. + * + * Convenience function for quickly destroying this fixture. + */ fun destroy() { checkNotNull(body) { "Already destroyed" }.destroyFixture(this) } diff --git a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/dynamics/internal/Island.kt b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/dynamics/internal/Island.kt index ea85b155..ed52b0bd 100644 --- a/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/dynamics/internal/Island.kt +++ b/src/kbox2d/kotlin/ru/dbotthepony/kbox2d/dynamics/internal/Island.kt @@ -191,7 +191,7 @@ internal class Island( body.sweep.c0 = body.sweep.c body.sweep.a0 = body.sweep.a - if (body.type == ru.dbotthepony.kbox2d.api.BodyType.DYNAMIC) { + if (body.type == BodyType.DYNAMIC) { // Integrate velocities. v += (gravity * body.gravityScale * body.mass + body.force) * body.invMass * h w += h * body.rotInertiaInv * body.torque diff --git a/src/kvector/kotlin/ru/dbotthepony/kvector/util2d/AABB.kt b/src/kvector/kotlin/ru/dbotthepony/kvector/util2d/AABB.kt index c690b5d1..f0707e81 100644 --- a/src/kvector/kotlin/ru/dbotthepony/kvector/util2d/AABB.kt +++ b/src/kvector/kotlin/ru/dbotthepony/kvector/util2d/AABB.kt @@ -368,6 +368,8 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) { Vector2d(x + width / 2.0, y + height / 2.0), ) } + + @JvmField val ZERO = AABB(Vector2d.ZERO, Vector2d.ZERO) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index dee34393..9fde86b2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -4,20 +4,19 @@ 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.shapes.ChainShape import ru.dbotthepony.kbox2d.collision.shapes.CircleShape import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape -import ru.dbotthepony.kbox2d.dynamics.B2World -import ru.dbotthepony.kbox2d.dynamics.B2Body -import ru.dbotthepony.kbox2d.dynamics.joint.MouseJoint import ru.dbotthepony.kstarbound.client.StarboundClient +import ru.dbotthepony.kstarbound.defs.TileDefinition +import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.Chunk +import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.entities.Move import ru.dbotthepony.kstarbound.world.entities.PlayerEntity +import ru.dbotthepony.kstarbound.world.entities.Projectile import ru.dbotthepony.kvector.vector.ndouble.Vector2d import java.io.File import java.util.* -import kotlin.collections.ArrayList private val LOGGER = LogManager.getLogger() @@ -32,380 +31,14 @@ fun main() { //return } - val world = B2World(Vector2d(y = -10.0)) - - val groundDef = BodyDef( - position = Vector2d(y = 0.0) - ) - - val ground = world.createBody(groundDef) - - //val groundPoly = PolygonShape() - //groundPoly.setAsBox(50.0, 10.0) - - val groundPoly = ChainShape() - groundPoly.createLoop(listOf( - Vector2d(-30.0, 10.0), - Vector2d(-25.0, 0.0), - Vector2d(25.0, 0.0), - Vector2d(30.0, 10.0), - Vector2d(30.0, -2.0), - Vector2d(-30.0, -2.0)).asReversed()) - - ground.createFixture(groundPoly, 0.0) - - val boxes = ArrayList() - - /*run { - val movingDef = BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(y = 4.0), - angle = PI / 4.0 - ) - val movingBody = world.createBody(movingDef) - - val dynamicBox = PolygonShape() - dynamicBox.setAsBox(1.0, 1.0) - - val fixtureDef = FixtureDef( - shape = dynamicBox, - density = 1.0, - friction = 0.3 - ) - - movingBody.createFixture(fixtureDef) - boxes.add(movingBody) - }*/ - - run { - if (true) - return@run - val movingDef = BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = -1.0, y = 6.0), - ) - - val movingBody = world.createBody(movingDef) - - val dynamicBox = PolygonShape() - dynamicBox.setAsBox(1.0, 1.0) - - val fixtureDef = FixtureDef( - shape = dynamicBox, - density = 1.0, - friction = 0.3 - ) - - movingBody.createFixture(fixtureDef) - boxes.add(movingBody) - } - - val rand = Random() - - val mouseJoints = ArrayList() - - run { - val stripes = 4 - - for (stripe in 0 until stripes) { - for (x in 0 .. (stripes - stripe)) { - val movingBody = world.createBody(BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = (-stripes + stripe) * 1.0 + x * 2.1, y = 8.0 + stripe * 2.1), - gravityScale = 1.1 - )) - - val dynamicBox: IShape<*> - - if (false) { - dynamicBox = PolygonShape() - dynamicBox.setAsBox(1.0, 1.0) - } else { - dynamicBox = CircleShape(1.0) - } - - movingBody.createFixture(FixtureDef( - shape = dynamicBox, - density = 1.0, - friction = 0.3 - )) - - /*for (otherBody in boxes) { - val def = DistanceJointDef(movingBody, otherBody, movingBody.position, otherBody.position) - world.createJoint(def) - }*/ - - /*val mouse = world.createJoint(MouseJointDef( - Vector2d(y = 10.0, x = 10.0), - bodyB = movingBody, - maxForce = 1000.0 - ).linearStiffness(50.0, 0.7, bodyB = movingBody, bodyA = null)) - - mouseJoints.add(mouse as MouseJoint)*/ - - boxes.add(movingBody) - } - } - } - - run { - if (true) - return@run - val movingDef1 = BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = -4.0, y = 4.0), - gravityScale = 1.1 - ) - - val movingBody1 = world.createBody(movingDef1) - - val dynamicBox = PolygonShape() - dynamicBox.setAsBox(1.0, 1.0) - - val fixtureDef1 = FixtureDef( - shape = dynamicBox, - density = 1.0, - friction = 0.3 - ) - - movingBody1.createFixture(fixtureDef1) - - boxes.add(movingBody1) - - val movingDef2 = BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = 4.0, y = 6.0), - gravityScale = 1.1 - ) - - val movingBody2 = world.createBody(movingDef2) - - val fixtureDef2 = FixtureDef( - shape = dynamicBox, - density = 1.0, - friction = 0.3 - ) - - movingBody2.createFixture(fixtureDef2) - boxes.add(movingBody2) - - val groundAnchor1 = movingBody1.position + Vector2d(y = 10.0) - val groundAnchor2 = movingBody2.position + Vector2d(y = 12.0) - val def = PulleyJointDef(movingBody1, movingBody2, groundAnchor1, groundAnchor2, movingBody1.position, movingBody2.position, 1.0) - - world.createJoint(def) - } - - run { - val base = world.createBody(BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = 0.0, y = 16.0), - )) - - val wheel1 = world.createBody(BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = -2.0, y = 15.0), - )) - - val wheel2 = world.createBody(BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = 2.0, y = 15.0), - )) - - base.createFixture(FixtureDef( - shape = PolygonShape().also { it.setAsBox(2.0, 0.5) }, - density = 1.0 - )) - - wheel1.createFixture(FixtureDef( - shape = CircleShape(0.5), - density = 1.0, - friction = 1.5 - )) - - wheel2.createFixture(FixtureDef( - shape = CircleShape(0.5), - density = 1.0, - friction = 1.5 - )) - - world.createJoint(WheelJointDef( - bodyA = base, - bodyB = wheel1, - anchor = Vector2d(x = -2.0, y = 15.0), - axis = Vector2d.POSITIVE_Y, - enableLimit = true, - upperTranslation = 0.25, - lowerTranslation = -0.25, - )) - - world.createJoint(WheelJointDef( - bodyA = base, - bodyB = wheel2, - anchor = Vector2d(x = 2.0, y = 15.0), - axis = Vector2d.POSITIVE_Y, - enableLimit = true, - upperTranslation = 0.25, - lowerTranslation = -0.25, - )) - - base.setTransform(base.position, 0.56) - } - - run { - val circleNail = world.createBody(BodyDef( - type = BodyType.STATIC, - position = Vector2d(x = -12.0, y = 4.0), - )) - - val circleBody = world.createBody(BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = -12.0, y = 4.0), - )) - - circleBody.createFixture(FixtureDef( - shape = CircleShape(2.0), - density = 1.0, - friction = 0.3 - )) - - val circleNailJoint = world.createJoint(RevoluteJointDef( - circleNail, - circleBody, - Vector2d(x = -12.0, y = 4.0) - )) - - val boxNail = world.createBody(BodyDef( - type = BodyType.STATIC, - position = Vector2d(x = 2.0, y = 4.0), - )) - - val boxBody = world.createBody(BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = 2.0, y = 4.0), - )) - - boxBody.createFixture(FixtureDef( - shape = PolygonShape().also { it.setAsBox(3.5, 0.5) }, - density = 1.0, - friction = 0.3 - )) - - val boxNailJoint = world.createJoint(RevoluteJointDef( - boxNail, - boxBody, - Vector2d(x = 2.0, y = 4.0) - )) - - val gearJoint = world.createJoint(GearJointDef( - circleBody, - boxBody, - circleNailJoint, - boxNailJoint, - 1.0 - )) - } - - run { - val boxA = world.createBody(BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = -2.0, y = 6.0), - )) - - val boxB = world.createBody(BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = 0.0, y = 6.0), - )) - - val boxC = world.createBody(BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = 2.0, y = 6.0), - )) - - val dynamicBox = PolygonShape() - dynamicBox.setAsBox(1.0, 1.0) - - val fixtureDef = FixtureDef( - shape = dynamicBox, - density = 1.0, - friction = 0.3 - ) - - boxA.createFixture(fixtureDef) - boxB.createFixture(fixtureDef) - boxC.createFixture(fixtureDef) - - world.createJoint(WeldJointDef( - bodyA = boxA, bodyB = boxB, (boxA.position + boxB.position) * 0.5 - ).linearStiffness(5.0, 0.5, boxA, boxB)) - - world.createJoint(WeldJointDef( - bodyA = boxB, bodyB = boxC, (boxB.position + boxC.position) * 0.5 - ).linearStiffness(5.0, 0.5, boxB, boxC)) - } - - run { - val boxA = world.createBody(BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = -2.0, y = 8.0), - )) - - val dynamicBox = PolygonShape() - dynamicBox.setAsBox(1.0, 1.0) - - val fixtureDef = FixtureDef( - shape = dynamicBox, - density = 1.0, - friction = 0.3 - ) - - boxA.createFixture(fixtureDef) - - world.createJoint(FrictionJointDef( - bodyA = boxA, - bodyB = ground, - Vector2d(x = -2.0, y = 8.0), - maxForce = 1.0, - maxTorque = 10.0, - collideConnected = true - )) - } - - run { - val boxA = world.createBody(BodyDef( - type = BodyType.DYNAMIC, - position = Vector2d(x = -2.0, y = 8.0), - )) - - val dynamicBox = PolygonShape() - dynamicBox.setAsBox(1.0, 1.0) - - val fixtureDef = FixtureDef( - shape = dynamicBox, - density = 1.0, - friction = 0.3 - ) - - boxA.createFixture(fixtureDef) - - world.createJoint(MotorJointDef( - bodyA = boxA, - bodyB = ground, - maxForce = 1000.0, - collideConnected = true, - )) - } - - val timeStep = 1.0 / 144.0 - val client = StarboundClient() //Starbound.addFilePath(File("./unpacked_assets/")) Starbound.addPakPath(File("J:\\SteamLibrary\\steamapps\\common\\Starbound\\assets\\packed.pak")) - /*Starbound.initializeGame { finished, replaceStatus, status -> + Starbound.initializeGame { finished, replaceStatus, status -> client.putDebugLog(status, replaceStatus) - }*/ + } client.onTermination { Starbound.terminateLoading = true @@ -416,14 +49,14 @@ fun main() { val ent = PlayerEntity(client.world!!) Starbound.onInitialize { - /*chunkA = client.world!!.computeIfAbsent(ChunkPos(0, 0)).chunk + chunkA = client.world!!.computeIfAbsent(ChunkPos(0, 0)).chunk val chunkB = client.world!!.computeIfAbsent(ChunkPos(-1, 0)).chunk val chunkC = client.world!!.computeIfAbsent(ChunkPos(-2, 0)).chunk val tile = Starbound.getTileDefinition("alienrock") - for (x in -48 .. 48) { - for (y in 0 .. 20) { + for (x in -6 .. 6) { + for (y in 0 .. 4) { val chnk = client.world!!.computeIfAbsent(ChunkPos(x, y)) if (y == 0) { @@ -486,19 +119,48 @@ fun main() { chunkA!!.foreground[rand.nextInt(0, CHUNK_SIZE_FF), rand.nextInt(0, CHUNK_SIZE_FF)] = tile }*/ - ent.movement.dropToFloor() + // ent.movement.dropToFloor() for ((i, proj) in Starbound.projectilesAccess.values.withIndex()) { val projEnt = Projectile(client.world!!, proj) - projEnt.pos = Vector2d(i * 2.0, 10.0) + projEnt.position = Vector2d(i * 2.0, 10.0) projEnt.spawn() - }*/ + } + + run { + val stripes = 4 + + for (stripe in 0 until stripes) { + for (x in 0 .. (stripes - stripe)) { + val movingBody = client.world!!.physics.createBody(BodyDef( + type = BodyType.DYNAMIC, + position = Vector2d(x = (-stripes + stripe) * 1.0 + x * 2.1, y = 8.0 + stripe * 2.1), + gravityScale = 1.1 + )) + + val dynamicBox: IShape<*> + + if (false) { + dynamicBox = PolygonShape() + dynamicBox.setAsBox(1.0, 1.0) + } else { + dynamicBox = CircleShape(1.0) + } + + movingBody.createFixture(FixtureDef( + shape = dynamicBox, + density = 1.0, + friction = 0.3 + )) + } + } + } } - ent.pos += Vector2d(y = 36.0, x = -10.0) + ent.position += Vector2d(y = 36.0, x = -10.0) client.onDrawGUI { - client.gl.font.render("${ent.pos}", y = 100f, scale = 0.25f) + client.gl.font.render("${ent.position}", y = 100f, scale = 0.25f) client.gl.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f) } @@ -509,65 +171,13 @@ fun main() { client.camera.pos.y = 10f - world.debugDraw = client.gl.box2dRenderer - client.gl.box2dRenderer.drawShapes = true client.gl.box2dRenderer.drawPairs = false client.gl.box2dRenderer.drawAABB = false client.gl.box2dRenderer.drawJoints = false client.onPostDrawWorld { - world.debugDraw() - client.gl.quadWireframe { - var pos: Vector2d - - /*for (box in boxes) { - pos = box.position - - it.quadRotated( - -1f, - -1f, - 1f, - 1f, - pos.x.toFloat(), - pos.y.toFloat(), - box.angle, - ) - }*/ - - /*for (box in boxes) { - val broad = world.contactManager.broadPhase - val f = box.fixtureList!! - val proxy = f.proxies[0].proxyId - val aabb = broad.getFatAABB(proxy) - - it.quad( - aabb, - ) - }*/ - - /*pos = ground.position - - it.quad( - (pos.x - 50f).toFloat(), - (pos.y - 10f).toFloat(), - (pos.x + 50f).toFloat(), - (pos.y + 10f).toFloat(), - )*/ - } - - for (joint in mouseJoints) { - joint.targetA = Vector2d(rand.nextDouble() * 20.0, rand.nextDouble() * 20.0) - - if (rand.nextDouble() < 0.01) { - world.destroyJoint(joint) - mouseJoints.remove(joint) - break - } - } - - world.step(timeStep, 6, 4) } ent.spawn() @@ -576,8 +186,8 @@ fun main() { Starbound.pollCallbacks() //ent.think(client.frameRenderTime) - //client.camera.pos.x = ent.pos.x.toFloat() - //client.camera.pos.y = ent.pos.y.toFloat() + client.camera.pos.x = ent.position.x.toFloat() + client.camera.pos.y = ent.position.y.toFloat() //println(client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt index c9b95721..1cedfdfd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt @@ -6,6 +6,10 @@ import ru.dbotthepony.kstarbound.world.* import ru.dbotthepony.kvector.util2d.AABB class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World(seed) { + init { + physics.debugDraw = client.gl.box2dRenderer + } + override fun chunkFactory(pos: ChunkPos): ClientChunk { return ClientChunk( world = this, @@ -33,6 +37,8 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World, This : Chunk, This : Chunk() + + fun bakeCollisions() { + if (collisionChangeset == changeset) + return + collisionChangeset = changeset collisionCache.clear() + for (box in collisionBoxes) { + body.destroyFixture(box) + } + + collisionBoxes.clear() + val xAdd = pos.x * CHUNK_SIZEd val yAdd = pos.y * CHUNK_SIZEd @@ -393,12 +424,17 @@ abstract class Chunk, This : Chunk, This : Chunk, This : Chunk, This : Chunk, ChunkType : Chunk>(val seed: Long = 0L) { protected val chunkMap = HashMap>() + + /** + * Chunks, which have their collision mesh changed + */ + val dirtyPhysicsChunks = HashSet() + protected var lastAccessedChunk: IMutableWorldChunkTuple? = null + val physics = B2World(Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION)) + /** * Таймер этого мира, в секундах. * @@ -124,6 +133,14 @@ abstract class World, ChunkType : Chunk 0.0) { "Tried to update $this by $delta seconds" } + for (chunk in dirtyPhysicsChunks) { + chunk.bakeCollisions() + } + + dirtyPhysicsChunks.clear() + + physics.step(delta, 6, 4) + timer += delta thinkInner(delta) } @@ -145,7 +162,7 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk() for (ent in orphanedEntities) { - val cPos = ChunkPos.fromTilePosition(ent.pos) + val cPos = ChunkPos.fromTilePosition(ent.position) if (cPos == pos) { orphanedInThisChunk.add(ent) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt index 8926172b..979b6ce1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt @@ -14,16 +14,6 @@ enum class Move { } interface IWalkableEntity : IEntity { - /** - * AABB сущности, которая стоит - */ - val standingAABB: AABB - - /** - * AABB сущности, которая присела - */ - val duckingAABB: AABB - /** * Максимальная скорость передвижения этого AliveMovementController в Starbound Units/секунда * @@ -75,54 +65,48 @@ interface IWalkableEntity : IEntity { * Базовый абстрактный класс, реализующий сущность, которая ходит по земле */ abstract class WalkableMovementController(entity: T) : MovementController(entity) { + init { + body.isFixedRotation = true + } + protected abstract val moveDirection: Move - override val collisionResolution = CollisionResolution.SLIDE var wantsToDuck = false - var isDucked = false + open var isDucked = false protected set - override val currentAABB: AABB - get() { - if (isDucked) { - return entity.duckingAABB - } - - return entity.standingAABB - } - override fun thinkPhysics(delta: Double) { super.thinkPhysics(delta) thinkMovement(delta) } /** - * Смотрим [IWalkableEntity.topSpeed] + * See [IWalkableEntity.topSpeed] */ open val topSpeed by entity::topSpeed /** - * Смотрим [IWalkableEntity.moveSpeed] + * See [IWalkableEntity.moveSpeed] */ open val moveSpeed by entity::moveSpeed /** - * Смотрим [IWalkableEntity.freeFallMoveSpeed] + * See [IWalkableEntity.freeFallMoveSpeed] */ open val freeFallMoveSpeed by entity::freeFallMoveSpeed /** - * Смотрим [IWalkableEntity.brakeForce] + * See [IWalkableEntity.brakeForce] */ open val brakeForce by entity::brakeForce /** - * Смотрим [IWalkableEntity.stepSize] + * See [IWalkableEntity.stepSize] */ open val stepSize by entity::stepSize /** - * Смотрим [IWalkableEntity.jumpForce] + * See [IWalkableEntity.jumpForce] */ open val jumpForce by entity::jumpForce @@ -144,115 +128,64 @@ abstract class WalkableMovementController(entity: T) : Move } } + protected abstract fun canUnDuck(): Boolean + protected open fun thinkMovement(delta: Double) { - if (onGround || !affectedByGravity) { - var add = Vector2d.ZERO + if (onGround && !isDucked) { + when (moveDirection) { + Move.STAND_STILL -> { + body.linearVelocity += Vector2d(x = -body.linearVelocity.x * delta * brakeForce) + } - if (isDucked) { - thinkFriction(delta * brakeForce) - } else if (velocity.y.absoluteValue < 1 && !jumpRequested) { - when (moveDirection) { - Move.STAND_STILL -> { - thinkFriction(delta * brakeForce) - add = Vector2d.ZERO + Move.MOVE_LEFT -> { + if (body.linearVelocity.x > 0.0) { + body.linearVelocity += Vector2d(x = -body.linearVelocity.x * delta * brakeForce) } - Move.MOVE_LEFT -> { - if (velocity.x > 0.0) { - thinkFriction(delta * brakeForce) - } - - add = Vector2d(x = -delta * moveSpeed) - } - - Move.MOVE_RIGHT -> { - if (velocity.x < 0.0) { - thinkFriction(delta * brakeForce) - } - - add = Vector2d(x = delta * moveSpeed) + if (body.linearVelocity.x > -topSpeed) { + body.linearVelocity += Vector2d(x = -moveSpeed * delta) } } - } - if (add != Vector2d.ZERO) { - if (isSpaceOpen(add, delta)) { - velocity += add - - // спускание с "лестницы" - val sweep = sweepAbsolute(pos + velocity * delta * 4.0, Vector2d(y = -stepSize), delta) - - if (sweep.hitAnything && sweep.hitPosition.y < -0.1) { - dropToFloor() - thinkFriction(delta * 6.0) + Move.MOVE_RIGHT -> { + if (body.linearVelocity.x < 0.0) { + body.linearVelocity += Vector2d(x = -body.linearVelocity.x * delta * brakeForce) } - if (world is ClientWorld && world.client.settings.debugCollisions) { - world.client.onPostDrawWorldOnce { - world.client.gl.quadWireframe(worldAABB + velocity * delta * 4.0 + sweep.hitPosition, Color.RED) - } - } - } else { - // подъем по "лестнице" - val sweep = sweepRelative(Vector2d(y = stepSize), delta) - - if (!sweep.hitAnything) { - val sweep2 = sweepAbsolute(pos + sweep.hitPosition, Vector2d(x = -0.1 + add.x), delta) - - if (!sweep2.hitAnything) { - pos += sweep.hitPosition + sweep2.hitPosition - thinkFriction(delta * 64.0) - dropToFloor() - } - - if (world is ClientWorld && world.client.settings.debugCollisions) { - world.client.onPostDrawWorldOnce { - world.client.gl.quadWireframe(worldAABB + sweep.hitPosition + sweep2.hitPosition, Color.GREEN) - } - } - } - - if (world is ClientWorld && world.client.settings.debugCollisions) { - world.client.onPostDrawWorldOnce { - world.client.gl.quadWireframe(worldAABB + sweep.hitPosition, Color.BLUE) - } + if (body.linearVelocity.x < topSpeed) { + body.linearVelocity += Vector2d(x = moveSpeed * delta) } } } - if (velocity.length > topSpeed) { - thinkFriction(delta * (velocity.length - topSpeed)) - } - if (jumpRequested) { jumpRequested = false - velocity += groundNormal * jumpForce - onGround = false + nextJump = world.timer + 0.1 + + body.linearVelocity += Vector2d(y = jumpForce) } - } else if (!onGround && affectedByGravity) { + } else if (!onGround && !isDucked && freeFallMoveSpeed != 0.0) { when (moveDirection) { - Move.STAND_STILL -> {} + Move.STAND_STILL -> { + // do nothing + } Move.MOVE_LEFT -> { - val add = Vector2d(x = -delta * freeFallMoveSpeed) - - if (isSpaceOpen(add, delta)) - velocity += add + body.linearVelocity += Vector2d(x = -freeFallMoveSpeed * delta) } Move.MOVE_RIGHT -> { - val add = Vector2d(x = delta * freeFallMoveSpeed) - - if (isSpaceOpen(add, delta)) - velocity += add + body.linearVelocity += Vector2d(x = freeFallMoveSpeed * delta) } } + } else if (onGround && isDucked) { + body.linearVelocity += Vector2d(x = -body.linearVelocity.x * delta * brakeForce) } if (wantsToDuck && onGround) { isDucked = true } else if (isDucked) { - if (world.isSpaceEmptyFromTiles(entity.standingAABB + pos)) { + if (canUnDuck()) { isDucked = false } } @@ -270,7 +203,7 @@ abstract class AliveWalkingEntity(world: World<*, *>) : AliveEntity(world), IWal override val topSpeed = 20.0 override val moveSpeed = 64.0 override val freeFallMoveSpeed = 8.0 - override val brakeForce = 32.0 + override val brakeForce = 16.0 override val jumpForce = 20.0 override val stepSize = 1.1 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 bf5b5b23..052f5fbd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt @@ -9,12 +9,57 @@ import ru.dbotthepony.kvector.vector.ndouble.Vector2d * Интерфейс служит лишь для убирания жёсткой зависимости от класса Entity */ interface IEntity { + /** + * The world this entity in, never changes. + */ val world: World<*, *> + + /** + * The chunk this entity currently in, it is automatically updated on each change of [position] + */ var chunk: Chunk<*, *>? - var pos: Vector2d - var rotation: Double + + /** + * Current entity position. If entity is logical, this can only be changed + * by external means, otherwise it always stands where it is. + * + * If entity is physical, then movement controller will update this on each physics step + * to match position of physical body. + * + * Setting this value will update [chunk] immediately, if [isSpawned] is true and [isRemoved] is false. + */ + var position: Vector2d + + /** + * This entity's angle in radians. + * + * Logical entities never rotate. + * + * Alive entities usually don't rotate. + * + * If entity is physical, this value is updated by movement controller on + * each physics step to match angle of physical body. + */ + var angle: Double + + /** + * 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. + */ val movement: MovementController<*> + + /** + * Whenever is this entity spawned in world ([spawn] called). + * Doesn't mean entity still exists in world, check it with [isRemoved] + */ val isSpawned: Boolean + + /** + * Whenever is this entity was removed from world ([remove] called). + */ val isRemoved: Boolean fun spawn() @@ -33,11 +78,15 @@ abstract class Entity(override val world: World<*, *>) : IEntity { throw IllegalStateException("Trying to set chunk this entity belong to before spawning in world") } + if (isRemoved) { + throw IllegalStateException("This entity was removed") + } + if (value == field) { return } - val chunkPos = ChunkPos.fromTilePosition(pos) + val chunkPos = ChunkPos.fromTilePosition(position) if (value != null && chunkPos != value.pos) { throw IllegalStateException("Set proper position before setting chunk this Entity belongs to") @@ -57,7 +106,7 @@ abstract class Entity(override val world: World<*, *>) : IEntity { } } - override var pos = Vector2d() + override var position = Vector2d() set(value) { if (field == value) return @@ -65,7 +114,9 @@ abstract class Entity(override val world: World<*, *>) : IEntity { val old = field field = value - if (isSpawned) { + movement.notifyPositionChanged() + + if (isSpawned && !isRemoved) { val oldChunkPos = ChunkPos.fromTilePosition(old) val newChunkPos = ChunkPos.fromTilePosition(value) @@ -75,7 +126,7 @@ abstract class Entity(override val world: World<*, *>) : IEntity { } } - override var rotation: Double = 0.0 + override var angle: Double = 0.0 final override var isSpawned = false private set @@ -88,7 +139,7 @@ abstract class Entity(override val world: World<*, *>) : IEntity { isSpawned = true world.entities.add(this) - chunk = world.getChunk(ChunkPos.fromTilePosition(pos))?.chunk + chunk = world.getChunk(ChunkPos.fromTilePosition(position))?.chunk if (chunk == null) { world.orphanedEntities.add(this) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt index 358f56a0..b778af59 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.world.entities -import ru.dbotthepony.kstarbound.math.lerp +import ru.dbotthepony.kbox2d.api.BodyDef +import ru.dbotthepony.kbox2d.api.BodyType import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.ndouble.Vector2d @@ -13,129 +14,76 @@ enum class CollisionResolution { abstract class MovementController(val entity: T) { val world = entity.world - var pos by entity::pos - var rotation by entity::rotation + open var position by entity::position + open var angle by entity::angle - open val mass = 1.0 - - // наследуемые свойства - open val affectedByGravity = true - open val collisionResolution = CollisionResolution.STOP - - var velocity = Vector2d() - - /** - * Касается ли AABB сущности земли - * - * Данный флаг выставляется при обработке скорости, если данный флаг не будет выставлен - * правильно, то сущность будет иметь очень плохое движение в стороны - * - * Так же от него зависит то, может ли сущность двигаться, если она не парит - * - * Если сущность касается земли, то на неё не действует гравитация - */ - var onGround = false - protected set(value) { - field = value - nextOnGroundUpdate = world.timer + 0.1 - } - - /** - * Текущий AABB этого Movement Controller - * - * Это может быть как и статичное значение (для данного типа сущности), так и динамичное - * (к примеру, присевший игрок) - * - * Данное значение, хоть и является val, НЕ ЯВЛЯЕТСЯ КОНСТАНТОЙ! - */ - abstract val currentAABB: AABB - - /** - * Текущий AABB в отображении на мировые координаты - */ - val worldAABB: AABB get() = currentAABB + pos - - protected var nextOnGroundUpdate = 0.0 - - protected fun sweepRelative(velocity: Vector2d, delta: Double, collisionResolution: CollisionResolution = this.collisionResolution) = world.sweep(worldAABB, velocity, collisionResolution, delta) - protected fun sweepAbsolute(from: Vector2d, velocity: Vector2d, delta: Double, collisionResolution: CollisionResolution = this.collisionResolution) = world.sweep(currentAABB + from, velocity, collisionResolution, delta) - protected fun isSpaceOpen(relative: Vector2d, delta: Double) = !sweepRelative(relative, delta).hitAnything - - fun dropToFloor() { - val sweep = sweepRelative(DROP_TO_FLOOR, 1.0, CollisionResolution.STOP) - - if (!sweep.hitAnything) - return - - pos += sweep.hitPosition + protected val body by lazy { + world.physics.createBody(BodyDef( + position = position, + angle = angle, + type = BodyType.DYNAMIC + )) } - var groundNormal = Vector2d.ZERO - protected set + open val velocity get() = body.linearVelocity - protected open fun propagateVelocity(delta: Double) { - if (velocity.length == 0.0) - return - - val sweep = sweepRelative(velocity * delta, delta) - this.velocity = sweep.hitPosition / delta - this.pos += this.velocity * delta - - if (nextOnGroundUpdate <= world.timer || !onGround) { - onGround = sweep.hitNormal.dot(world.gravity.normalized) <= -0.98 - groundNormal = sweep.hitNormal - - if (!onGround) { - val sweepGround = sweepRelative(world.gravity * delta, delta) - onGround = sweepGround.hitAnything && sweepGround.hitNormal.dot(world.gravity.normalized) <= -0.98 - groundNormal = sweepGround.hitNormal + /** + * Returns whenever are we contacting something below us + */ + open val onGround: Boolean get() { + for (contact in body.contactEdgeIterator) { + if (contact.contact.manifold.localNormal.dot(world.gravity.normalized) >= 0.97) { + return true } } + + return false } - protected open fun thinkGravity(delta: Double) { - velocity += world.gravity * delta - } - - protected open fun thinkFriction(delta: Double) { - velocity *= Vector2d(lerp(delta, 1.0, 0.01), 1.0) + /** + * World space AABB, by default returning combined AABB of physical body. + */ + open val worldAABB: AABB get() { + return body.worldSpaceAABB } open fun thinkPhysics(delta: Double) { - if (!onGround && affectedByGravity) - thinkGravity(delta) - - propagateVelocity(delta) - - if (affectedByGravity && onGround) - thinkFriction(delta) + mutePositionChanged = true + position = body.position + angle = body.angle + mutePositionChanged = false } protected open fun onTouchSurface(velocity: Vector2d, normal: Vector2d) { entity.onTouchSurface(velocity, normal) } - companion object { - private val DROP_TO_FLOOR = Vector2d(y = -1_000.0) + protected var mutePositionChanged = false + + open fun notifyPositionChanged() { + if (mutePositionChanged) { + return + } + + body.setTransform(entity.position, entity.angle) } } /** - * MovementController который ничего не делает (прям совсем) + * Movement controller for logical entities, which does nothing. */ -class DummyMovementController(entity: Entity) : MovementController(entity) { - override val currentAABB = DUMMY_AABB - override val affectedByGravity = false +class LogicalMovementController(entity: Entity) : MovementController(entity) { + override val worldAABB: AABB get() = AABB(position, position) - override fun propagateVelocity(delta: Double) { + // Dummies never touch anything, since they don't have Box2D body + override val onGround: Boolean = false + override val velocity: Vector2d = Vector2d.ZERO + + override fun thinkPhysics(delta: Double) { // no-op } - override fun thinkGravity(delta: Double) { - // no-op - } - - override fun thinkFriction(delta: Double) { + override fun notifyPositionChanged() { // no-op } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt index 4dd3a0ef..21375ece 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt @@ -1,19 +1,66 @@ package ru.dbotthepony.kstarbound.world.entities +import ru.dbotthepony.kbox2d.api.FixtureDef +import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape +import ru.dbotthepony.kbox2d.dynamics.B2Fixture import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.ndouble.Vector2d class PlayerMovementController(entity: PlayerEntity) : WalkableMovementController(entity) { public override var moveDirection = Move.STAND_STILL + private var bodyFixture: B2Fixture + + override var isDucked: Boolean = false + set(value) { + if (value == field) + return + + 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 + } + + override fun canUnDuck(): Boolean { + return world.isSpaceEmptyFromTiles(STANDING_AABB + position) + } + + init { + bodyFixture = body.createFixture(FixtureDef( + shape = STANDING, + friction = 0.4, + density = 1.9, + )) + } + + 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 standingAABB = AABB.rectangle(Vector2d.ZERO, 1.8, 3.7) - override val duckingAABB = AABB.rectangle(Vector2d.ZERO, 1.8, 1.8) + Vector2d(y = -0.9) override val movement = PlayerMovementController(this) override fun thinkAI(delta: Double) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Projectile.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Projectile.kt index ae78dabf..c58cb24a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Projectile.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Projectile.kt @@ -4,7 +4,7 @@ import ru.dbotthepony.kstarbound.defs.projectile.ConfiguredProjectile import ru.dbotthepony.kstarbound.world.World class Projectile(world: World<*, *>, val def: ConfiguredProjectile) : Entity(world) { - override val movement: MovementController<*> = DummyMovementController(this) + override val movement: MovementController<*> = LogicalMovementController(this) override fun thinkAI(delta: Double) {