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 import ru.dbotthepony.kvector.vector.ndouble.Vector2d
/// The body type. /**
/// static: zero mass, zero velocity, may be manually moved * The body type.
/// kinematic: zero mass, non-zero velocity set by user, moved by solver * - static: zero mass, zero velocity, may be manually moved
/// dynamic: positive mass, non-zero velocity determined by forces, moved by solver * - 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 { enum class BodyType {
STATIC, STATIC,
KINEMATIC, 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. * 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 xf the world transform of the shape.
* @param childIndex the child shape * @param childIndex the child shape
*/ */
fun computeAABB(transform: Transform, childIndex: Int): AABB 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. * Compute the mass properties of this shape using its dimensions and density.
* The inertia tensor is computed about the local origin. * 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 { override fun computeMass(density: Double): MassData {
return MASS 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 { override fun computeMass(density: Double): MassData {
val mass = density * PI * radius * radius 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 { override fun computeMass(density: Double): MassData {
return MassData( return MassData(
center = 0.5 * (vertex1 + vertex2), 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 { override fun computeMass(density: Double): MassData {
// Polygon mass, centroid, and inertia. // Polygon mass, centroid, and inertia.
// Let rho be the polygon density in mass per unit area. // Let rho be the polygon density in mass per unit area.

View File

@ -4,6 +4,7 @@
package ru.dbotthepony.kbox2d.dynamics package ru.dbotthepony.kbox2d.dynamics
import ru.dbotthepony.kbox2d.api.* import ru.dbotthepony.kbox2d.api.*
import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.ndouble.Vector2d
class B2Body(def: BodyDef, world: B2World) { class B2Body(def: BodyDef, world: B2World) {
@ -395,6 +396,7 @@ class B2Body(def: BodyDef, world: B2World) {
} }
fixture.next = fixtureList fixture.next = fixtureList
fixtureList?.prev = fixture
fixtureList = fixture fixtureList = fixture
fixtureCount++ 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})" } 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" } check(fixtureCount > 0) { "Having no tracked fixtures, but $fixture belongs to us" }
var node: B2Fixture? = fixtureList val prev = fixture.prev
var found = false val next = fixture.next
var previous: B2Fixture? = null
while (node != null) { prev?.next = next
if (node == fixture) { next?.prev = prev
// TODO: Это должно работать
previous?.next = node.next if (fixture == fixtureList) {
found = true fixtureList = next ?: prev
break
} else {
previous = node
node = node.next
}
} }
check(found) { "Can't find $fixture in linked list of fixtures" } val density = fixture.density
val density = node!!.density
// Destroy any contacts associated with the fixture. // Destroy any contacts associated with the fixture.
var edge = contactEdge var edge = contactEdge
@ -556,6 +550,42 @@ class B2Body(def: BodyDef, world: B2World) {
linearVelocity += b2Cross(angularVelocity, sweep.c - oldCenter) 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. * Get the mass data of the body.
* @return a struct containing the mass, inertia and center 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 point the world position of the point of application.
* @param wake also wake up the body * @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) if (type != BodyType.DYNAMIC)
return return
@ -762,7 +792,7 @@ class B2Body(def: BodyDef, world: B2World) {
* @param force the world force vector, usually in Newtons (N). * @param force the world force vector, usually in Newtons (N).
* @param wake also wake up the body * @param wake also wake up the body
*/ */
fun applyForceToCenter(force: Vector2d, wake: Boolean) { fun applyForceToCenter(force: Vector2d, wake: Boolean = true) {
if (type != BodyType.DYNAMIC) if (type != BodyType.DYNAMIC)
return 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 torque about the z-axis (out of the screen), usually in N-m.
* @param wake also wake up the body * @param wake also wake up the body
*/ */
fun applyTorque(torque: Double, wake: Boolean) { fun applyTorque(torque: Double, wake: Boolean = true) {
if (type != BodyType.DYNAMIC) if (type != BodyType.DYNAMIC)
return return
@ -801,7 +831,7 @@ class B2Body(def: BodyDef, world: B2World) {
* @param point the world position of the point of application. * @param point the world position of the point of application.
* @param wake also wake up the body * @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) if (type != BodyType.DYNAMIC)
return 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 impulse the world impulse vector, usually in N-seconds or kg-m/s.
* @param wake also wake up the body * @param wake also wake up the body
*/ */
fun applyLinearImpulseToCenter(impulse: Vector2d, wake: Boolean) { fun applyLinearImpulseToCenter(impulse: Vector2d, wake: Boolean = true) {
if (type != BodyType.DYNAMIC) if (type != BodyType.DYNAMIC)
return return
@ -836,7 +866,7 @@ class B2Body(def: BodyDef, world: B2World) {
* @param impulse the angular impulse in units of kg*m*m/s * @param impulse the angular impulse in units of kg*m*m/s
* @param wake also wake up the body * @param wake also wake up the body
*/ */
fun applyAngularImpulse(impulse: Double, wake: Boolean) { fun applyAngularImpulse(impulse: Double, wake: Boolean = true) {
if (type != BodyType.DYNAMIC) if (type != BodyType.DYNAMIC)
return return

View File

@ -31,6 +31,14 @@ class B2Fixture(
* @return the next shape. * @return the next shape.
*/ */
var next: B2Fixture? = null 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 * Get the user data that was assigned in the fixture definition. Use this to
@ -126,6 +134,11 @@ class B2Fixture(
refilter() refilter()
} }
/**
* **KBox2D extension**.
*
* Convenience function for quickly destroying this fixture.
*/
fun destroy() { fun destroy() {
checkNotNull(body) { "Already destroyed" }.destroyFixture(this) checkNotNull(body) { "Already destroyed" }.destroyFixture(this)
} }

View File

@ -191,7 +191,7 @@ internal class Island(
body.sweep.c0 = body.sweep.c body.sweep.c0 = body.sweep.c
body.sweep.a0 = body.sweep.a body.sweep.a0 = body.sweep.a
if (body.type == ru.dbotthepony.kbox2d.api.BodyType.DYNAMIC) { if (body.type == BodyType.DYNAMIC) {
// Integrate velocities. // Integrate velocities.
v += (gravity * body.gravityScale * body.mass + body.force) * body.invMass * h v += (gravity * body.gravityScale * body.mass + body.force) * body.invMass * h
w += h * body.rotInertiaInv * body.torque 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), 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.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.shapes.ChainShape
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.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.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.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.entities.Move import ru.dbotthepony.kstarbound.world.entities.Move
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
import ru.dbotthepony.kstarbound.world.entities.Projectile
import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import java.io.File import java.io.File
import java.util.* import java.util.*
import kotlin.collections.ArrayList
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
@ -32,380 +31,14 @@ fun main() {
//return //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() val client = StarboundClient()
//Starbound.addFilePath(File("./unpacked_assets/")) //Starbound.addFilePath(File("./unpacked_assets/"))
Starbound.addPakPath(File("J:\\SteamLibrary\\steamapps\\common\\Starbound\\assets\\packed.pak")) 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.putDebugLog(status, replaceStatus)
}*/ }
client.onTermination { client.onTermination {
Starbound.terminateLoading = true Starbound.terminateLoading = true
@ -416,14 +49,14 @@ fun main() {
val ent = PlayerEntity(client.world!!) val ent = PlayerEntity(client.world!!)
Starbound.onInitialize { 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 chunkB = client.world!!.computeIfAbsent(ChunkPos(-1, 0)).chunk
val chunkC = client.world!!.computeIfAbsent(ChunkPos(-2, 0)).chunk val chunkC = client.world!!.computeIfAbsent(ChunkPos(-2, 0)).chunk
val tile = Starbound.getTileDefinition("alienrock") val tile = Starbound.getTileDefinition("alienrock")
for (x in -48 .. 48) { for (x in -6 .. 6) {
for (y in 0 .. 20) { for (y in 0 .. 4) {
val chnk = client.world!!.computeIfAbsent(ChunkPos(x, y)) val chnk = client.world!!.computeIfAbsent(ChunkPos(x, y))
if (y == 0) { if (y == 0) {
@ -486,19 +119,48 @@ fun main() {
chunkA!!.foreground[rand.nextInt(0, CHUNK_SIZE_FF), rand.nextInt(0, CHUNK_SIZE_FF)] = tile 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()) { for ((i, proj) in Starbound.projectilesAccess.values.withIndex()) {
val projEnt = Projectile(client.world!!, proj) val projEnt = Projectile(client.world!!, proj)
projEnt.pos = Vector2d(i * 2.0, 10.0) projEnt.position = Vector2d(i * 2.0, 10.0)
projEnt.spawn() 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.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) client.gl.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f)
} }
@ -509,65 +171,13 @@ fun main() {
client.camera.pos.y = 10f client.camera.pos.y = 10f
world.debugDraw = client.gl.box2dRenderer
client.gl.box2dRenderer.drawShapes = true client.gl.box2dRenderer.drawShapes = true
client.gl.box2dRenderer.drawPairs = false client.gl.box2dRenderer.drawPairs = false
client.gl.box2dRenderer.drawAABB = false client.gl.box2dRenderer.drawAABB = false
client.gl.box2dRenderer.drawJoints = false client.gl.box2dRenderer.drawJoints = false
client.onPostDrawWorld { 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() ent.spawn()
@ -576,8 +186,8 @@ fun main() {
Starbound.pollCallbacks() Starbound.pollCallbacks()
//ent.think(client.frameRenderTime) //ent.think(client.frameRenderTime)
//client.camera.pos.x = ent.pos.x.toFloat() client.camera.pos.x = ent.position.x.toFloat()
//client.camera.pos.y = ent.pos.y.toFloat() client.camera.pos.y = ent.position.y.toFloat()
//println(client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1) //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 import ru.dbotthepony.kvector.util2d.AABB
class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWorld, ClientChunk>(seed) { class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWorld, ClientChunk>(seed) {
init {
physics.debugDraw = client.gl.box2dRenderer
}
override fun chunkFactory(pos: ChunkPos): ClientChunk { override fun chunkFactory(pos: ChunkPos): ClientChunk {
return ClientChunk( return ClientChunk(
world = this, world = this,
@ -33,6 +37,8 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWo
renderLayeredList(client.gl.matrixStack, determineRenderers) renderLayeredList(client.gl.matrixStack, determineRenderers)
physics.debugDraw()
for (renderer in determineRenderers) { for (renderer in determineRenderers) {
renderer.renderDebug() renderer.renderDebug()
} }

View File

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

View File

@ -1,5 +1,9 @@
package ru.dbotthepony.kstarbound.world 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.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.world.entities.Entity 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()) 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 { 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 val collisionCacheView = Collections.unmodifiableCollection(collisionCache)
private var collisionChangeset = -1 private var collisionChangeset = -1
// максимально грубое комбинирование тайлов в бруски для коллизии private val body = world.physics.createBody(BodyDef(
// TODO: https://ru.wikipedia.org/wiki/R-дерево_(структураанных) position = pos.firstBlock.toDoubleVector(),
private fun bakeCollisions() { ))
private val collisionBoxes = ArrayList<B2Fixture>()
fun bakeCollisions() {
if (collisionChangeset == changeset)
return
collisionChangeset = changeset collisionChangeset = changeset
collisionCache.clear() collisionCache.clear()
for (box in collisionBoxes) {
body.destroyFixture(box)
}
collisionBoxes.clear()
val xAdd = pos.x * CHUNK_SIZEd val xAdd = pos.x * CHUNK_SIZEd
val yAdd = pos.y * CHUNK_SIZEd val yAdd = pos.y * CHUNK_SIZEd
@ -393,12 +424,17 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
last = x last = x
} else { } else {
if (first != null) { if (first != null) {
collisionCache.add( val aabb = AABB(
AABB(
Vector2d(x = xAdd + first.toDouble(), y = y.toDouble() + yAdd), Vector2d(x = xAdd + first.toDouble(), y = y.toDouble() + yAdd),
Vector2d(x = xAdd + last.toDouble() + 1.0, y = y.toDouble() + 1.0 + 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 first = null
} }
@ -406,12 +442,17 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
if (first != null) { if (first != null) {
collisionCache.add( val aabb = AABB(
AABB(
Vector2d(x = first.toDouble() + xAdd, y = y.toDouble() + yAdd), Vector2d(x = first.toDouble() + xAdd, y = y.toDouble() + yAdd),
Vector2d(x = last.toDouble() + 1.0 + xAdd, y = y.toDouble() + 1.0 + 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++ changeset++
tiles[x or (y shl CHUNK_SHIFT)] = tile tiles[x or (y shl CHUNK_SHIFT)] = tile
markPhysicsDirty()
} }
override operator fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? { 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 val chunkTile = if (tile != null) ChunkTile(this, tile) else null
this[x, y] = chunkTile this[x, y] = chunkTile
changeset++ changeset++
markPhysicsDirty()
return chunkTile return chunkTile
} }

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kbox2d.dynamics.B2World
import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.* 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) { abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val seed: Long = 0L) {
protected val chunkMap = HashMap<ChunkPos, IMutableWorldChunkTuple<This, ChunkType>>() 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 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) { fun think(delta: Double) {
require(delta > 0.0) { "Tried to update $this by $delta seconds" } 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 timer += delta
thinkInner(delta) thinkInner(delta)
} }
@ -145,7 +162,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
* *
* При Vector2d.ZERO = невесомость * При Vector2d.ZERO = невесомость
*/ */
var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION) var gravity by physics::gravity
protected abstract fun chunkFactory( protected abstract fun chunkFactory(
pos: ChunkPos, pos: ChunkPos,
@ -200,7 +217,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val orphanedInThisChunk = ArrayList<Entity>() val orphanedInThisChunk = ArrayList<Entity>()
for (ent in orphanedEntities) { for (ent in orphanedEntities) {
val cPos = ChunkPos.fromTilePosition(ent.pos) val cPos = ChunkPos.fromTilePosition(ent.position)
if (cPos == pos) { if (cPos == pos) {
orphanedInThisChunk.add(ent) orphanedInThisChunk.add(ent)

View File

@ -14,16 +14,6 @@ enum class Move {
} }
interface IWalkableEntity : IEntity { interface IWalkableEntity : IEntity {
/**
* AABB сущности, которая стоит
*/
val standingAABB: AABB
/**
* AABB сущности, которая присела
*/
val duckingAABB: AABB
/** /**
* Максимальная скорость передвижения этого AliveMovementController в Starbound Units/секунда * Максимальная скорость передвижения этого AliveMovementController в Starbound Units/секунда
* *
@ -75,54 +65,48 @@ interface IWalkableEntity : IEntity {
* Базовый абстрактный класс, реализующий сущность, которая ходит по земле * Базовый абстрактный класс, реализующий сущность, которая ходит по земле
*/ */
abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : MovementController<T>(entity) { abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : MovementController<T>(entity) {
init {
body.isFixedRotation = true
}
protected abstract val moveDirection: Move protected abstract val moveDirection: Move
override val collisionResolution = CollisionResolution.SLIDE
var wantsToDuck = false var wantsToDuck = false
var isDucked = false open var isDucked = false
protected set protected set
override val currentAABB: AABB
get() {
if (isDucked) {
return entity.duckingAABB
}
return entity.standingAABB
}
override fun thinkPhysics(delta: Double) { override fun thinkPhysics(delta: Double) {
super.thinkPhysics(delta) super.thinkPhysics(delta)
thinkMovement(delta) thinkMovement(delta)
} }
/** /**
* Смотрим [IWalkableEntity.topSpeed] * See [IWalkableEntity.topSpeed]
*/ */
open val topSpeed by entity::topSpeed open val topSpeed by entity::topSpeed
/** /**
* Смотрим [IWalkableEntity.moveSpeed] * See [IWalkableEntity.moveSpeed]
*/ */
open val moveSpeed by entity::moveSpeed open val moveSpeed by entity::moveSpeed
/** /**
* Смотрим [IWalkableEntity.freeFallMoveSpeed] * See [IWalkableEntity.freeFallMoveSpeed]
*/ */
open val freeFallMoveSpeed by entity::freeFallMoveSpeed open val freeFallMoveSpeed by entity::freeFallMoveSpeed
/** /**
* Смотрим [IWalkableEntity.brakeForce] * See [IWalkableEntity.brakeForce]
*/ */
open val brakeForce by entity::brakeForce open val brakeForce by entity::brakeForce
/** /**
* Смотрим [IWalkableEntity.stepSize] * See [IWalkableEntity.stepSize]
*/ */
open val stepSize by entity::stepSize open val stepSize by entity::stepSize
/** /**
* Смотрим [IWalkableEntity.jumpForce] * See [IWalkableEntity.jumpForce]
*/ */
open val jumpForce by entity::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) { protected open fun thinkMovement(delta: Double) {
if (onGround || !affectedByGravity) { if (onGround && !isDucked) {
var add = Vector2d.ZERO when (moveDirection) {
Move.STAND_STILL -> {
body.linearVelocity += Vector2d(x = -body.linearVelocity.x * delta * brakeForce)
}
if (isDucked) { Move.MOVE_LEFT -> {
thinkFriction(delta * brakeForce) if (body.linearVelocity.x > 0.0) {
} else if (velocity.y.absoluteValue < 1 && !jumpRequested) { body.linearVelocity += Vector2d(x = -body.linearVelocity.x * delta * brakeForce)
when (moveDirection) {
Move.STAND_STILL -> {
thinkFriction(delta * brakeForce)
add = Vector2d.ZERO
} }
Move.MOVE_LEFT -> { if (body.linearVelocity.x > -topSpeed) {
if (velocity.x > 0.0) { body.linearVelocity += Vector2d(x = -moveSpeed * delta)
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 (add != Vector2d.ZERO) { Move.MOVE_RIGHT -> {
if (isSpaceOpen(add, delta)) { if (body.linearVelocity.x < 0.0) {
velocity += add body.linearVelocity += Vector2d(x = -body.linearVelocity.x * delta * brakeForce)
// спускание с "лестницы"
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)
} }
if (world is ClientWorld && world.client.settings.debugCollisions) { if (body.linearVelocity.x < topSpeed) {
world.client.onPostDrawWorldOnce { body.linearVelocity += Vector2d(x = moveSpeed * delta)
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 (velocity.length > topSpeed) {
thinkFriction(delta * (velocity.length - topSpeed))
}
if (jumpRequested) { if (jumpRequested) {
jumpRequested = false jumpRequested = false
velocity += groundNormal * jumpForce nextJump = world.timer + 0.1
onGround = false
body.linearVelocity += Vector2d(y = jumpForce)
} }
} else if (!onGround && affectedByGravity) { } else if (!onGround && !isDucked && freeFallMoveSpeed != 0.0) {
when (moveDirection) { when (moveDirection) {
Move.STAND_STILL -> {} Move.STAND_STILL -> {
// do nothing
}
Move.MOVE_LEFT -> { Move.MOVE_LEFT -> {
val add = Vector2d(x = -delta * freeFallMoveSpeed) body.linearVelocity += Vector2d(x = -freeFallMoveSpeed * delta)
if (isSpaceOpen(add, delta))
velocity += add
} }
Move.MOVE_RIGHT -> { Move.MOVE_RIGHT -> {
val add = Vector2d(x = delta * freeFallMoveSpeed) body.linearVelocity += Vector2d(x = freeFallMoveSpeed * delta)
if (isSpaceOpen(add, delta))
velocity += add
} }
} }
} else if (onGround && isDucked) {
body.linearVelocity += Vector2d(x = -body.linearVelocity.x * delta * brakeForce)
} }
if (wantsToDuck && onGround) { if (wantsToDuck && onGround) {
isDucked = true isDucked = true
} else if (isDucked) { } else if (isDucked) {
if (world.isSpaceEmptyFromTiles(entity.standingAABB + pos)) { if (canUnDuck()) {
isDucked = false isDucked = false
} }
} }
@ -270,7 +203,7 @@ abstract class AliveWalkingEntity(world: World<*, *>) : AliveEntity(world), IWal
override val topSpeed = 20.0 override val topSpeed = 20.0
override val moveSpeed = 64.0 override val moveSpeed = 64.0
override val freeFallMoveSpeed = 8.0 override val freeFallMoveSpeed = 8.0
override val brakeForce = 32.0 override val brakeForce = 16.0
override val jumpForce = 20.0 override val jumpForce = 20.0
override val stepSize = 1.1 override val stepSize = 1.1

View File

@ -9,12 +9,57 @@ import ru.dbotthepony.kvector.vector.ndouble.Vector2d
* Интерфейс служит лишь для убирания жёсткой зависимости от класса Entity * Интерфейс служит лишь для убирания жёсткой зависимости от класса Entity
*/ */
interface IEntity { interface IEntity {
/**
* The world this entity in, never changes.
*/
val world: World<*, *> val world: World<*, *>
/**
* The chunk this entity currently in, it is automatically updated on each change of [position]
*/
var chunk: Chunk<*, *>? 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<*> 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 val isSpawned: Boolean
/**
* Whenever is this entity was removed from world ([remove] called).
*/
val isRemoved: Boolean val isRemoved: Boolean
fun spawn() 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") 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) { if (value == field) {
return return
} }
val chunkPos = ChunkPos.fromTilePosition(pos) val chunkPos = ChunkPos.fromTilePosition(position)
if (value != null && chunkPos != value.pos) { if (value != null && chunkPos != value.pos) {
throw IllegalStateException("Set proper position before setting chunk this Entity belongs to") 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) { set(value) {
if (field == value) if (field == value)
return return
@ -65,7 +114,9 @@ abstract class Entity(override val world: World<*, *>) : IEntity {
val old = field val old = field
field = value field = value
if (isSpawned) { movement.notifyPositionChanged()
if (isSpawned && !isRemoved) {
val oldChunkPos = ChunkPos.fromTilePosition(old) val oldChunkPos = ChunkPos.fromTilePosition(old)
val newChunkPos = ChunkPos.fromTilePosition(value) 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 final override var isSpawned = false
private set private set
@ -88,7 +139,7 @@ abstract class Entity(override val world: World<*, *>) : IEntity {
isSpawned = true isSpawned = true
world.entities.add(this) world.entities.add(this)
chunk = world.getChunk(ChunkPos.fromTilePosition(pos))?.chunk chunk = world.getChunk(ChunkPos.fromTilePosition(position))?.chunk
if (chunk == null) { if (chunk == null) {
world.orphanedEntities.add(this) world.orphanedEntities.add(this)

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.world.entities 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.util2d.AABB
import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.ndouble.Vector2d
@ -13,129 +14,76 @@ enum class CollisionResolution {
abstract class MovementController<T : IEntity>(val entity: T) { abstract class MovementController<T : IEntity>(val entity: T) {
val world = entity.world val world = entity.world
var pos by entity::pos open var position by entity::position
var rotation by entity::rotation open var angle by entity::angle
open val mass = 1.0 protected val body by lazy {
world.physics.createBody(BodyDef(
// наследуемые свойства position = position,
open val affectedByGravity = true angle = angle,
open val collisionResolution = CollisionResolution.STOP type = BodyType.DYNAMIC
))
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
} }
var groundNormal = Vector2d.ZERO open val velocity get() = body.linearVelocity
protected set
protected open fun propagateVelocity(delta: Double) { /**
if (velocity.length == 0.0) * Returns whenever are we contacting something below us
return */
open val onGround: Boolean get() {
val sweep = sweepRelative(velocity * delta, delta) for (contact in body.contactEdgeIterator) {
this.velocity = sweep.hitPosition / delta if (contact.contact.manifold.localNormal.dot(world.gravity.normalized) >= 0.97) {
this.pos += this.velocity * delta return true
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
} }
} }
return false
} }
protected open fun thinkGravity(delta: Double) { /**
velocity += world.gravity * delta * World space AABB, by default returning combined AABB of physical body.
} */
open val worldAABB: AABB get() {
protected open fun thinkFriction(delta: Double) { return body.worldSpaceAABB
velocity *= Vector2d(lerp(delta, 1.0, 0.01), 1.0)
} }
open fun thinkPhysics(delta: Double) { open fun thinkPhysics(delta: Double) {
if (!onGround && affectedByGravity) mutePositionChanged = true
thinkGravity(delta) position = body.position
angle = body.angle
propagateVelocity(delta) mutePositionChanged = false
if (affectedByGravity && onGround)
thinkFriction(delta)
} }
protected open fun onTouchSurface(velocity: Vector2d, normal: Vector2d) { protected open fun onTouchSurface(velocity: Vector2d, normal: Vector2d) {
entity.onTouchSurface(velocity, normal) entity.onTouchSurface(velocity, normal)
} }
companion object { protected var mutePositionChanged = false
private val DROP_TO_FLOOR = Vector2d(y = -1_000.0)
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) { class LogicalMovementController(entity: Entity) : MovementController<Entity>(entity) {
override val currentAABB = DUMMY_AABB override val worldAABB: AABB get() = AABB(position, position)
override val affectedByGravity = false
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 // no-op
} }
override fun thinkGravity(delta: Double) { override fun notifyPositionChanged() {
// no-op
}
override fun thinkFriction(delta: Double) {
// no-op // no-op
} }

View File

@ -1,19 +1,66 @@
package ru.dbotthepony.kstarbound.world.entities 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.kstarbound.world.World
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.ndouble.Vector2d 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
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) { 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 val movement = PlayerMovementController(this)
override fun thinkAI(delta: Double) { override fun thinkAI(delta: Double) {

View File

@ -4,7 +4,7 @@ import ru.dbotthepony.kstarbound.defs.projectile.ConfiguredProjectile
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
class Projectile(world: World<*, *>, val def: ConfiguredProjectile) : Entity(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) { override fun thinkAI(delta: Double) {