Box2d integration test

This commit is contained in:
DBotThePony 2022-02-20 17:20:42 +07:00
parent c82c89dfec
commit 4a02a0e0de
Signed by: DBot
GPG Key ID: DCC23B5715498507
21 changed files with 463 additions and 692 deletions

View File

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

View File

@ -58,12 +58,19 @@ interface IShape<S : IShape<S>> {
/**
* 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.

View File

@ -172,6 +172,27 @@ class ChainShape : IShape<ChainShape> {
)
}
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
}

View File

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

View File

@ -105,6 +105,21 @@ class EdgeShape : IShape<EdgeShape> {
)
}
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),

View File

@ -306,6 +306,24 @@ class PolygonShape : IShape<PolygonShape> {
)
}
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.

View File

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

View File

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

View File

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

View File

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

View File

@ -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<B2Body>()
/*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<MouseJoint>()
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)

View File

@ -6,6 +6,10 @@ import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kvector.util2d.AABB
class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWorld, ClientChunk>(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<ClientWo
renderLayeredList(client.gl.matrixStack, determineRenderers)
physics.debugDraw()
for (renderer in determineRenderers) {
renderer.renderDebug()
}

View File

@ -25,7 +25,6 @@ class StarboundClient : AutoCloseable {
val window: Long
val camera = Camera(this)
val input = UserInput()
var world: ClientWorld? = ClientWorld(this, 0L)
var gameTerminated = false
private set
@ -126,6 +125,8 @@ class StarboundClient : AutoCloseable {
val gl = GLStateTracker()
var world: ClientWorld? = ClientWorld(this, 0L)
fun ensureSameThread() = gl.ensureSameThread()
init {

View File

@ -16,7 +16,7 @@ import java.io.Closeable
* Считается, что процесс отрисовки ограничен лишь одним слоем (т.е. отрисовка происходит в один проход)
*/
open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open var chunk: ClientChunk?) : Closeable {
open val renderPos: Vector2d get() = entity.pos
open val renderPos: Vector2d get() = entity.position
open fun render(stack: Matrix4fStack) {

View File

@ -1,5 +1,9 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kbox2d.api.BodyDef
import ru.dbotthepony.kbox2d.api.FixtureDef
import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
import ru.dbotthepony.kbox2d.dynamics.B2Fixture
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.world.entities.Entity
@ -355,6 +359,20 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble())
var isPhysicsDirty = false
fun markPhysicsDirty() {
if (isPhysicsDirty)
return
isPhysicsDirty = true
world.dirtyPhysicsChunks.add(this as This)
}
fun bakeCollisions() {
foreground.bakeCollisions()
}
inner class TileLayer : IMutableTileChunk {
/**
* Возвращает счётчик изменений этого слоя
@ -371,12 +389,25 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
private val collisionCacheView = Collections.unmodifiableCollection(collisionCache)
private var collisionChangeset = -1
// максимально грубое комбинирование тайлов в бруски для коллизии
// TODO: https://ru.wikipedia.org/wiki/R-дерево_(структураанных)
private fun bakeCollisions() {
private val body = world.physics.createBody(BodyDef(
position = pos.firstBlock.toDoubleVector(),
))
private val collisionBoxes = ArrayList<B2Fixture>()
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<WorldType : World<WorldType, This>, This : Chunk<WorldType,
last = x
} else {
if (first != null) {
collisionCache.add(
AABB(
val aabb = AABB(
Vector2d(x = xAdd + first.toDouble(), y = y.toDouble() + yAdd),
Vector2d(x = xAdd + last.toDouble() + 1.0, y = y.toDouble() + 1.0 + yAdd),
)
)
collisionCache.add(aabb)
body.createFixture(FixtureDef(
shape = PolygonShape().also { it.setAsBox(aabb.width / 2.0, aabb.height / 2.0, aabb.centre - pos.firstBlock.toDoubleVector(), 0.0) },
friction = 0.4,
))
first = null
}
@ -406,12 +442,17 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
}
if (first != null) {
collisionCache.add(
AABB(
val aabb = AABB(
Vector2d(x = first.toDouble() + xAdd, y = y.toDouble() + yAdd),
Vector2d(x = last.toDouble() + 1.0 + xAdd, y = y.toDouble() + 1.0 + yAdd),
)
)
collisionCache.add(aabb)
body.createFixture(FixtureDef(
shape = PolygonShape().also { it.setAsBox(aabb.width / 2.0, aabb.height / 2.0, aabb.centre - pos.firstBlock.toDoubleVector(), 0.0) },
friction = 0.4,
))
}
}
}
@ -451,6 +492,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
changeset++
tiles[x or (y shl CHUNK_SHIFT)] = tile
markPhysicsDirty()
}
override operator fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? {
@ -460,6 +502,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
val chunkTile = if (tile != null) ChunkTile(this, tile) else null
this[x, y] = chunkTile
changeset++
markPhysicsDirty()
return chunkTile
}

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kbox2d.dynamics.B2World
import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.*
@ -98,8 +99,16 @@ private const val EPSILON = 0.00001
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val seed: Long = 0L) {
protected val chunkMap = HashMap<ChunkPos, IMutableWorldChunkTuple<This, ChunkType>>()
/**
* Chunks, which have their collision mesh changed
*/
val dirtyPhysicsChunks = HashSet<ChunkType>()
protected var lastAccessedChunk: IMutableWorldChunkTuple<This, ChunkType>? = null
val physics = B2World(Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION))
/**
* Таймер этого мира, в секундах.
*
@ -124,6 +133,14 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
fun think(delta: Double) {
require(delta > 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<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
*
* При Vector2d.ZERO = невесомость
*/
var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION)
var gravity by physics::gravity
protected abstract fun chunkFactory(
pos: ChunkPos,
@ -200,7 +217,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val orphanedInThisChunk = ArrayList<Entity>()
for (ent in orphanedEntities) {
val cPos = ChunkPos.fromTilePosition(ent.pos)
val cPos = ChunkPos.fromTilePosition(ent.position)
if (cPos == pos) {
orphanedInThisChunk.add(ent)

View File

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

View File

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

View File

@ -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<T : IEntity>(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>(entity) {
override val currentAABB = DUMMY_AABB
override val affectedByGravity = false
class LogicalMovementController(entity: Entity) : MovementController<Entity>(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
}

View File

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

View File

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