Actor movement controller

This commit is contained in:
DBotThePony 2024-01-25 19:46:41 +07:00
parent 8fe6da7218
commit f58b0bca80
Signed by: DBot
GPG Key ID: DCC23B5715498507
33 changed files with 1450 additions and 719 deletions

View File

@ -1,12 +1,11 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.ClientConfigParameters import ru.dbotthepony.kstarbound.defs.ClientConfigParameters
import ru.dbotthepony.kstarbound.defs.MovementParameters import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask import java.util.concurrent.ForkJoinTask
import java.util.concurrent.Future import java.util.concurrent.Future
import kotlin.reflect.KMutableProperty0 import kotlin.reflect.KMutableProperty0

View File

@ -11,7 +11,9 @@ import ru.dbotthepony.kstarbound.player.Avatar
import ru.dbotthepony.kstarbound.world.entities.ItemEntity import ru.dbotthepony.kstarbound.world.entities.ItemEntity
import ru.dbotthepony.kstarbound.json.VersionedJson import ru.dbotthepony.kstarbound.json.VersionedJson
import ru.dbotthepony.kstarbound.io.readVarInt import ru.dbotthepony.kstarbound.io.readVarInt
import ru.dbotthepony.kstarbound.json.BinaryJsonReader
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.api.MutableCell import ru.dbotthepony.kstarbound.world.api.MutableCell
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.WorldObject
@ -43,10 +45,12 @@ fun main() {
val db = BTreeDB(File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\389760395_938904237_-238610574_5.world")) val db = BTreeDB(File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\389760395_938904237_-238610574_5.world"))
//val db = BTreeDB(File("world.world")) //val db = BTreeDB(File("world.world"))
//val meta = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(db.read(byteArrayOf(0, 0, 0, 0, 0))), Inflater()))) val meta = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(db.read(byteArrayOf(0, 0, 0, 0, 0))), Inflater())))
//println(meta.readInt()) println(meta.readInt())
//println(meta.readInt()) println(meta.readInt())
println(VersionedJson(meta))
val client = StarboundClient() val client = StarboundClient()
@ -125,14 +129,14 @@ fun main() {
val rand = Random() val rand = Random()
for (i in 0 until 128) { for (i in 0 until 0) {
val item = ItemEntity(client.world!!, Registries.items.keys.values.random().value) val item = ItemEntity(client.world!!, Registries.items.keys.values.random().value)
item.position = Vector2d(225.0 - i, 785.0) item.position = Vector2d(225.0 - i, 785.0)
item.spawn() item.spawn()
item.velocity = Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0) item.movement.velocity = Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0)
item.mailbox.scheduleAtFixedRate({ item.velocity += Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0) }, 1000 + rand.nextLong(-100, 100), 1000 + rand.nextLong(-100, 100), TimeUnit.MILLISECONDS) item.mailbox.scheduleAtFixedRate({ item.movement.velocity += Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0) }, 1000 + rand.nextLong(-100, 100), 1000 + rand.nextLong(-100, 100), TimeUnit.MILLISECONDS)
//item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0)) //item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0))
} }
@ -189,10 +193,9 @@ fun main() {
if (ply != null) { if (ply != null) {
client.camera.pos = ply!!.position client.camera.pos = ply!!.position
ply!!.velocity += Vector2d( ply!!.movement.controlMove = if (client.input.KEY_A_DOWN) Direction.LEFT else if (client.input.KEY_D_DOWN) Direction.RIGHT else null
(if (client.input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0), ply!!.movement.controlJump = client.input.KEY_SPACE_DOWN
(if (client.input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) ply!!.movement.controlRun = !client.input.KEY_LEFT_SHIFT_DOWN
) * 8.0
} }
if (client.input.KEY_ESCAPE_PRESSED) { if (client.input.KEY_ESCAPE_PRESSED) {

View File

@ -80,6 +80,7 @@ import kotlin.random.Random
object Starbound : ISBFileLocator { object Starbound : ISBFileLocator {
const val TICK_TIME_ADVANCE = 0.01666666666666664 const val TICK_TIME_ADVANCE = 0.01666666666666664
const val TICK_TIME_ADVANCE_NANOS = 16_666_666L const val TICK_TIME_ADVANCE_NANOS = 16_666_666L
const val DEDUP_CELL_STATES = true
val thread = Thread(::universeThread, "Starbound Universe") val thread = Thread(::universeThread, "Starbound Universe")
val mailbox = MailboxExecutorService(thread) val mailbox = MailboxExecutorService(thread)

View File

@ -271,12 +271,12 @@ class StarboundClient : Closeable {
val pHeight = stack.mallocInt(1) val pHeight = stack.mallocInt(1)
val pChannels = stack.mallocInt(1) val pChannels = stack.mallocInt(1)
val readFromDisk = readInternalBytes("starbound.png") val readFromDisk = readInternalBytes("starbound_icon.png")
val buff = ByteBuffer.allocateDirect(readFromDisk.size) val buff = ByteBuffer.allocateDirect(readFromDisk.size)
buff.put(readFromDisk) buff.put(readFromDisk)
buff.position(0) buff.position(0)
val data = STBImage.stbi_load_from_memory(buff, pWidth, pHeight, pChannels, 4) ?: throw IllegalStateException("Unable to decode starbound.png") val data = STBImage.stbi_load_from_memory(buff, pWidth, pHeight, pChannels, 4) ?: throw IllegalStateException("Unable to decode starbound_icon.png")
val img = GLFWImage.malloc() val img = GLFWImage.malloc()
img.set(pWidth[0], pHeight[0], data) img.set(pWidth[0], pHeight[0], data)

View File

@ -0,0 +1,30 @@
package ru.dbotthepony.kstarbound.defs
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class ActorMovementModifiers(
val groundMovementModifier: Double = 0.0,
val liquidMovementModifier: Double = 0.0,
val speedModifier: Double = 0.0,
val airJumpModifier: Double = 0.0,
val liquidJumpModifier: Double = 0.0,
val runningSuppressed: Boolean = false,
val jumpingSuppressed: Boolean = false,
val movementSuppressed: Boolean = false,
val facingSuppressed: Boolean = false,
) {
fun combine(other: ActorMovementModifiers): ActorMovementModifiers {
return ActorMovementModifiers(
groundMovementModifier = groundMovementModifier * other.groundMovementModifier,
liquidMovementModifier = liquidMovementModifier * other.liquidMovementModifier,
speedModifier = speedModifier * other.speedModifier,
airJumpModifier = airJumpModifier * other.airJumpModifier,
liquidJumpModifier = liquidJumpModifier * other.liquidJumpModifier,
runningSuppressed = runningSuppressed || other.runningSuppressed,
jumpingSuppressed = jumpingSuppressed || other.jumpingSuppressed,
movementSuppressed = movementSuppressed || other.movementSuppressed,
facingSuppressed = facingSuppressed || other.facingSuppressed,
)
}
}

View File

@ -1,66 +0,0 @@
package ru.dbotthepony.kstarbound.defs
import ru.dbotthepony.kstarbound.json.builder.JsonAlias
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat
import ru.dbotthepony.kstarbound.world.physics.Poly
@JsonFactory
data class ActorMovementParameters(
@JsonFlat
val base: BaseMovementParameters.Impl = BaseMovementParameters.Impl(),
@JsonAlias("collisionPoly")
val standingPoly: Poly? = null,
@JsonAlias("collisionPoly")
val crouchingPoly: Poly? = null,
val walkSpeed: Double? = null,
val runSpeed: Double? = null,
val flySpeed: Double? = null,
val minimumLiquidPercentage: Double? = null,
val liquidImpedance: Double? = null,
val normalGroundFriction: Double? = null,
val ambulatingGroundFriction: Double? = null,
val groundForce: Double? = null,
val airForce: Double? = null,
val liquidForce: Double? = null,
val airJumpProfile: JumpProfile = JumpProfile(),
val liquidJumpProfile: JumpProfile = JumpProfile(),
val fallStatusSpeedMin: Double? = null,
val fallThroughSustainFrames: Int? = null,
val groundMovementMinimumSustain: Double? = null,
val groundMovementMaximumSustain: Double? = null,
val groundMovementCheckDistance: Double? = null,
val pathExploreRate: Double? = null,
) : BaseMovementParameters by base {
fun merge(other: ActorMovementParameters): ActorMovementParameters {
return ActorMovementParameters(
base = base.merge(other.base),
standingPoly = standingPoly ?: other.standingPoly,
crouchingPoly = crouchingPoly ?: other.crouchingPoly,
walkSpeed = walkSpeed ?: other.walkSpeed,
runSpeed = runSpeed ?: other.runSpeed,
flySpeed = flySpeed ?: other.flySpeed,
minimumLiquidPercentage = minimumLiquidPercentage ?: other.minimumLiquidPercentage,
liquidImpedance = liquidImpedance ?: other.liquidImpedance,
normalGroundFriction = normalGroundFriction ?: other.normalGroundFriction,
ambulatingGroundFriction = ambulatingGroundFriction ?: other.ambulatingGroundFriction,
groundForce = groundForce ?: other.groundForce,
airForce = airForce ?: other.airForce,
liquidForce = liquidForce ?: other.liquidForce,
airJumpProfile = airJumpProfile.merge(other.airJumpProfile),
liquidJumpProfile = liquidJumpProfile.merge(other.liquidJumpProfile),
fallStatusSpeedMin = fallStatusSpeedMin ?: other.fallStatusSpeedMin,
fallThroughSustainFrames = fallThroughSustainFrames ?: other.fallThroughSustainFrames,
groundMovementMinimumSustain = groundMovementMinimumSustain ?: other.groundMovementMinimumSustain,
groundMovementMaximumSustain = groundMovementMaximumSustain ?: other.groundMovementMaximumSustain,
groundMovementCheckDistance = groundMovementCheckDistance ?: other.groundMovementCheckDistance,
)
}
}

View File

@ -1,102 +0,0 @@
package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonImplementation
import ru.dbotthepony.kstarbound.util.KOptional
val BaseMovementParameters.ignorePlatformCollision get() = if (this is MovementParameters) this.ignorePlatformCollision ?: false else false
val BaseMovementParameters.restDuration get() = if (this is MovementParameters) this.restDuration ?: 0 else 0
@JsonImplementation(BaseMovementParameters.Impl::class)
sealed interface BaseMovementParameters {
val mass: Double?
val gravityMultiplier: Double?
val liquidBuoyancy: Double?
val airBuoyancy: Double?
val bounceFactor: Double?
// If set to true, during an update that has more than one internal movement
// step, the movement will stop on the first bounce.
val stopOnFirstBounce: Boolean?
// Cheat when sliding on the ground, by trying to correct upwards before
// other directions (within a set limit). Allows smooth sliding along
// horizontal ground without losing horizontal speed.
val enableSurfaceSlopeCorrection: Boolean?
val slopeSlidingFactor: Double?
// ignored
val maxMovementPerStep: Double?
val maximumCorrection: Double?
val speedLimit: Double?
val stickyCollision: Boolean?
val stickyForce: Double?
val airFriction: Double?
val liquidFriction: Double?
val groundFriction: Double?
val collisionEnabled: Boolean?
val frictionEnabled: Boolean?
val gravityEnabled: Boolean?
val maximumPlatformCorrection: Double?
val maximumPlatformCorrectionVelocityFactor: Double?
val physicsEffectCategories: ImmutableSet<String>?
@JsonFactory
data class Impl(
override val mass: Double? = null,
override val gravityMultiplier: Double? = null,
override val liquidBuoyancy: Double? = null,
override val airBuoyancy: Double? = null,
override val bounceFactor: Double? = null,
override val stopOnFirstBounce: Boolean? = null,
override val enableSurfaceSlopeCorrection: Boolean? = null,
override val slopeSlidingFactor: Double? = null,
override val maxMovementPerStep: Double? = null,
override val maximumCorrection: Double? = null,
override val speedLimit: Double? = null,
override val stickyCollision: Boolean? = null,
override val stickyForce: Double? = null,
override val airFriction: Double? = null,
override val liquidFriction: Double? = null,
override val groundFriction: Double? = null,
override val collisionEnabled: Boolean? = null,
override val frictionEnabled: Boolean? = null,
override val gravityEnabled: Boolean? = null,
override val maximumPlatformCorrection: Double? = null,
override val maximumPlatformCorrectionVelocityFactor: Double? = null,
override val physicsEffectCategories: ImmutableSet<String>? = null,
) : BaseMovementParameters {
fun merge(other: Impl): Impl {
return Impl(
mass = mass ?: other.mass,
gravityMultiplier = gravityMultiplier ?: other.gravityMultiplier,
liquidBuoyancy = liquidBuoyancy ?: other.liquidBuoyancy,
airBuoyancy = airBuoyancy ?: other.airBuoyancy,
bounceFactor = bounceFactor ?: other.bounceFactor,
stopOnFirstBounce = stopOnFirstBounce ?: other.stopOnFirstBounce,
enableSurfaceSlopeCorrection = enableSurfaceSlopeCorrection ?: other.enableSurfaceSlopeCorrection,
slopeSlidingFactor = slopeSlidingFactor ?: other.slopeSlidingFactor,
maxMovementPerStep = maxMovementPerStep ?: other.maxMovementPerStep,
maximumCorrection = maximumCorrection ?: other.maximumCorrection,
speedLimit = speedLimit ?: other.speedLimit,
stickyCollision = stickyCollision ?: other.stickyCollision,
stickyForce = stickyForce ?: other.stickyForce,
airFriction = airFriction ?: other.airFriction,
liquidFriction = liquidFriction ?: other.liquidFriction,
groundFriction = groundFriction ?: other.groundFriction,
collisionEnabled = collisionEnabled ?: other.collisionEnabled,
frictionEnabled = frictionEnabled ?: other.frictionEnabled,
gravityEnabled = gravityEnabled ?: other.gravityEnabled,
maximumPlatformCorrection = maximumPlatformCorrection ?: other.maximumPlatformCorrection,
maximumPlatformCorrectionVelocityFactor = maximumPlatformCorrectionVelocityFactor ?: other.maximumPlatformCorrectionVelocityFactor,
physicsEffectCategories = physicsEffectCategories ?: other.physicsEffectCategories,
)
}
}
}

View File

@ -5,27 +5,31 @@ import ru.dbotthepony.kstarbound.util.KOptional
@JsonFactory @JsonFactory
data class JumpProfile( data class JumpProfile(
val jumpSpeed: KOptional<Double> = KOptional.empty(), val jumpSpeed: Double? = null,
val jumpControlForce: KOptional<Double> = KOptional.empty(), val jumpControlForce: Double? = null,
val jumpInitialPercentage: KOptional<Double> = KOptional.empty(), val jumpInitialPercentage: Double? = null,
val jumpHoldTime: KOptional<Double> = KOptional.empty(), val jumpHoldTime: Double? = null,
val jumpTotalHoldTime: KOptional<Double> = KOptional.empty(), val jumpTotalHoldTime: Double? = null,
val multiJump: KOptional<Boolean> = KOptional.empty(), val multiJump: Boolean? = null,
val reJumpDelay: KOptional<Double> = KOptional.empty(), val reJumpDelay: Double? = null,
val autoJump: KOptional<Boolean> = KOptional.empty(), val autoJump: Boolean? = null,
val collisionCancelled: KOptional<Boolean> = KOptional.empty(), val collisionCancelled: Boolean? = null,
) { ) {
fun merge(other: JumpProfile): JumpProfile { fun merge(other: JumpProfile): JumpProfile {
return JumpProfile( return JumpProfile(
jumpSpeed = jumpSpeed.or(other.jumpSpeed), jumpSpeed = other.jumpSpeed ?: jumpSpeed,
jumpControlForce = jumpControlForce.or(other.jumpControlForce), jumpControlForce = other.jumpControlForce ?: jumpControlForce,
jumpInitialPercentage = jumpInitialPercentage.or(other.jumpInitialPercentage), jumpInitialPercentage = other.jumpInitialPercentage ?: jumpInitialPercentage,
jumpHoldTime = jumpHoldTime.or(other.jumpHoldTime), jumpHoldTime = other.jumpHoldTime ?: jumpHoldTime,
jumpTotalHoldTime = jumpTotalHoldTime.or(other.jumpTotalHoldTime), jumpTotalHoldTime = other.jumpTotalHoldTime ?: jumpTotalHoldTime,
multiJump = multiJump.or(other.multiJump), multiJump = other.multiJump ?: multiJump,
reJumpDelay = reJumpDelay.or(other.reJumpDelay), reJumpDelay = other.reJumpDelay ?: reJumpDelay,
autoJump = autoJump.or(other.autoJump), autoJump = other.autoJump ?: autoJump,
collisionCancelled = collisionCancelled.or(other.collisionCancelled), collisionCancelled = other.collisionCancelled ?: collisionCancelled,
) )
} }
companion object {
val EMPTY = JumpProfile()
}
} }

View File

@ -1,27 +1,218 @@
package ru.dbotthepony.kstarbound.defs package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kstarbound.json.builder.JsonAlias
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat import ru.dbotthepony.kstarbound.json.builder.JsonImplementation
import ru.dbotthepony.kstarbound.util.KOptional import ru.dbotthepony.kstarbound.util.Either
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
sealed class BaseMovementParameters {
abstract val mass: Double?
abstract val gravityMultiplier: Double?
abstract val liquidBuoyancy: Double?
abstract val airBuoyancy: Double?
abstract val bounceFactor: Double?
// If set to true, during an update that has more than one internal movement
// step, the movement will stop on the first bounce.
abstract val stopOnFirstBounce: Boolean?
// Cheat when sliding on the ground, by trying to correct upwards before
// other directions (within a set limit). Allows smooth sliding along
// horizontal ground without losing horizontal speed.
abstract val enableSurfaceSlopeCorrection: Boolean?
abstract val slopeSlidingFactor: Double?
abstract val maxMovementPerStep: Double?
abstract val maximumCorrection: Double?
abstract val speedLimit: Double?
abstract val stickyCollision: Boolean?
abstract val stickyForce: Double?
abstract val airFriction: Double?
abstract val liquidFriction: Double?
abstract val groundFriction: Double?
abstract val collisionEnabled: Boolean?
abstract val frictionEnabled: Boolean?
abstract val gravityEnabled: Boolean?
abstract val maximumPlatformCorrection: Double?
abstract val maximumPlatformCorrectionVelocityFactor: Double?
abstract val physicsEffectCategories: ImmutableSet<String>?
}
@JsonFactory @JsonFactory
data class MovementParameters( data class MovementParameters(
@JsonFlat override val mass: Double? = null,
val base: BaseMovementParameters.Impl = BaseMovementParameters.Impl(), override val gravityMultiplier: Double? = null,
override val liquidBuoyancy: Double? = null,
override val airBuoyancy: Double? = null,
override val bounceFactor: Double? = null,
override val stopOnFirstBounce: Boolean? = null,
override val enableSurfaceSlopeCorrection: Boolean? = null,
override val slopeSlidingFactor: Double? = null,
override val maxMovementPerStep: Double? = null,
override val maximumCorrection: Double? = null,
override val speedLimit: Double? = null,
override val stickyCollision: Boolean? = null,
override val stickyForce: Double? = null,
override val airFriction: Double? = null,
override val liquidFriction: Double? = null,
override val groundFriction: Double? = null,
override val collisionEnabled: Boolean? = null,
override val frictionEnabled: Boolean? = null,
override val gravityEnabled: Boolean? = null,
override val maximumPlatformCorrection: Double? = null,
override val maximumPlatformCorrectionVelocityFactor: Double? = null,
override val physicsEffectCategories: ImmutableSet<String>? = null,
val discontinuityThreshold: Float? = null, val discontinuityThreshold: Float? = null,
val collisionPoly: Poly? = null, val collisionPoly: Either<Poly, ImmutableList<Poly>>? = null,
val ignorePlatformCollision: Boolean? = null, val ignorePlatformCollision: Boolean? = null,
val restDuration: Int? = null, val restDuration: Int? = null,
) : BaseMovementParameters by base { ) : BaseMovementParameters() {
fun merge(other: MovementParameters): MovementParameters { fun merge(other: MovementParameters): MovementParameters {
return MovementParameters( return MovementParameters(
base = base.merge(other.base), mass = other.mass ?: mass,
discontinuityThreshold = discontinuityThreshold ?: other.discontinuityThreshold, gravityMultiplier = other.gravityMultiplier ?: gravityMultiplier,
collisionPoly = collisionPoly ?: other.collisionPoly, liquidBuoyancy = other.liquidBuoyancy ?: liquidBuoyancy,
ignorePlatformCollision = ignorePlatformCollision ?: other.ignorePlatformCollision, airBuoyancy = other.airBuoyancy ?: airBuoyancy,
restDuration = restDuration ?: other.restDuration, bounceFactor = other.bounceFactor ?: bounceFactor,
stopOnFirstBounce = other.stopOnFirstBounce ?: stopOnFirstBounce,
enableSurfaceSlopeCorrection = other.enableSurfaceSlopeCorrection ?: enableSurfaceSlopeCorrection,
slopeSlidingFactor = other.slopeSlidingFactor ?: slopeSlidingFactor,
maxMovementPerStep = other.maxMovementPerStep ?: maxMovementPerStep,
maximumCorrection = other.maximumCorrection ?: maximumCorrection,
speedLimit = other.speedLimit ?: speedLimit,
stickyCollision = other.stickyCollision ?: stickyCollision,
stickyForce = other.stickyForce ?: stickyForce,
airFriction = other.airFriction ?: airFriction,
liquidFriction = other.liquidFriction ?: liquidFriction,
groundFriction = other.groundFriction ?: groundFriction,
collisionEnabled = other.collisionEnabled ?: collisionEnabled,
frictionEnabled = other.frictionEnabled ?: frictionEnabled,
gravityEnabled = other.gravityEnabled ?: gravityEnabled,
maximumPlatformCorrection = other.maximumPlatformCorrection ?: maximumPlatformCorrection,
maximumPlatformCorrectionVelocityFactor = other.maximumPlatformCorrectionVelocityFactor ?: maximumPlatformCorrectionVelocityFactor,
physicsEffectCategories = other.physicsEffectCategories ?: physicsEffectCategories,
discontinuityThreshold = other.discontinuityThreshold ?: discontinuityThreshold,
collisionPoly = other.collisionPoly ?: collisionPoly,
ignorePlatformCollision = other.ignorePlatformCollision ?: ignorePlatformCollision,
restDuration = other.restDuration ?: restDuration,
) )
} }
companion object {
val EMPTY = MovementParameters()
}
}
@JsonFactory
data class ActorMovementParameters(
override val mass: Double? = null,
override val gravityMultiplier: Double? = null,
override val liquidBuoyancy: Double? = null,
override val airBuoyancy: Double? = null,
override val bounceFactor: Double? = null,
override val stopOnFirstBounce: Boolean? = null,
override val enableSurfaceSlopeCorrection: Boolean? = null,
override val slopeSlidingFactor: Double? = null,
override val maxMovementPerStep: Double? = null,
override val maximumCorrection: Double? = null,
override val speedLimit: Double? = null,
override val stickyCollision: Boolean? = null,
override val stickyForce: Double? = null,
override val airFriction: Double? = null,
override val liquidFriction: Double? = null,
override val groundFriction: Double? = null,
override val collisionEnabled: Boolean? = null,
override val frictionEnabled: Boolean? = null,
override val gravityEnabled: Boolean? = null,
override val maximumPlatformCorrection: Double? = null,
override val maximumPlatformCorrectionVelocityFactor: Double? = null,
override val physicsEffectCategories: ImmutableSet<String>? = null,
@JsonAlias("collisionPoly")
val standingPoly: Either<Poly, ImmutableList<Poly>>? = null,
@JsonAlias("collisionPoly")
val crouchingPoly: Either<Poly, ImmutableList<Poly>>? = null,
val walkSpeed: Double? = null,
val runSpeed: Double? = null,
val flySpeed: Double? = null,
val minimumLiquidPercentage: Double? = null,
val liquidImpedance: Double? = null,
val normalGroundFriction: Double? = null,
val ambulatingGroundFriction: Double? = null,
val groundForce: Double? = null,
val airForce: Double? = null,
val liquidForce: Double? = null,
val airJumpProfile: JumpProfile = JumpProfile.EMPTY,
val liquidJumpProfile: JumpProfile = JumpProfile.EMPTY,
val fallStatusSpeedMin: Double? = null,
val fallThroughSustainFrames: Int? = null,
val groundMovementMinimumSustain: Double? = null,
val groundMovementMaximumSustain: Double? = null,
val groundMovementCheckDistance: Double? = null,
val pathExploreRate: Double? = null,
) : BaseMovementParameters() {
fun merge(other: ActorMovementParameters): ActorMovementParameters {
return ActorMovementParameters(
mass = other.mass ?: mass,
gravityMultiplier = other.gravityMultiplier ?: gravityMultiplier,
liquidBuoyancy = other.liquidBuoyancy ?: liquidBuoyancy,
airBuoyancy = other.airBuoyancy ?: airBuoyancy,
bounceFactor = other.bounceFactor ?: bounceFactor,
stopOnFirstBounce = other.stopOnFirstBounce ?: stopOnFirstBounce,
enableSurfaceSlopeCorrection = other.enableSurfaceSlopeCorrection ?: enableSurfaceSlopeCorrection,
slopeSlidingFactor = other.slopeSlidingFactor ?: slopeSlidingFactor,
maxMovementPerStep = other.maxMovementPerStep ?: maxMovementPerStep,
maximumCorrection = other.maximumCorrection ?: maximumCorrection,
speedLimit = other.speedLimit ?: speedLimit,
stickyCollision = other.stickyCollision ?: stickyCollision,
stickyForce = other.stickyForce ?: stickyForce,
airFriction = other.airFriction ?: airFriction,
liquidFriction = other.liquidFriction ?: liquidFriction,
groundFriction = other.groundFriction ?: groundFriction,
collisionEnabled = other.collisionEnabled ?: collisionEnabled,
frictionEnabled = other.frictionEnabled ?: frictionEnabled,
gravityEnabled = other.gravityEnabled ?: gravityEnabled,
maximumPlatformCorrection = other.maximumPlatformCorrection ?: maximumPlatformCorrection,
maximumPlatformCorrectionVelocityFactor = other.maximumPlatformCorrectionVelocityFactor ?: maximumPlatformCorrectionVelocityFactor,
physicsEffectCategories = other.physicsEffectCategories ?: physicsEffectCategories,
standingPoly = other.standingPoly ?: standingPoly,
crouchingPoly = other.crouchingPoly ?: crouchingPoly,
walkSpeed = other.walkSpeed ?: walkSpeed,
runSpeed = other.runSpeed ?: runSpeed,
flySpeed = other.flySpeed ?: flySpeed,
minimumLiquidPercentage = other.minimumLiquidPercentage ?: minimumLiquidPercentage,
liquidImpedance = other.liquidImpedance ?: liquidImpedance,
normalGroundFriction = other.normalGroundFriction ?: normalGroundFriction,
ambulatingGroundFriction = other.ambulatingGroundFriction ?: ambulatingGroundFriction,
groundForce = other.groundForce ?: groundForce,
airForce = other.airForce ?: airForce,
liquidForce = other.liquidForce ?: liquidForce,
airJumpProfile = airJumpProfile.merge(other.airJumpProfile),
liquidJumpProfile = liquidJumpProfile.merge(other.liquidJumpProfile),
fallStatusSpeedMin = other.fallStatusSpeedMin ?: fallStatusSpeedMin,
fallThroughSustainFrames = other.fallThroughSustainFrames ?: fallThroughSustainFrames,
groundMovementMinimumSustain = other.groundMovementMinimumSustain ?: groundMovementMinimumSustain,
groundMovementMaximumSustain = other.groundMovementMaximumSustain ?: groundMovementMaximumSustain,
groundMovementCheckDistance = other.groundMovementCheckDistance ?: groundMovementCheckDistance,
)
}
companion object {
val EMPTY = ActorMovementParameters()
}
} }

View File

@ -4,10 +4,10 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.IScriptable import ru.dbotthepony.kstarbound.defs.IScriptable
import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory

View File

@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.defs.player
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory @JsonFactory
class PlayerMovementModifiers( class ActorMovementModifiers(
val groundMovementModifier: Double = 1.0, val groundMovementModifier: Double = 1.0,
val liquidMovementModifier: Double = 1.0, val liquidMovementModifier: Double = 1.0,
val speedModifier: Double = 1.0, val speedModifier: Double = 1.0,
@ -14,8 +14,8 @@ class PlayerMovementModifiers(
val facingSuppressed: Boolean = false, val facingSuppressed: Boolean = false,
val movementSuppressed: Boolean = false, val movementSuppressed: Boolean = false,
) { ) {
fun combine(other: PlayerMovementModifiers): PlayerMovementModifiers { fun combine(other: ActorMovementModifiers): ActorMovementModifiers {
return PlayerMovementModifiers( return ActorMovementModifiers(
groundMovementModifier = groundMovementModifier * other.groundMovementModifier, groundMovementModifier = groundMovementModifier * other.groundMovementModifier,
liquidMovementModifier = liquidMovementModifier * other.liquidMovementModifier, liquidMovementModifier = liquidMovementModifier * other.liquidMovementModifier,
speedModifier = speedModifier * other.speedModifier, speedModifier = speedModifier * other.speedModifier,
@ -27,4 +27,8 @@ class PlayerMovementModifiers(
movementSuppressed = movementSuppressed || other.movementSuppressed, movementSuppressed = movementSuppressed || other.movementSuppressed,
) )
} }
companion object {
val EMPTY = ActorMovementModifiers()
}
} }

View File

@ -5,8 +5,8 @@ import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.Species import ru.dbotthepony.kstarbound.defs.Species
import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition

View File

@ -0,0 +1,39 @@
package ru.dbotthepony.kstarbound.util
import ru.dbotthepony.kstarbound.Starbound
class GameTimer(val time: Double = 0.0) {
var timer = time
private set
fun reset() {
timer = time
}
var hasFinished: Boolean
get() = timer <= 0.0
set(value) {
if (value)
timer = 0.0
else
timer = time
}
val percent: Double
get() = if (time != 0.0) timer / time else 0.0
fun invert() {
timer = time - timer
}
fun tick(delta: Double = Starbound.TICK_TIME_ADVANCE): Boolean {
timer = (timer - delta).coerceAtLeast(0.0)
return timer == 0.0
}
fun wrapTick(delta: Double = Starbound.TICK_TIME_ADVANCE): Boolean {
val result = tick(delta)
if (result) reset()
return result
}
}

View File

@ -7,6 +7,7 @@ import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.util.MailboxExecutorService import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.ParallelPerform import ru.dbotthepony.kstarbound.util.ParallelPerform
import ru.dbotthepony.kstarbound.util.filterNotNull
import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.api.TileView
@ -14,6 +15,7 @@ import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.WorldObject
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly
import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
import ru.dbotthepony.kvector.api.IStruct2d import ru.dbotthepony.kvector.api.IStruct2d
@ -24,7 +26,9 @@ import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import java.util.stream.Stream
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>( abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(
@ -184,7 +188,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val chunkMap: ChunkMap = if (size.x <= 32000 && size.y <= 32000) ArrayChunkMap() else SparseChunkMap() val chunkMap: ChunkMap = if (size.x <= 32000 && size.y <= 32000) ArrayChunkMap() else SparseChunkMap()
val random: RandomGenerator = RandomGenerator.of("Xoroshiro128PlusPlus") val random: RandomGenerator = RandomGenerator.of("Xoroshiro128PlusPlus")
var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION) var gravity = Vector2d(0.0, -80.0)
abstract val isClient: Boolean abstract val isClient: Boolean
// used to synchronize read/writes to various world state stuff/memory structure // used to synchronize read/writes to various world state stuff/memory structure
@ -194,7 +198,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
try { try {
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
val entities = lock.withLock { ObjectArrayList(entities) } val entities = lock.withLock { ObjectArrayList(entities) }
ForkJoinPool.commonPool().submit(ParallelPerform(entities.spliterator(), Entity::move)).join() ForkJoinPool.commonPool().submit(ParallelPerform(entities.spliterator(), { it.movement.move() })).join()
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
for (ent in entities) { for (ent in entities) {
@ -247,6 +251,21 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
return result return result
} }
fun collide(with: Poly, filter: Predicate<CollisionPoly>): Stream<Poly.Penetration> {
return queryCollisions(with.aabb.enlarge(1.0, 1.0)).stream()
.filter(filter)
.map { with.intersect(it.poly) }
.filterNotNull()
}
fun polyIntersects(with: Poly, filter: Predicate<CollisionPoly> = Predicate { true }, tolerance: Double = 0.0): Boolean {
return collide(with, filter).anyMatch { it.penetration >= tolerance }
}
fun polyIntersects(with: Poly, filter: Collection<CollisionType>, tolerance: Double = 0.0): Boolean {
return polyIntersects(with, Predicate { it.type in filter }, tolerance)
}
fun gravityAt(pos: IStruct2i): Vector2d { fun gravityAt(pos: IStruct2i): Vector2d {
return gravity return gravity
} }

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.world.api package ru.dbotthepony.kstarbound.world.api
import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Interner
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.util.HashTableInterner import ru.dbotthepony.kstarbound.util.HashTableInterner
import java.io.DataInputStream import java.io.DataInputStream
@ -26,7 +27,7 @@ sealed class AbstractCell {
} }
@JvmStatic @JvmStatic
protected val POOL: Interner<ImmutableCell> = HashTableInterner() protected val POOL: Interner<ImmutableCell> = if (Starbound.DEDUP_CELL_STATES) HashTableInterner() else Interner { it }
val EMPTY: ImmutableCell = POOL.intern(ImmutableCell(AbstractTileState.EMPTY, AbstractTileState.EMPTY, AbstractLiquidState.EMPTY, 0, 0, 0, false)) val EMPTY: ImmutableCell = POOL.intern(ImmutableCell(AbstractTileState.EMPTY, AbstractTileState.EMPTY, AbstractLiquidState.EMPTY, 0, 0, 0, false))
val NULL: ImmutableCell = POOL.intern(ImmutableCell(AbstractTileState.NULL, AbstractTileState.NULL, AbstractLiquidState.EMPTY, 0, 0, 0, false)) val NULL: ImmutableCell = POOL.intern(ImmutableCell(AbstractTileState.NULL, AbstractTileState.NULL, AbstractLiquidState.EMPTY, 0, 0, 0, false))

View File

@ -1,7 +1,10 @@
package ru.dbotthepony.kstarbound.world.api package ru.dbotthepony.kstarbound.world.api
import com.github.benmanes.caffeine.cache.Interner
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.util.HashTableInterner
import java.io.DataInputStream import java.io.DataInputStream
sealed class AbstractLiquidState { sealed class AbstractLiquidState {
@ -18,6 +21,9 @@ sealed class AbstractLiquidState {
stream.skipNBytes(1 + 4 + 4 + 1) stream.skipNBytes(1 + 4 + 4 + 1)
} }
val EMPTY = ImmutableLiquidState() @JvmStatic
protected val POOL: Interner<ImmutableLiquidState> = if (Starbound.DEDUP_CELL_STATES) HashTableInterner() else Interner { it }
val EMPTY: ImmutableLiquidState = POOL.intern(ImmutableLiquidState())
} }
} }

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.api
import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Interner
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
@ -24,7 +25,7 @@ sealed class AbstractTileState {
} }
@JvmStatic @JvmStatic
protected val POOL: Interner<ImmutableTileState> = HashTableInterner() protected val POOL: Interner<ImmutableTileState> = if (Starbound.DEDUP_CELL_STATES) HashTableInterner() else Interner { it }
val EMPTY: ImmutableTileState = POOL.intern(ImmutableTileState(BuiltinMetaMaterials.EMPTY)) val EMPTY: ImmutableTileState = POOL.intern(ImmutableTileState(BuiltinMetaMaterials.EMPTY))
val NULL: ImmutableTileState = POOL.intern(ImmutableTileState(BuiltinMetaMaterials.NULL)) val NULL: ImmutableTileState = POOL.intern(ImmutableTileState(BuiltinMetaMaterials.NULL))

View File

@ -24,8 +24,6 @@ data class MutableLiquidState(
} }
override fun immutable(): ImmutableLiquidState { override fun immutable(): ImmutableLiquidState {
val result = ImmutableLiquidState(def, level, pressure, isInfinite) return POOL.intern(ImmutableLiquidState(def, level, pressure, isInfinite))
if (result == EMPTY) return EMPTY
return result
} }
} }

View File

@ -0,0 +1,429 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.JumpProfile
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.player.ActorMovementModifiers
import ru.dbotthepony.kstarbound.util.GameTimer
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kvector.vector.Vector2d
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.sign
abstract class AbstractActorMovementController : AbstractMovementController() {
abstract var controlRun: Boolean
abstract var controlCrouch: Boolean
abstract var controlDown: Boolean
abstract var lastControlDown: Boolean
abstract var controlFly: Vector2d?
abstract var controlFace: Direction?
abstract var isRunning: Boolean
protected set
abstract var isWalking: Boolean
protected set
abstract var isCrouching: Boolean
protected set
abstract var isFlying: Boolean
protected set
abstract var isFalling: Boolean
protected set
abstract var isJumping: Boolean
protected set
abstract var canJump: Boolean
abstract var controlJump: Boolean
abstract var controlJumpAnyway: Boolean
abstract var lastControlJump: Boolean
protected set
abstract var lastControlCrouch: Boolean
protected set
abstract var isGroundMovement: Boolean
protected set
abstract var isLiquidMovement: Boolean
protected set
abstract var controlRotationRate: Double
abstract var controlAcceleration: Vector2d
abstract var controlForce: Vector2d
abstract var fallThroughSustain: Int
protected set
// Target horizontal velocity for walking / running
abstract var targetHorizontalAmbulatingVelocity: Double
protected set
abstract var controlPathMove: Pair<Vector2d, Boolean>?
abstract var pathMoveResult: Pair<Vector2d, Boolean>?
abstract var controlMove: Direction?
abstract var actorMovementParameters: ActorMovementParameters
abstract var movementModifiers: ActorMovementModifiers
abstract var controlActorMovementParameters: ActorMovementParameters
abstract var controlMovementModifiers: ActorMovementModifiers
abstract val approachVelocities: MutableList<ApproachVelocityCommand>
abstract val approachVelocityAngles: MutableList<ApproachVelocityAngleCommand>
abstract var movingDirection: Direction?
abstract var facingDirection: Direction?
// this is set internally on each move step
final override var movementParameters: MovementParameters = MovementParameters.EMPTY
abstract var anchorEntity: Entity?
var pathController: PathController? = null
var groundMovementSustainTimer: GameTimer = GameTimer(0.0)
var reJumpTimer: GameTimer = GameTimer(0.0)
var jumpHoldTimer: GameTimer? = null
data class ApproachVelocityCommand(
val target: Vector2d,
val maxControlForce: Double
)
data class ApproachVelocityAngleCommand(
val alongAngle: Double,
val targetVelocity: Double,
val maxControlForce: Double,
val positiveOnly: Boolean
)
fun calculateMovementParameters(base: ActorMovementParameters): MovementParameters {
val mass = base.mass
val gravityMultiplier = base.gravityMultiplier
val liquidBuoyancy = base.liquidBuoyancy
val airBuoyancy = base.airBuoyancy
val bounceFactor = base.bounceFactor
val stopOnFirstBounce = base.stopOnFirstBounce
val enableSurfaceSlopeCorrection = base.enableSurfaceSlopeCorrection
val slopeSlidingFactor = base.slopeSlidingFactor
val maxMovementPerStep = base.maxMovementPerStep
val collisionPoly = if (isCrouching) base.crouchingPoly else base.standingPoly
val stickyCollision = base.stickyCollision
val stickyForce = base.stickyForce
val airFriction = base.airFriction
val liquidFriction = base.liquidFriction
// If we are traveling in the correct direction while in a movement mode that
// requires contact with the ground (ambulating i.e. walking or running), and
// not traveling faster than our target horizontal movement, then apply the
// special 'ambulatingGroundFriction'.
val relativeXVelocity = velocity.x - surfaceVelocity.x
val useAmbulatingGroundFriction = (isWalking || isRunning) &&
targetHorizontalAmbulatingVelocity.sign == relativeXVelocity.sign &&
relativeXVelocity.absoluteValue <= targetHorizontalAmbulatingVelocity.absoluteValue
val groundFriction = if (useAmbulatingGroundFriction) base.ambulatingGroundFriction else base.normalGroundFriction
val ignorePlatformCollision = fallThroughSustain > 0 || controlFly != null || controlDown
val collisionEnabled = base.collisionEnabled
val frictionEnabled = base.frictionEnabled
val gravityEnabled = base.gravityEnabled
val maximumPlatformCorrection = base.maximumPlatformCorrection
val maximumPlatformCorrectionVelocityFactor = base.maximumPlatformCorrectionVelocityFactor
val physicsEffectCategories = base.physicsEffectCategories
val maximumCorrection = base.maximumCorrection
val speedLimit = base.speedLimit
return MovementParameters(
mass = mass,
gravityMultiplier = gravityMultiplier,
liquidBuoyancy = liquidBuoyancy,
airBuoyancy = airBuoyancy,
bounceFactor = bounceFactor,
stopOnFirstBounce = stopOnFirstBounce,
enableSurfaceSlopeCorrection = enableSurfaceSlopeCorrection,
slopeSlidingFactor = slopeSlidingFactor,
maxMovementPerStep = maxMovementPerStep,
maximumCorrection = maximumCorrection,
speedLimit = speedLimit,
stickyCollision = stickyCollision,
stickyForce = stickyForce,
airFriction = airFriction,
liquidFriction = liquidFriction,
groundFriction = groundFriction,
collisionEnabled = collisionEnabled,
frictionEnabled = frictionEnabled,
gravityEnabled = gravityEnabled,
maximumPlatformCorrection = maximumPlatformCorrection,
maximumPlatformCorrectionVelocityFactor = maximumPlatformCorrectionVelocityFactor,
physicsEffectCategories = physicsEffectCategories,
ignorePlatformCollision = ignorePlatformCollision,
collisionPoly = collisionPoly,
)
}
fun updateMovementParameters(base: ActorMovementParameters): MovementParameters {
val params = calculateMovementParameters(base)
movementParameters = params
return params
}
open fun clearControls() {
controlRotationRate = 0.0
controlAcceleration = Vector2d.ZERO
controlForce = Vector2d.ZERO
controlRun = false
controlCrouch = false
controlJump = false
controlJumpAnyway = false
controlFly = null
controlPathMove = null
controlActorMovementParameters = ActorMovementParameters.EMPTY
controlMovementModifiers = ActorMovementModifiers.EMPTY
}
override fun move() {
// TODO: anchor entity
if (anchorEntity?.isRemoved == true)
anchorEntity = null
val anchorEntity = anchorEntity
if (anchorEntity != null) {
controlRun = false
controlCrouch = false
isFlying = false
isFalling = false
canJump = false
controlJump = false
isGroundMovement = false
isLiquidMovement = false
velocity = (anchorEntity.position - position) / Starbound.TICK_TIME_ADVANCE
super.move()
position = anchorEntity.position
} else {
val movementParameters = actorMovementParameters.merge(controlActorMovementParameters)
val movementModifiers = movementModifiers.combine(controlMovementModifiers)
if (movementModifiers.movementSuppressed) {
controlMove = null
controlRun = false
controlCrouch = false
controlDown = false
controlJump = false
controlPathMove = null
}
// controlling any other movement overrides the pathing
if (controlMove != null || controlCrouch || controlDown || controlJump || controlFly != null || approachVelocities.isNotEmpty() || approachVelocityAngles.isNotEmpty()) {
controlPathMove = null
}
if (controlPathMove != null && pathMoveResult == null) {
if (appliedForceRegion) {
pathController?.reset()
} else if (!pathController!!.isPathfinding) {
// TODO: path move code
} else {
}
} else {
pathController = null
}
if (controlFly != null)
controlMove = null
if ((controlDown && !lastControlDown) || controlFly != null)
fallThroughSustain = movementParameters.fallThroughSustainFrames ?: 0
else if (fallThroughSustain > 0)
fallThroughSustain--
updateMovementParameters(movementParameters)
targetHorizontalAmbulatingVelocity = 0.0
rotation = (rotation + controlRotationRate * Starbound.TICK_TIME_ADVANCE) % (PI * 2.0)
velocity += controlAcceleration * Starbound.TICK_TIME_ADVANCE + controlForce / mass * Starbound.TICK_TIME_ADVANCE
approachVelocities.forEach {
approachVelocity(it.target, it.maxControlForce)
}
approachVelocityAngles.forEach {
approachVelocityAlongAngle(it.alongAngle, it.targetVelocity, it.maxControlForce, it.positiveOnly)
}
isLiquidMovement = liquidPercentage >= (actorMovementParameters.minimumLiquidPercentage ?: 0.0)
val liquidImpedance = liquidPercentage * (actorMovementParameters.liquidImpedance ?: 0.0)
var updatedMovingDirection: Direction? = null
val isRunning = controlRun && !movementModifiers.runningSuppressed
if (controlFly != null) {
var flyVelocity = controlFly!!
if (flyVelocity.lengthSquared != 0.0)
flyVelocity = flyVelocity.unitVector * (actorMovementParameters.flySpeed ?: 0.0)
if (isLiquidMovement)
approachVelocity(flyVelocity * (1.0 - liquidImpedance) * movementModifiers.speedModifier, (movementParameters.liquidForce ?: 0.0) * movementModifiers.liquidMovementModifier)
else
approachVelocity(flyVelocity * movementModifiers.speedModifier, movementParameters.airForce ?: 0.0)
if (flyVelocity.x > 0.0)
updatedMovingDirection = Direction.RIGHT
else if (flyVelocity.x < 0.0)
updatedMovingDirection = Direction.LEFT
groundMovementSustainTimer = GameTimer(0.0)
} else {
val jumpModifier: Double
val jumpProfile: JumpProfile
if (isLiquidMovement) {
jumpModifier = movementModifiers.liquidJumpModifier
jumpProfile = movementParameters.liquidJumpProfile.copy(jumpSpeed = (movementParameters.liquidJumpProfile.jumpSpeed ?: 0.0) * (1.0 - liquidImpedance))
} else {
jumpModifier = movementModifiers.airJumpModifier
jumpProfile = movementParameters.airJumpProfile
}
var startJump = false
var holdJump = false
// If we are on the ground, then reset the ground movement sustain timer
// to the maximum. If we are not on the ground or near the ground
// according to the nearGroundCheckDistance, and we are past the minimum
// sustain time, then go ahead and immediately clear the ground movement
// sustain timer.
val minGroundSustain = movementParameters.groundMovementMinimumSustain ?: 0.0
val maxGroundSustain = movementParameters.groundMovementMaximumSustain ?: 0.0
val groundCheckDistance = movementParameters.groundMovementCheckDistance ?: 0.0
groundMovementSustainTimer.tick()
if (isOnGround) {
groundMovementSustainTimer = GameTimer(maxGroundSustain)
} else if (!groundMovementSustainTimer.hasFinished && groundCheckDistance > 0.0 && maxGroundSustain - groundMovementSustainTimer.timer > minGroundSustain) {
val collideAny = localHitboxes
.map { it + Vector2d(0.0, -groundCheckDistance) }
.anyMatch {
world.polyIntersects(it, { it.type >= CollisionType.PLATFORM })
}
if (collideAny)
groundMovementSustainTimer = GameTimer(0.0)
}
val standingJumpable = !groundMovementSustainTimer.hasFinished
val controlJump = this.controlJump && (!movementModifiers.jumpingSuppressed || controlJumpAnyway)
// We are doing a jump if m_reJumpTimer has run out and there has been a
// new m_controlJump command which was just recently triggered. If
// jumpProfile.autoJump is set, then we don't care whether it is a new
// m_controlJump command, m_controlJump can be held.
if (reJumpTimer.hasFinished && controlJump && (jumpProfile.autoJump == true || !lastControlJump)) {
if (standingJumpable || jumpProfile.multiJump == true || controlJumpAnyway)
startJump = true
} else if (isJumping && controlJump && jumpHoldTimer?.hasFinished != true) {
if (jumpProfile.collisionCancelled != true || collisionCorrection.y >= 0.0)
holdJump = true
}
if (startJump) {
isJumping = true
reJumpTimer = GameTimer(jumpProfile.reJumpDelay ?: 0.0)
jumpHoldTimer = if ((jumpProfile.jumpHoldTime ?: 0.0) >= 0.0) {
GameTimer(jumpProfile.jumpHoldTime ?: 0.0)
} else {
null
}
velocity = velocity.copy(y = velocity.y + (jumpProfile.jumpSpeed ?: 0.0) * (jumpProfile.jumpInitialPercentage ?: 0.0) * jumpModifier)
groundMovementSustainTimer = GameTimer(0.0)
} else if (holdJump) {
reJumpTimer.tick()
jumpHoldTimer?.tick()
approachYVelocity((jumpProfile.jumpSpeed ?: 0.0) * jumpModifier, (jumpProfile.jumpControlForce ?: 0.0) * jumpModifier)
} else {
isJumping = false
reJumpTimer.tick()
}
if (controlMove == Direction.LEFT) {
updatedMovingDirection = Direction.LEFT
targetHorizontalAmbulatingVelocity = -1.0 * (if (isRunning) movementParameters.runSpeed ?: 0.0 else movementParameters.walkSpeed ?: 0.0) * movementModifiers.speedModifier
} else if (controlMove == Direction.RIGHT) {
updatedMovingDirection = Direction.RIGHT
targetHorizontalAmbulatingVelocity = 1.0 * (if (isRunning) movementParameters.runSpeed ?: 0.0 else movementParameters.walkSpeed ?: 0.0) * movementModifiers.speedModifier
}
if (isLiquidMovement)
targetHorizontalAmbulatingVelocity *= 1.0 - liquidImpedance
// don't ambulate if we're already moving faster than the target velocity in the direction of ambulation
val ambulationWouldAccelerate = (targetHorizontalAmbulatingVelocity + surfaceVelocity.x).absoluteValue > velocity.x.absoluteValue ||
targetHorizontalAmbulatingVelocity < 0 != velocity.x < 0
if (targetHorizontalAmbulatingVelocity != 0.0 && ambulationWouldAccelerate) {
val ambulatingAccelerate = if (isOnGround)
(movementParameters.groundForce ?: 0.0) * movementModifiers.groundMovementModifier
else if (isLiquidMovement)
(movementParameters.liquidForce ?: 0.0) * movementModifiers.liquidMovementModifier
else
movementParameters.airForce ?: 0.0
approachXVelocity(targetHorizontalAmbulatingVelocity + surfaceVelocity.x, ambulatingAccelerate)
}
}
movingDirection = updatedMovingDirection
if (!movementModifiers.facingSuppressed) {
if (controlFace != null)
facingDirection = controlFace
else if (updatedMovingDirection != null)
facingDirection = updatedMovingDirection
else if (controlPathMove != null && pathController?.controlFace != null)
facingDirection = pathController?.controlFace
}
isGroundMovement = !groundMovementSustainTimer.hasFinished
if (isGroundMovement) {
this.isRunning = isRunning && controlMove != null
this.isWalking = !isRunning && controlMove != null
this.isCrouching = controlCrouch && controlMove != null
}
isFlying = controlFly != null
isFalling = (velocity.y < (movementParameters.fallStatusSpeedMin ?: 0.0)) && !isGroundMovement
super.move()
lastControlDown = controlDown
lastControlJump = controlJump
if (isLiquidMovement)
canJump = reJumpTimer.hasFinished && (!groundMovementSustainTimer.hasFinished || movementParameters.liquidJumpProfile.multiJump == true)
else
canJump = reJumpTimer.hasFinished && (!groundMovementSustainTimer.hasFinished || movementParameters.airJumpProfile.multiJump == true)
}
clearControls()
}
}

View File

@ -0,0 +1,492 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly
import ru.dbotthepony.kvector.util.linearInterpolation
import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.times
import java.util.stream.Stream
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.acos
import kotlin.math.cos
import kotlin.math.sin
abstract class AbstractMovementController {
abstract val world: World<*, *>
val localHitboxes: Stream<Poly>
get() { return (movementParameters.collisionPoly?.map({ Stream.of(it) }, { it.stream() }) ?: return Stream.of()).map { it.rotate(rotation) + position } }
abstract var position: Vector2d
open fun shouldCollideWithType(type: CollisionType): Boolean {
return type !== CollisionType.NONE
}
open fun shouldCollideWithBody(body: CollisionPoly): Boolean {
return shouldCollideWithType(body.type)
}
// Movement variables
abstract var isOnGround: Boolean
protected set
abstract var isColliding: Boolean
protected set
abstract var isCollisionStuck: Boolean
protected set
abstract var isCollidingWithNull: Boolean
protected set
abstract var stickingDirection: Double?
protected set
abstract var surfaceSlope: Vector2d
protected set
abstract var surfaceVelocity: Vector2d
protected set
abstract var collisionCorrection: Vector2d
protected set
abstract var liquidPercentage: Double
protected set
abstract var appliedForceRegion: Boolean
protected set
abstract var isZeroGravity: Boolean
protected set
abstract var movementParameters: MovementParameters
abstract var rotation: Double
open var gravityMultiplier = 1.0
open var isGravityDisabled = false
val mass: Double
get() = movementParameters.mass ?: 1.0
var velocity = Vector2d.ZERO
fun determineGravity(): Vector2d {
if (isZeroGravity || isGravityDisabled)
return Vector2d.ZERO
return world.gravityAt(position)
}
open fun updateLiquidPercentage() {
}
open fun updateForceRegions() {
}
fun approachVelocity(targetVelocity: Vector2d, maxControlForce: Double) {
// Instead of applying the force directly, work backwards and figure out the
// maximum acceleration that could be achieved by the current control force,
// and maximize the change in velocity based on that.
val diff = targetVelocity - velocity
val mag = diff.length
if (mag == 0.0) return
val maximumAcceleration = maxControlForce / mass * Starbound.TICK_TIME_ADVANCE
val clampedMag = mag.coerceIn(0.0, maximumAcceleration)
velocity += diff * (clampedMag / mag)
}
fun approachVelocityAlongAngle(angle: Double, targetVelocity: Double, maxControlForce: Double, positiveOnly: Boolean = false) {
// Same strategy as approachVelocity, work backwards to figure out the
// maximum acceleration and apply that.
// Project the current velocity along the axis normal, the velocity
// difference is the difference between the targetVelocity and this
// projection.
val axis = Vector2d(cos(angle), sin(angle))
val velocityAlongAxis = velocity.dot(axis)
val diff = targetVelocity - velocityAlongAxis
if (diff == 0.0 || positiveOnly && diff < 0.0) return
val maximumAcceleration = maxControlForce / mass * Starbound.TICK_TIME_ADVANCE
val diffMag = diff.absoluteValue
val clampedMag = diffMag.coerceIn(0.0, maximumAcceleration)
velocity += axis * diff * (clampedMag / diffMag)
}
fun approachXVelocity(velocity: Double, maxControlForce: Double) {
approachVelocityAlongAngle(0.0, velocity, maxControlForce)
}
fun approachYVelocity(velocity: Double, maxControlForce: Double) {
approachVelocityAlongAngle(PI / 2.0, velocity, maxControlForce)
}
/**
* this function is executed in parallel
*/
// TODO: Ghost collisions occur, where objects trip on edges
open fun move() {
isZeroGravity = isGravityDisabled || gravityMultiplier == 0.0 || determineGravity().lengthSquared == 0.0
val movementParameters = movementParameters
movementParameters.speedLimit?.let {
if (velocity.length > it)
velocity = velocity.unitVector * it
}
// TODO: Here: moving platforms sticky code
if (movementParameters.collisionPoly == null || !movementParameters.collisionPoly.map({ true }, { it.isNotEmpty() }) || movementParameters.collisionEnabled != true) {
position += velocity * Starbound.TICK_TIME_ADVANCE
surfaceSlope = Vector2d.POSITIVE_Y
surfaceVelocity = Vector2d.ZERO
isOnGround = false
stickingDirection = null
isColliding = false
isCollidingWithNull = false
isCollisionStuck = false
return
}
var steps = 1
movementParameters.maxMovementPerStep?.let {
steps = (velocity.length * Starbound.TICK_TIME_ADVANCE / it).toInt() + 1
}
var relativeVelocity = velocity
surfaceSlope = Vector2d.POSITIVE_Y
// TODO: Here: moving platforms sticky code
val dt = Starbound.TICK_TIME_ADVANCE / steps
for (step in 0 until steps) {
val velocityMagnitude = relativeVelocity.length
val velocityDirection = relativeVelocity / velocityMagnitude
val movement = relativeVelocity * dt
val ignorePlatforms = (movementParameters.ignorePlatformCollision ?: false) || relativeVelocity.y > 0.0
val maximumCorrection = movementParameters.maximumCorrection ?: 0.0
val maximumPlatformCorrection = (movementParameters.maximumPlatformCorrection ?: Double.POSITIVE_INFINITY) +
(movementParameters.maximumPlatformCorrectionVelocityFactor ?: 0.0) * velocityMagnitude
val localHitboxes = localHitboxes.toList()
val aabb = localHitboxes.stream().map { it.aabb }.reduce(AABB::combine).get()
var queryBounds = aabb.enlarge(maximumCorrection, maximumCorrection)
queryBounds = queryBounds.combine(queryBounds + movement)
val polies = world.queryCollisions(queryBounds).filter(this::shouldCollideWithBody)
val results = ArrayList<CollisionResult>(localHitboxes.size)
for (hitbox in localHitboxes) {
results.add(collisionSweep(hitbox, polies, movement, ignorePlatforms, (movementParameters.enableSurfaceSlopeCorrection ?: false) && !isZeroGravity, maximumCorrection, maximumPlatformCorrection, aabb.centre))
}
val result = results.minOrNull()!!
position += result.movement
if (result.collisionType == CollisionType.NULL) {
isCollidingWithNull = true
break
} else {
isCollidingWithNull = false
}
val correction = result.correction
val normCorrection = correction.unitVector
surfaceSlope = result.groundSlope
collisionCorrection = result.correction
isColliding = correction != Vector2d.ZERO || result.isStuck
isOnGround = !isZeroGravity && result.isOnGround
isCollisionStuck = result.isStuck
// If we have collided, apply either sticky or normal (bouncing) collision physics
if (correction != Vector2d.ZERO) {
if (movementParameters.stickyCollision == true && result.collisionType !== CollisionType.SLIPPERY) {
// When sticking, cancel all velocity and apply stickyForce in the
// opposite of the direction of collision correction.
relativeVelocity = -normCorrection * (movementParameters.stickyForce ?: 0.0) / mass * dt
stickingDirection = -normCorrection.toAngle()
break
} else {
stickingDirection = null
val correctionDirection = correction.unitVector
if (movementParameters.bounceFactor != null && movementParameters.bounceFactor != 0.0) {
val adjustment = correctionDirection * (velocityMagnitude * (correctionDirection * -velocityDirection))
relativeVelocity += adjustment + movementParameters.bounceFactor * adjustment
if (movementParameters.stopOnFirstBounce == true) {
// When bouncing, stop integrating at the moment of bounce. This
// prevents the frame of contact from being missed due to multiple
// iterations per frame.
break
}
} else {
// Only adjust the velocity to the extent that the collision was
// caused by the velocity in each axis, to eliminate collision
// induced velocity in a platformery way (each axis considered
// independently).
if (relativeVelocity.x < 0.0 && correction.x > 0.0)
relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TICK_TIME_ADVANCE).coerceAtMost(0.0))
else if (relativeVelocity.x > 0.0 && correction.x < 0.0)
relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TICK_TIME_ADVANCE).coerceAtLeast(0.0))
if (relativeVelocity.y < 0.0 && correction.y > 0.0)
relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TICK_TIME_ADVANCE).coerceAtMost(0.0))
else if (relativeVelocity.y > 0.0 && correction.y < 0.0)
relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TICK_TIME_ADVANCE).coerceAtLeast(0.0))
}
}
}
}
var newVelocity = relativeVelocity
updateLiquidPercentage()
// TODO: sticky collision update
// In order to make control work accurately, passive forces need to be
// applied to velocity *after* integrating. This prevents control from
// having to account for one timestep of passive forces in order to result
// in the correct controlled movement.
if (!isZeroGravity && stickingDirection == null) {
val buoyancy = (movementParameters.liquidBuoyancy ?: 0.0).coerceIn(0.0, 1.0) + liquidPercentage + (movementParameters.airBuoyancy ?: 0.0).coerceIn(0.0, 1.0) * (1.0 - liquidPercentage)
val gravity = determineGravity() * (movementParameters.gravityMultiplier ?: 1.0) * (1.0 - buoyancy)
var environmentVelocity = gravity * Starbound.TICK_TIME_ADVANCE
if (isOnGround && (movementParameters.slopeSlidingFactor ?: 0.0) != 0.0 && surfaceSlope != Vector2d.ZERO)
environmentVelocity += -surfaceSlope * (surfaceSlope.x * surfaceSlope.y) * (movementParameters.slopeSlidingFactor ?: 0.0)
newVelocity += environmentVelocity
}
if (movementParameters.frictionEnabled == true) {
var refVel = Vector2d.ZERO
var friction = liquidPercentage * (movementParameters.liquidFriction ?: 0.0).coerceIn(0.0, 1.0) + (1.0 - liquidPercentage) * (movementParameters.airFriction ?: 0.0).coerceIn(0.0, 1.0)
if (isOnGround) {
friction = friction.coerceAtLeast(movementParameters.groundFriction ?: 0.0)
refVel = surfaceVelocity
}
// The equation for friction here is effectively:
// frictionForce = friction * (refVel - velocity)
// but it is applied here as a multiplicative factor from [0, 1] so it does
// not induce oscillation at very high friction and so it cannot be
// negative.
val frictionFactor = (friction / mass * Starbound.TICK_TIME_ADVANCE).coerceIn(0.0, 1.0)
newVelocity = linearInterpolation(frictionFactor, newVelocity, refVel)
}
velocity = newVelocity
updateForceRegions()
}
protected data class CollisionSeparation(
var correction: Vector2d = Vector2d.ZERO,
var solutionFound: Boolean = false,
var collisionType: CollisionType = CollisionType.NONE,
var axis: Vector2d = Vector2d.POSITIVE_Y,
var movingCollisionId: Int? = null, // TODO
)
protected data class CollisionResult(
val movement: Vector2d = Vector2d.ZERO,
val correction: Vector2d = Vector2d.ZERO,
var movingCollisionId: Int? = null, // TODO
var isStuck: Boolean = false,
var isOnGround: Boolean = false,
var groundSlope: Vector2d = Vector2d.ZERO,
var collisionType: CollisionType = CollisionType.NULL,
) : Comparable<CollisionResult> {
override fun compareTo(other: CollisionResult): Int {
return movement.lengthSquared.compareTo(other.movement.lengthSquared)
}
}
protected fun collisionSweep(
body: Poly, staticBodies: List<CollisionPoly>,
movement: Vector2d, ignorePlatforms: Boolean,
slopeCorrection: Boolean, maximumCorrection: Double,
maximumPlatformCorrection: Double, sortCenter: Vector2d
): CollisionResult {
val translatedBody = body + movement
var checkBody = translatedBody
var maxCollided = CollisionType.NONE
var separation = CollisionSeparation()
var totalCorrection = Vector2d.ZERO
var movingCollisionId: Int? = null
val sorted = staticBodies.stream()
.map { it to (it.poly.aabb.centre - sortCenter).lengthSquared }
.sorted { o1, o2 -> o1.second.compareTo(o2.second) }
.map { it.first }
.toList()
if (slopeCorrection) {
// Starbound: First try separating with our ground sliding cheat.
separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, true, Entity.SEPARATION_TOLERANCE)
totalCorrection += separation.correction
checkBody += separation.correction
maxCollided = maxCollided.maxOf(separation.collisionType)
movingCollisionId = separation.movingCollisionId
val upwardResult = movement + separation.correction
val upwardMagnitude = upwardResult.length
val upwardUnit = upwardResult / upwardMagnitude
// Starbound: Angle off of horizontal (minimum of either direction)
val horizontalAngle = acos(Vector2d.POSITIVE_X.dot(upwardUnit)).coerceAtMost(acos(Vector2d.NEGATIVE_X.dot(upwardUnit)))
// Starbound: We need to make sure that even if we found a solution with the sliding
// Starbound: cheat, we are not beyond the angle and correction limits for the ground
// Starbound: cheat correction.
separation.solutionFound = separation.solutionFound && (upwardMagnitude < 0.2 || horizontalAngle < PI / 3.0) && totalCorrection.length <= maximumCorrection
// KStarbound: if we got pushed into world geometry, then consider slide cheat didn't find a solution
if (separation.solutionFound) {
separation.solutionFound = staticBodies.all { it.poly.intersect(checkBody).let { it == null || it.penetration.absoluteValue <= Entity.SEPARATION_TOLERANCE } }
}
}
if (!separation.solutionFound) {
checkBody = translatedBody
totalCorrection = Vector2d.ZERO
movingCollisionId = null
for (i in 0 until Entity.SEPARATION_STEPS) {
separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, false, Entity.SEPARATION_TOLERANCE)
totalCorrection += separation.correction
checkBody += separation.correction
maxCollided = maxCollided.maxOf(separation.collisionType)
if (totalCorrection.length >= maximumCorrection) {
separation.solutionFound = false
break
} else if (separation.solutionFound) {
break
}
}
}
if (!separation.solutionFound && movement != Vector2d.ZERO) {
checkBody = body
totalCorrection = -movement
for (i in 0 until Entity.SEPARATION_STEPS) {
separation = collisionSeparate(checkBody, sorted, true, maximumPlatformCorrection, false, Entity.SEPARATION_TOLERANCE)
totalCorrection += separation.correction
checkBody += separation.correction
maxCollided = maxCollided.maxOf(separation.collisionType)
if (totalCorrection.length >= maximumCorrection) {
separation.solutionFound = false
break
} else if (separation.solutionFound) {
break
}
}
}
if (separation.solutionFound) {
val result = CollisionResult(
movement = movement + totalCorrection,
correction = totalCorrection,
isStuck = false,
isOnGround = -totalCorrection.dot(determineGravity()) > Entity.SEPARATION_TOLERANCE,
movingCollisionId = movingCollisionId,
collisionType = maxCollided,
// groundSlope = Vector2d.POSITIVE_Y,
groundSlope = separation.axis
)
// TODO: what they wanted to achieve with this?
/*if (result.isOnGround) {
// If we are on the ground and need to find the ground slope, look for a
// vertex on the body being moved that is touching an edge of one of the
// collision polys. We only want a slope to be produced from an edge of
// colision geometry, not an edge of the colliding body. Pick the
// touching edge that is the most horizontally overlapped with the
// geometry, rather than off to the side.
var maxSideHorizontalOverlap = 0.0
var touchingBounds = checkBody.aabb.enlarge(SEPARATION_TOLERANCE, SEPARATION_TOLERANCE)
for (poly in staticBodies) {
for (edge in poly.poly.edges) {
}
}
}*/
return result
} else {
return CollisionResult(Vector2d.ZERO, -movement, null, true, true, Vector2d.POSITIVE_Y, maxCollided)
}
}
protected fun collisionSeparate(
poly: Poly, staticBodies: List<CollisionPoly>,
ignorePlatforms: Boolean, maximumPlatformCorrection: Double,
upward: Boolean, separationTolerance: Double
): CollisionSeparation {
val separation = CollisionSeparation()
var intersects = false
var correctedPoly = poly
for (body in staticBodies) {
if (ignorePlatforms && body.type === CollisionType.PLATFORM)
continue
var result = if (upward)
correctedPoly.intersect(body.poly, Vector2d.POSITIVE_Y, false)
else if (body.type == CollisionType.PLATFORM)
correctedPoly.intersect(body.poly, Vector2d.POSITIVE_Y, true)
else
correctedPoly.intersect(body.poly)
if (body.type === CollisionType.PLATFORM && result != null && (result.penetration <= 0.0 || result.penetration > maximumPlatformCorrection))
result = null
if (result != null) {
intersects = true
correctedPoly += result.vector
separation.correction += result.vector
separation.collisionType = separation.collisionType.maxOf(body.type)
}
}
separation.solutionFound = true
if (intersects) {
for (body in staticBodies) {
if (body.type === CollisionType.PLATFORM)
continue
val result = correctedPoly.intersect(body.poly)
if (result != null && result.penetration > separationTolerance) {
separation.collisionType = separation.collisionType.maxOf(body.type)
separation.solutionFound = false
break
}
}
}
return separation
}
}

View File

@ -1,19 +0,0 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kstarbound.world.World
abstract class ActorEntity(world: World<*, *>) : Entity(world) {
var isWalking = false
var isRunning = false
var isCrouching = false
var isFlying = false
var isFalling = false
var canJump = false
var isJumping = false
var isGroundMovement = false
var isLiquidMovement = false
override fun move() {
super.move()
}
}

View File

@ -1,27 +1,13 @@
package ru.dbotthepony.kstarbound.world.entities package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.defs.BaseMovementParameters
import ru.dbotthepony.kstarbound.defs.ignorePlatformCollision
import ru.dbotthepony.kstarbound.defs.restDuration
import ru.dbotthepony.kstarbound.util.MailboxExecutorService import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly
import ru.dbotthepony.kvector.util.linearInterpolation
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.RGBAColor
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.times
import java.util.EnumSet
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.acos
abstract class Entity(val world: World<*, *>) { abstract class Entity(val world: World<*, *>) {
var chunk: Chunk<*, *>? = null var chunk: Chunk<*, *>? = null
@ -63,7 +49,6 @@ abstract class Entity(val world: World<*, *>) {
val old = field val old = field
field = Vector2d(world.x.cell(value.x), world.y.cell(value.y)) field = Vector2d(world.x.cell(value.x), world.y.cell(value.y))
physicsSleepTicks = 0
if (isSpawned && !isRemoved) { if (isSpawned && !isRemoved) {
val oldChunkPos = world.chunkFromCell(old) val oldChunkPos = world.chunkFromCell(old)
@ -75,53 +60,10 @@ abstract class Entity(val world: World<*, *>) {
} }
} }
var velocity = Vector2d.ZERO abstract val movement: AbstractMovementController
set(value) {
field = value
physicsSleepTicks = 0
}
// Movement variables
var isZeroGravity = false
private set
var isOnGround = false
private set
var isColliding = false
private set
var isCollisionStuck = false
private set
var isCollidingWithNull = false
private set
var stickingDirection: Double? = null
private set
var surfaceSlope = Vector2d.ZERO
private set
var surfaceVelocity = Vector2d.ZERO
private set
var collisionCorrection = Vector2d.ZERO
private set
var liquidPercentage = 0.0
private set
// Movement parameters
open val movementParameters: BaseMovementParameters = GlobalDefaults.movementParameters
var gravityMultiplier = 1.0
var isGravityDisabled = false
var mass = 1.0
set(value) {
require(value > 0.0) { "Invalid mass: $value" }
field = value
}
var physicsSleepTicks = 0
val mailbox = MailboxExecutorService(world.mailbox.thread) val mailbox = MailboxExecutorService(world.mailbox.thread)
protected val hitboxes = ArrayList<Poly>()
protected val collisionFilter: EnumSet<CollisionType> = EnumSet.of(CollisionType.NONE)
/** /**
* true - whitelist, false - blacklist * true - whitelist, false - blacklist
*/ */
@ -186,419 +128,15 @@ abstract class Entity(val world: World<*, *>) {
} }
fun updateLiquidPercentage() {
}
fun updateForceRegions() {
}
fun determineGravity(): Vector2d {
if (isZeroGravity || isGravityDisabled)
return Vector2d.ZERO
return world.gravityAt(position)
}
/**
* this function is executed in parallel
*/
// TODO: Ghost collisions occur, where objects trip on edges
open fun move() {
isZeroGravity = isGravityDisabled || gravityMultiplier == 0.0 || determineGravity().lengthSquared == 0.0
if (!isZeroGravity)
velocity += world.gravity * Starbound.TICK_TIME_ADVANCE
movementParameters.speedLimit?.let {
if (velocity.length > it)
velocity = velocity.unitVector * it
}
// TODO: Here: moving platforms sticky code
if (hitboxes.isEmpty() || movementParameters.collisionEnabled != true) {
position += velocity * Starbound.TICK_TIME_ADVANCE
surfaceSlope = Vector2d.POSITIVE_Y
surfaceVelocity = Vector2d.ZERO
isOnGround = false
stickingDirection = null
isColliding = false
isCollidingWithNull = false
isCollisionStuck = false
return
}
var steps = 1
movementParameters.maxMovementPerStep?.let {
steps = (velocity.length * Starbound.TICK_TIME_ADVANCE / it).toInt() + 1
}
var relativeVelocity = if (physicsSleepTicks > 0) {
physicsSleepTicks--
Vector2d.ZERO
} else {
velocity
}
val originalMovement = relativeVelocity * Starbound.TICK_TIME_ADVANCE
surfaceSlope = Vector2d.POSITIVE_Y
// TODO: Here: moving platforms sticky code
val dt = Starbound.TICK_TIME_ADVANCE / steps
for (step in 0 until steps) {
val velocityMagnitude = relativeVelocity.length
val velocityDirection = relativeVelocity / velocityMagnitude
val movement = relativeVelocity * dt
val ignorePlatforms = movementParameters.ignorePlatformCollision || relativeVelocity.y > 0.0
val maximumCorrection = movementParameters.maximumCorrection ?: 0.0
val maximumPlatformCorrection = (movementParameters.maximumPlatformCorrection ?: Double.POSITIVE_INFINITY) +
(movementParameters.maximumPlatformCorrectionVelocityFactor ?: 0.0) * velocityMagnitude
val localHitboxes = hitboxes.map { it + position }
val aabb = localHitboxes.stream().map { it.aabb }.reduce(AABB::combine).get()
var queryBounds = aabb.enlarge(maximumCorrection, maximumCorrection)
queryBounds = queryBounds.combine(queryBounds + movement)
val polies = world.queryCollisions(queryBounds).filter {
if (collisionFilterMode)
it.type in collisionFilter
else
it.type !in collisionFilter
}
val results = ArrayList<CollisionResult>(localHitboxes.size)
for (hitbox in localHitboxes) {
results.add(collisionSweep(hitbox, polies, movement, ignorePlatforms, (movementParameters.enableSurfaceSlopeCorrection ?: false) && !isZeroGravity, maximumCorrection, maximumPlatformCorrection, aabb.centre))
}
val result = results.minOrNull()!!
position += result.movement
if (result.collisionType == CollisionType.NULL) {
isCollidingWithNull = true
break
} else {
isCollidingWithNull = false
}
val correction = result.correction
val normCorrection = correction.unitVector
surfaceSlope = result.groundSlope
collisionCorrection = result.correction
isColliding = correction != Vector2d.ZERO || result.isStuck
isOnGround = !isZeroGravity && result.isOnGround
isCollisionStuck = result.isStuck
// If we have collided, apply either sticky or normal (bouncing) collision physics
if (correction != Vector2d.ZERO) {
if (movementParameters.stickyCollision == true && result.collisionType !== CollisionType.SLIPPERY) {
// When sticking, cancel all velocity and apply stickyForce in the
// opposite of the direction of collision correction.
relativeVelocity = -normCorrection * (movementParameters.stickyForce ?: 0.0) / mass * dt
stickingDirection = -normCorrection.toAngle()
break
} else {
stickingDirection = null
val correctionDirection = correction.unitVector
if (movementParameters.bounceFactor != null && movementParameters.bounceFactor != 0.0) {
val adjustment = correctionDirection * (velocityMagnitude * (correctionDirection * -velocityDirection))
relativeVelocity += adjustment + movementParameters.bounceFactor!! * adjustment
if (movementParameters.stopOnFirstBounce == true) {
// When bouncing, stop integrating at the moment of bounce. This
// prevents the frame of contact from being missed due to multiple
// iterations per frame.
break
}
} else {
// Only adjust the velocity to the extent that the collision was
// caused by the velocity in each axis, to eliminate collision
// induced velocity in a platformery way (each axis considered
// independently).
if (relativeVelocity.x < 0.0 && correction.x > 0.0)
relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TICK_TIME_ADVANCE).coerceAtMost(0.0))
else if (relativeVelocity.x > 0.0 && correction.x < 0.0)
relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TICK_TIME_ADVANCE).coerceAtLeast(0.0))
if (relativeVelocity.y < 0.0 && correction.y > 0.0)
relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TICK_TIME_ADVANCE).coerceAtMost(0.0))
else if (relativeVelocity.y > 0.0 && correction.y < 0.0)
relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TICK_TIME_ADVANCE).coerceAtLeast(0.0))
}
}
}
}
var newVelocity = relativeVelocity
updateLiquidPercentage()
// TODO: sticky collision update
// In order to make control work accurately, passive forces need to be
// applied to velocity *after* integrating. This prevents control from
// having to account for one timestep of passive forces in order to result
// in the correct controlled movement.
if (!isZeroGravity && stickingDirection == null) {
val buoyancy = (movementParameters.liquidBuoyancy ?: 0.0).coerceIn(0.0, 1.0) + liquidPercentage + (movementParameters.airBuoyancy ?: 0.0).coerceIn(0.0, 1.0) * (1.0 - liquidPercentage)
val gravity = determineGravity() * (movementParameters.gravityMultiplier ?: 1.0) * (1.0 - buoyancy)
var environmentVelocity = gravity * Starbound.TICK_TIME_ADVANCE
if (isOnGround && (movementParameters.slopeSlidingFactor ?: 0.0) != 0.0 && surfaceSlope != Vector2d.ZERO)
environmentVelocity += surfaceSlope * (surfaceSlope.x * surfaceSlope.y) * (movementParameters.slopeSlidingFactor ?: 0.0)
newVelocity += environmentVelocity
}
// If original movement was entirely (almost) in the direction of gravity
// and was entirely (almost) cancelled by collision correction, put the
// entity into rest for restDuration
if (
physicsSleepTicks == 0 &&
originalMovement.dot(determineGravity()) in 0.99 .. 1.01 &&
collisionCorrection.dot(determineGravity()) in -1.01 .. -0.99
) {
physicsSleepTicks = movementParameters.restDuration
}
if (movementParameters.frictionEnabled == true) {
var refVel = Vector2d.ZERO
var friction = liquidPercentage * (movementParameters.liquidFriction ?: 0.0).coerceIn(0.0, 1.0) + (1.0 - liquidPercentage) * (movementParameters.airFriction ?: 0.0).coerceIn(0.0, 1.0)
if (isOnGround) {
friction = friction.coerceAtLeast(movementParameters.groundFriction ?: 0.0)
refVel = surfaceVelocity
}
// The equation for friction here is effectively:
// frictionForce = friction * (refVel - velocity)
// but it is applied here as a multiplicative factor from [0, 1] so it does
// not induce oscillation at very high friction and so it cannot be
// negative.
val frictionFactor = (friction / mass * Starbound.TICK_TIME_ADVANCE).coerceIn(0.0, 1.0)
newVelocity = linearInterpolation(frictionFactor, newVelocity, refVel)
}
velocity = newVelocity
updateForceRegions()
}
protected data class CollisionSeparation(
var correction: Vector2d = Vector2d.ZERO,
var solutionFound: Boolean = false,
var collisionType: CollisionType = CollisionType.NONE,
var axis: Vector2d = Vector2d.POSITIVE_Y,
var movingCollisionId: Int? = null, // TODO
)
protected data class CollisionResult(
val movement: Vector2d = Vector2d.ZERO,
val correction: Vector2d = Vector2d.ZERO,
var movingCollisionId: Int? = null, // TODO
var isStuck: Boolean = false,
var isOnGround: Boolean = false,
var groundSlope: Vector2d = Vector2d.ZERO,
var collisionType: CollisionType = CollisionType.NULL,
) : Comparable<CollisionResult> {
override fun compareTo(other: CollisionResult): Int {
return movement.lengthSquared.compareTo(other.movement.lengthSquared)
}
}
protected fun collisionSweep(
body: Poly, staticBodies: List<CollisionPoly>,
movement: Vector2d, ignorePlatforms: Boolean,
slopeCorrection: Boolean, maximumCorrection: Double,
maximumPlatformCorrection: Double, sortCenter: Vector2d
): CollisionResult {
val translatedBody = body + movement
var checkBody = translatedBody
var maxCollided = CollisionType.NONE
var separation = CollisionSeparation()
var totalCorrection = Vector2d.ZERO
var movingCollisionId: Int? = null
val sorted = staticBodies.stream()
.map { it to (it.poly.aabb.centre - sortCenter).lengthSquared }
.sorted { o1, o2 -> o1.second.compareTo(o2.second) }
.map { it.first }
.toList()
if (slopeCorrection) {
// Starbound: First try separating with our ground sliding cheat.
separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, true, SEPARATION_TOLERANCE)
totalCorrection += separation.correction
checkBody += separation.correction
maxCollided = maxCollided.maxOf(separation.collisionType)
movingCollisionId = separation.movingCollisionId
val upwardResult = movement + separation.correction
val upwardMagnitude = upwardResult.length
val upwardUnit = upwardResult / upwardMagnitude
// Starbound: Angle off of horizontal (minimum of either direction)
val horizontalAngle = acos(Vector2d.POSITIVE_X.dot(upwardUnit)).coerceAtMost(acos(Vector2d.NEGATIVE_X.dot(upwardUnit)))
// Starbound: We need to make sure that even if we found a solution with the sliding
// Starbound: cheat, we are not beyond the angle and correction limits for the ground
// Starbound: cheat correction.
separation.solutionFound = separation.solutionFound && (upwardMagnitude < 0.2 || horizontalAngle < PI / 3.0) && totalCorrection.length <= maximumCorrection
// KStarbound: if we got pushed into world geometry, then consider slide cheat didn't find a solution
if (separation.solutionFound) {
separation.solutionFound = staticBodies.all { it.poly.intersect(checkBody).let { it == null || it.penetration.absoluteValue <= SEPARATION_TOLERANCE } }
}
}
if (!separation.solutionFound) {
checkBody = translatedBody
totalCorrection = Vector2d.ZERO
movingCollisionId = null
for (i in 0 until SEPARATION_STEPS) {
separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, false, SEPARATION_TOLERANCE)
totalCorrection += separation.correction
checkBody += separation.correction
maxCollided = maxCollided.maxOf(separation.collisionType)
if (totalCorrection.length >= maximumCorrection) {
separation.solutionFound = false
break
} else if (separation.solutionFound) {
break
}
}
}
if (!separation.solutionFound && movement != Vector2d.ZERO) {
checkBody = body
totalCorrection = -movement
for (i in 0 until SEPARATION_STEPS) {
separation = collisionSeparate(checkBody, sorted, true, maximumPlatformCorrection, false, SEPARATION_TOLERANCE)
totalCorrection += separation.correction
checkBody += separation.correction
maxCollided = maxCollided.maxOf(separation.collisionType)
if (totalCorrection.length >= maximumCorrection) {
separation.solutionFound = false
break
} else if (separation.solutionFound) {
break
}
}
}
if (separation.solutionFound) {
val result = CollisionResult(
movement = movement + totalCorrection,
correction = totalCorrection,
isStuck = false,
isOnGround = -totalCorrection.dot(determineGravity()) > SEPARATION_TOLERANCE,
movingCollisionId = movingCollisionId,
collisionType = maxCollided,
// groundSlope = Vector2d.POSITIVE_Y,
groundSlope = separation.axis
)
// TODO: what they wanted to achieve with this?
/*if (result.isOnGround) {
// If we are on the ground and need to find the ground slope, look for a
// vertex on the body being moved that is touching an edge of one of the
// collision polys. We only want a slope to be produced from an edge of
// colision geometry, not an edge of the colliding body. Pick the
// touching edge that is the most horizontally overlapped with the
// geometry, rather than off to the side.
var maxSideHorizontalOverlap = 0.0
var touchingBounds = checkBody.aabb.enlarge(SEPARATION_TOLERANCE, SEPARATION_TOLERANCE)
for (poly in staticBodies) {
for (edge in poly.poly.edges) {
}
}
}*/
return result
} else {
return CollisionResult(Vector2d.ZERO, -movement, null, true, true, Vector2d.POSITIVE_Y, maxCollided)
}
}
protected fun collisionSeparate(
poly: Poly, staticBodies: List<CollisionPoly>,
ignorePlatforms: Boolean, maximumPlatformCorrection: Double,
upward: Boolean, separationTolerance: Double
): CollisionSeparation {
val separation = CollisionSeparation()
var intersects = false
var correctedPoly = poly
for (body in staticBodies) {
if (ignorePlatforms && body.type === CollisionType.PLATFORM)
continue
var result = if (upward)
correctedPoly.intersect(body.poly, Vector2d.POSITIVE_Y, false)
else if (body.type == CollisionType.PLATFORM)
correctedPoly.intersect(body.poly, Vector2d.POSITIVE_Y, true)
else
correctedPoly.intersect(body.poly)
if (body.type === CollisionType.PLATFORM && result != null && (result.penetration <= 0.0 || result.penetration > maximumPlatformCorrection))
result = null
if (result != null) {
intersects = true
correctedPoly += result.vector
separation.correction += result.vector
separation.collisionType = separation.collisionType.maxOf(body.type)
}
}
separation.solutionFound = true
if (intersects) {
for (body in staticBodies) {
if (body.type === CollisionType.PLATFORM)
continue
val result = correctedPoly.intersect(body.poly)
if (result != null && result.penetration > separationTolerance) {
separation.collisionType = separation.collisionType.maxOf(body.type)
separation.solutionFound = false
break
}
}
}
return separation
}
protected open fun onTouch(velocity: Vector2d, normal: Vector2d, poly: CollisionPoly) {
}
open fun render(client: StarboundClient = StarboundClient.current()) { open fun render(client: StarboundClient = StarboundClient.current()) {
hitboxes.forEach { (it + position).render(client) } val hitboxes = movement.localHitboxes.toList()
if (hitboxes.isEmpty()) return
hitboxes.forEach { it.render(client) }
world.queryCollisions( world.queryCollisions(
hitboxes.stream().map { (it + position).aabb }.reduce(AABB::combine).get().enlarge(2.0, 2.0) hitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().enlarge(2.0, 2.0)
).filter { ).filter(movement::shouldCollideWithBody).forEach { it.poly.render(client, BLOCK_COLLISION_COLOR) }
if (collisionFilterMode)
it.type in collisionFilter
else
it.type !in collisionFilter
}.forEach { it.poly.render(client, BLOCK_COLLISION_COLOR) }
} }
open var maxHealth = 0.0 open var maxHealth = 0.0

View File

@ -0,0 +1,69 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.player.ActorMovementModifiers
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.physics.Poly
import ru.dbotthepony.kvector.vector.Vector2d
class EntityActorMovementController(val entity: Entity) : AbstractActorMovementController() {
override val world: World<*, *> by entity::world
override var position: Vector2d by entity::position
override var actorMovementParameters: ActorMovementParameters = GlobalDefaults.actorMovementParameters
override var lastControlDown: Boolean = false
override var fallThroughSustain: Int = 0
override var targetHorizontalAmbulatingVelocity: Double = 0.0
override var controlRun: Boolean = false
override var controlCrouch: Boolean = false
override var controlDown: Boolean = false
override var controlFly: Vector2d? = null
override var isFlying: Boolean = false
override var isFalling: Boolean = false
override var canJump: Boolean = true
override var controlJump: Boolean = false
override var isGroundMovement: Boolean = false
override var isLiquidMovement: Boolean = false
override var controlPathMove: Pair<Vector2d, Boolean>? = null
override var pathMoveResult: Pair<Vector2d, Boolean>? = null
override var controlMove: Direction? = null
override var movementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY
override var controlActorMovementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY
override var controlMovementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY
override var isOnGround: Boolean = false
override var isColliding: Boolean = false
override var isCollisionStuck: Boolean = false
override var isCollidingWithNull: Boolean = false
override var stickingDirection: Double? = null
override var surfaceSlope: Vector2d = Vector2d.ZERO
override var surfaceVelocity: Vector2d = Vector2d.ZERO
override var collisionCorrection: Vector2d = Vector2d.ZERO
override var liquidPercentage: Double = 0.0
override var appliedForceRegion: Boolean = false
override var isZeroGravity: Boolean = false
override var controlRotationRate: Double = 0.0
override var controlAcceleration: Vector2d = Vector2d.ZERO
override var controlForce: Vector2d = Vector2d.ZERO
override var rotation: Double = 0.0
override var lastControlCrouch: Boolean = false
override var controlFace: Direction? = null
override var isRunning: Boolean = false
override var isWalking: Boolean = false
override var isCrouching: Boolean = false
override var isJumping: Boolean = false
override var lastControlJump: Boolean = false
override var controlJumpAnyway: Boolean = false
override val approachVelocities: MutableList<ApproachVelocityCommand> = ArrayList()
override val approachVelocityAngles: MutableList<ApproachVelocityAngleCommand> = ArrayList()
override var movingDirection: Direction? = null
override var facingDirection: Direction? = null
override var anchorEntity: Entity? = null
}

View File

@ -0,0 +1,26 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.physics.Poly
import ru.dbotthepony.kvector.vector.Vector2d
class EntityMovementController(val entity: Entity) : AbstractMovementController() {
override val world: World<*, *> by entity::world
override var position: Vector2d by entity::position
override var movementParameters: MovementParameters = GlobalDefaults.movementParameters
override var isOnGround: Boolean = false
override var isColliding: Boolean = false
override var isCollisionStuck: Boolean = false
override var isCollidingWithNull: Boolean = false
override var stickingDirection: Double? = null
override var surfaceSlope: Vector2d = Vector2d.ZERO
override var surfaceVelocity: Vector2d = Vector2d.ZERO
override var collisionCorrection: Vector2d = Vector2d.ZERO
override var liquidPercentage: Double = 0.0
override var appliedForceRegion: Boolean = false
override var isZeroGravity: Boolean = false
override var rotation: Double = 0.0
}

View File

@ -1,13 +1,16 @@
package ru.dbotthepony.kstarbound.world.entities package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.util.Either
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
class ItemEntity(world: World<*, *>, val def: IItemDefinition) : Entity(world) { class ItemEntity(world: World<*, *>, val def: IItemDefinition) : Entity(world) {
override val movement = EntityMovementController(this)
init { init {
hitboxes.add(Poly(AABB.rectangle(Vector2d.ZERO, 0.75, 0.75))) movement.movementParameters = movement.movementParameters.copy(collisionPoly = Either.left(Poly(AABB.rectangle(Vector2d.ZERO, 0.75, 0.75))))
} }
} }

View File

@ -0,0 +1,24 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kvector.vector.Vector2d
class PathController(val world: World<*, *>, var edgeTimer: Double = 0.0) {
var startPosition: Vector2d? = null
private set
var endPosition: Vector2d? = null
private set
var controlFace: Direction? = null
private set
fun reset() {
edgeTimer = 0.0
startPosition = null
endPosition = null
controlFace = null
}
val isPathfinding: Boolean
get() = TODO()
}

View File

@ -1,18 +1,45 @@
package ru.dbotthepony.kstarbound.world.entities package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.util.Either
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
class PlayerEntity(world: World<*, *>) : Entity(world) { class PlayerEntity(world: World<*, *>) : Entity(world) {
override val movementParameters: ActorMovementParameters = GlobalDefaults.actorMovementParameters override val movement = EntityActorMovementController(this)
init { init {
GlobalDefaults.actorMovementParameters.standingPoly?.let { movement.actorMovementParameters = movement.actorMovementParameters.merge(
//hitboxes.add(it) Starbound.gson.fromJson("""
hitboxes.add(Starbound.gson.fromJson("""[ [-0.75, -2.0], [-0.35, -2.5], [0.35, -2.5], [0.75, -2.0], [0.75, 0.65], [0.35, 1.22], [-0.35, 1.22], [-0.75, 0.65] ]""", Poly::class.java)) {
} "standingPoly" : [ [-0.75, -2.0], [-0.35, -2.5], [0.35, -2.5], [0.75, -2.0], [0.75, 0.65], [0.35, 1.22], [-0.35, 1.22], [-0.75, 0.65] ],
"crouchingPoly" : [ [-0.75, -2.0], [-0.35, -2.5], [0.35, -2.5], [0.75, -2.0], [0.75, -1], [0.35, -0.5], [-0.35, -0.5], [-0.75, -1] ],
"mass" : 1.6,
// should keep the player from teleporting through walls
"maximumCorrection" : 3,
"maxMovementPerStep" : 0.4,
"liquidFriction" : 13.0,
"normalGroundFriction" : 35.0,
"groundForce" : 250.0,
"airForce" : 50.0,
"liquidForce" : 80.0
}
""".trimIndent(), ActorMovementParameters::class.java)
).merge(Starbound.gson.fromJson("""
{
"flySpeed" : 0,
"airFriction" : 0.5,
"airJumpProfile" : {
"jumpSpeed" : 23.0,
"jumpInitialPercentage" : 0.75,
"jumpHoldTime" : 0.2
}
}
""".trimIndent(), ActorMovementParameters::class.java))
} }
} }

View File

@ -159,6 +159,9 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
} }
fun rotate(radians: Double): Poly { fun rotate(radians: Double): Poly {
if (radians == 0.0)
return this
val sin = sin(radians) val sin = sin(radians)
val cos = cos(radians) val cos = cos(radians)

Binary file not shown.

Before

(image error) Size: 1.9 KiB

After

(image error) Size: 1.4 KiB

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -0.5 10 13" shape-rendering="crispEdges">
<metadata>Made with Pixels to Svg https://codepen.io/shshaw/pen/XbxvNj</metadata>
<path stroke="#4d4d6f" d="M3 0h1M7 0h1M2 1h1M8 1h1M2 4h1M8 4h1M3 6h1M9 6h1M1 8h1M0 11h2" />
<path stroke="#5b627d" d="M4 0h3M3 1h5M2 2h2M5 2h1M2 3h4M3 4h5M2 5h2M1 6h2M1 7h2M2 8h1M2 9h1M1 10h1M2 11h3" />
<path stroke="#d9c5da" d="M4 2h1M5 5h1M7 5h1M4 6h1M8 6h1M4 7h1M4 8h1M4 9h2M8 9h1M5 10h4M5 11h3" />
<path stroke="#c0754b" d="M6 2h1M8 11h1M4 12h1M9 12h1" />
<path stroke="#faba6a" d="M7 2h2M6 3h4M2 12h2M6 12h3" />
<path stroke="#a392b0" d="M4 5h1M8 5h1M4 10h1" />
<path stroke="#f5eaed" d="M6 5h1M5 6h3M5 7h4M5 8h4M6 9h2" />
<path stroke="#322b49" d="M3 7h1M9 7h1M3 8h1M9 8h1M1 9h1M3 9h1M9 9h1M2 10h2M9 10h1" />
</svg>

After

(image error) Size: 796 B

Binary file not shown.

Binary file not shown.

After

(image error) Size: 2.4 KiB

Binary file not shown.