platform wip2

This commit is contained in:
minjaesong
2026-02-08 03:43:37 +09:00
parent b328b609cc
commit 623ee14d93
4 changed files with 62 additions and 32 deletions

View File

@@ -15,6 +15,8 @@ import org.dyn4j.geometry.Vector2
*
* Subclasses must set [platformVelocity] before calling `super.updateImpl(delta)`.
*
* TODO: in the future this must be generalised as a PhysContraption
*
* Created by minjaesong on 2022-02-28.
*/
open class ActorMovingPlatform() : ActorWithBody() {
@@ -23,6 +25,11 @@ open class ActorMovingPlatform() : ActorWithBody() {
constructor(newTilewiseWidth: Int) : this() {
this.tilewiseWidth = newTilewiseWidth
physProp = PhysProperties.MOBILE_OBJECT()
collisionType = COLLISION_KINEMATIC
setHitboxDimension(TILE_SIZE * newTilewiseWidth, TILE_SIZE, 0, 0)
}
/** Actors currently riding this platform, stored by ActorID for serialisation. */
@@ -34,8 +41,11 @@ open class ActorMovingPlatform() : ActorWithBody() {
/** Actual displacement applied this tick (after clampHitbox). */
@Transient private val appliedVelocity = Vector2(0.0, 0.0)
/** Tolerance in pixels for "feet on top of platform" detection. */
@Transient private val MOUNT_TOLERANCE_Y = 2.0
/** Tolerance above platform top for "feet on top" detection (pixels). */
@Transient private val MOUNT_TOLERANCE_ABOVE = (TILE_SIZE / 2).toDouble()//2.0
/** Tolerance below platform top — how far feet can sink before dismount (pixels). */
@Transient private val MOUNT_TOLERANCE_BELOW = (TILE_SIZE / 2).toDouble()
/** Minimum combined Y velocity to count as "jumping up" (prevents mount while jumping). */
@Transient private val JUMP_THRESHOLD_Y = -0.5
@@ -86,12 +96,25 @@ open class ActorMovingPlatform() : ActorWithBody() {
// Compute actual displacement (clampHitbox may have wrapped coordinates)
appliedVelocity.set(hitbox.startX - oldX, hitbox.startY - oldY)
// --- Mount detection and rider management ---
// --- Step 1: Move existing riders BEFORE mount detection ---
// This keeps riders aligned with the platform's new position so the
// mount check doesn't fail when the platform is moving fast.
for (riderId in actorsRiding.toList()) {
val rider = INGAME.getActorByID(riderId) as? ActorWithBody
if (rider != null) {
rider.hitbox.translate(appliedVelocity)
rider.hitbox.setPositionY(hitbox.startY - rider.hitbox.height)
if (rider.externalV.y > 0.0) {
rider.externalV.y = 0.0
}
rider.walledBottom = true
}
}
// --- Step 2: Mount detection (riders are now at correct positions) ---
val ridersToRemove = ArrayList<ActorID>()
// Build set of actors currently on top of this platform
val newRiders = ArrayList<ActorWithBody>()
val currentRiders = ArrayList<ActorWithBody>()
// Check all active actors + actorNowPlaying
val candidates = ArrayList<ActorWithBody>()
@@ -104,10 +127,15 @@ open class ActorMovingPlatform() : ActorWithBody() {
for (actor in candidates) {
val feetY = actor.hitbox.endY
val headY = actor.hitbox.startY
val platTop = hitbox.startY
// Check vertical proximity: feet within tolerance of platform top
val verticallyAligned = Math.abs(feetY - platTop) <= MOUNT_TOLERANCE_Y
// Feet are near platform top: slightly above or sunk partway in
val feetNearPlatTop = feetY >= platTop - MOUNT_TOLERANCE_ABOVE &&
feetY <= platTop + MOUNT_TOLERANCE_BELOW
// Actor's head must be above platform top (prevents mounting from below)
val comingFromAbove = headY < platTop
// Check horizontal overlap
val horizontalOverlap = actor.hitbox.endX > hitbox.startX && actor.hitbox.startX < hitbox.endX
@@ -116,18 +144,24 @@ open class ActorMovingPlatform() : ActorWithBody() {
val combinedVelY = actor.externalV.y + (actor.controllerV?.y ?: 0.0)
val notJumping = combinedVelY >= JUMP_THRESHOLD_Y
if (verticallyAligned && horizontalOverlap && notJumping) {
if (feetNearPlatTop && comingFromAbove && horizontalOverlap && notJumping) {
if (!actorsRiding.contains(actor.referenceID)) {
// New rider — mount and snap
mount(actor)
actor.hitbox.setPositionY(hitbox.startY - actor.hitbox.height)
if (actor.externalV.y > 0.0) {
actor.externalV.y = 0.0
}
actor.walledBottom = true
}
newRiders.add(actor)
currentRiders.add(actor)
}
}
// Dismount actors that are no longer on top
val newRiderIds = newRiders.map { it.referenceID }.toSet()
// --- Step 3: Dismount actors no longer on top ---
val currentRiderIds = currentRiders.map { it.referenceID }.toSet()
for (riderId in actorsRiding.toList()) {
if (riderId !in newRiderIds) {
if (riderId !in currentRiderIds) {
val rider = INGAME.getActorByID(riderId)
if (rider is ActorWithBody) {
dismount(rider)
@@ -138,18 +172,6 @@ open class ActorMovingPlatform() : ActorWithBody() {
}
}
ridersToRemove.forEach { actorsRiding.remove(it) }
// Move riders and suppress their gravity
for (rider in newRiders) {
// Translate rider by platform's actual displacement
rider.hitbox.translate(appliedVelocity)
// Snap rider's feet to platform top
rider.hitbox.setPositionY(hitbox.startY - rider.hitbox.height)
// Suppress gravity for this tick
rider.walledBottom = true
}
}
/**

View File

@@ -582,7 +582,7 @@ open class ActorWithBody : Actor {
// --> Apply more forces <-- //
// Actors are subject to the gravity and the buoyancy if they are not levitating
if (!isNoSubjectToGrav) {
if (!isNoSubjectToGrav && platformsRiding.isEmpty()) {
applyGravitation()
applyBuoyancy()
}
@@ -603,7 +603,14 @@ open class ActorWithBody : Actor {
* If and only if:
* This body is NON-STATIC and the other body is STATIC
*/
if (!isNoCollideWorld) {
if (platformsRiding.isNotEmpty()) {
// Riding a platform: skip displaceHitbox entirely.
// The CCD/collision solver doesn't know about platforms and
// will corrupt the rider's position (bounce, stair-step, etc.).
// Only apply horizontal movement; the platform owns Y.
hitbox.translate(vecSum.x, 0.0)
}
else if (!isNoCollideWorld) {
val (collisionStatus, collisionDamage) = displaceHitbox(true)
@@ -665,7 +672,7 @@ open class ActorWithBody : Actor {
walledLeft = isWalled(hitbox, COLLIDING_LEFT)
walledRight = isWalled(hitbox, COLLIDING_RIGHT)
walledTop = isWalled(hitbox, COLLIDING_TOP)
walledBottom = isWalled(hitbox, COLLIDING_BOTTOM)
walledBottom = isWalled(hitbox, COLLIDING_BOTTOM) || platformsRiding.isNotEmpty()
colliding = isColliding(hitbox)
if (isNoCollideWorld) {

View File

@@ -2,6 +2,7 @@ package net.torvald.terrarum.modulebasegame.console
import net.torvald.terrarum.INGAME
import net.torvald.terrarum.Terrarum
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED
import net.torvald.terrarum.console.ConsoleAlias
import net.torvald.terrarum.console.ConsoleCommand
import net.torvald.terrarum.console.Echo
@@ -13,12 +14,12 @@ import net.torvald.terrarum.modulebasegame.gameactors.ActorTestPlatform
@ConsoleAlias("spawnplatform")
internal object SpawnMovingPlatform : ConsoleCommand {
override fun execute(args: Array<String>) {
val mouseX = Terrarum.mouseX
val mouseY = Terrarum.mouseY
val mouseX = Terrarum.mouseTileX * TILE_SIZED
val mouseY = Terrarum.mouseTileY * TILE_SIZED
val platform = ActorTestPlatform()
// setPosition places bottom-centre at the given point; offset Y so the platform is centred at cursor
platform.setPosition(mouseX, mouseY + platform.hitbox.height / 2.0)
platform.setPosition(mouseX, mouseY)
INGAME.queueActorAddition(platform)

View File

@@ -18,7 +18,7 @@ import kotlin.math.sin
class ActorTestPlatform : ActorMovingPlatform(8) {
/** Movement pattern index (0-3). */
private val pattern: Int = (0..3).random()
private val pattern: Int = 1//(0..3).random()
/** Speed in pixels per tick (2.0 to 4.0). */
private val speed: Double = 2.0 + Math.random() * 2.0