moving platform wip

This commit is contained in:
minjaesong
2026-02-08 02:06:29 +09:00
parent 4997353f83
commit ff9f02322b
6 changed files with 277 additions and 14 deletions

View File

@@ -1,40 +1,192 @@
package net.torvald.terrarum.gameactors
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.graphics.g2d.TextureRegion
import net.torvald.terrarum.INGAME
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
import net.torvald.terrarum.gameactors.ActorID
import net.torvald.terrarum.gameactors.ActorWithBody
import net.torvald.terrarum.gameactors.PhysProperties
import net.torvald.terrarum.modulebasegame.gameactors.ActorHumanoid
import org.dyn4j.geometry.Vector2
/**
* Base class for autonomously moving platforms that carry other actors standing on them.
*
* Subclasses must set [platformVelocity] before calling `super.updateImpl(delta)`.
*
* Created by minjaesong on 2022-02-28.
*/
open class ActorMovingPlatform() : ActorWithBody() {
protected var tilewiseWidth = 3
@Transient protected val actorsRiding = ArrayList<ActorID>() // saving actorID due to serialisation issues
constructor(newTilewiseWidth: Int) : this() {
this.tilewiseWidth = newTilewiseWidth
}
/** Actors currently riding this platform, stored by ActorID for serialisation. */
@Transient protected val actorsRiding = ArrayList<ActorID>()
/** Velocity the platform intends to move this tick. Subclasses set this before calling super.updateImpl(). */
@Transient protected val platformVelocity = Vector2(0.0, 0.0)
/** 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
/** Minimum combined Y velocity to count as "jumping up" (prevents mount while jumping). */
@Transient private val JUMP_THRESHOLD_Y = -0.5
@Transient private var platformTexture: Texture? = null
@Transient private var platformTextureRegion: TextureRegion? = null
init {
physProp = PhysProperties.PHYSICS_OBJECT()
physProp = PhysProperties.MOBILE_OBJECT()
collisionType = COLLISION_KINEMATIC
setHitboxDimension(TILE_SIZE * tilewiseWidth, TILE_SIZE, 0, 0)
}
private fun ensureTexture() {
if (platformTexture == null) {
val w = TILE_SIZE * tilewiseWidth
val h = TILE_SIZE
val pixmap = Pixmap(w, h, Pixmap.Format.RGBA8888)
// grey-blue colour
pixmap.setColor(Color(0.45f, 0.55f, 0.65f, 1f))
pixmap.fill()
// slightly darker border
pixmap.setColor(Color(0.35f, 0.45f, 0.55f, 1f))
pixmap.drawRectangle(0, 0, w, h)
platformTexture = Texture(pixmap)
platformTextureRegion = TextureRegion(platformTexture)
pixmap.dispose()
}
}
override fun updateImpl(delta: Float) {
TODO("Not yet implemented")
// Snapshot position before movement
val oldX = hitbox.startX
val oldY = hitbox.startY
// Set externalV to our platform velocity so super translates the hitbox
externalV.set(platformVelocity.x, platformVelocity.y)
// super.updateImpl handles:
// - sprite updates
// - hitbox.translate(externalV) (since usePhysics=false -> isNoCollideWorld=true)
// - clampHitbox
// - tilewise hitbox cache updates
// - position vector updates
super.updateImpl(delta)
// Compute actual displacement (clampHitbox may have wrapped coordinates)
appliedVelocity.set(hitbox.startX - oldX, hitbox.startY - oldY)
// --- Mount detection and rider management ---
val ridersToRemove = ArrayList<ActorID>()
// Build set of actors currently on top of this platform
val newRiders = ArrayList<ActorWithBody>()
// Check all active actors + actorNowPlaying
val candidates = ArrayList<ActorWithBody>()
INGAME.actorContainerActive.forEach {
if (it is ActorWithBody && it !== this && it !is ActorMovingPlatform) {
candidates.add(it)
}
}
INGAME.actorNowPlaying?.let { candidates.add(it) }
for (actor in candidates) {
val feetY = actor.hitbox.endY
val platTop = hitbox.startY
// Check vertical proximity: feet within tolerance of platform top
val verticallyAligned = Math.abs(feetY - platTop) <= MOUNT_TOLERANCE_Y
// Check horizontal overlap
val horizontalOverlap = actor.hitbox.endX > hitbox.startX && actor.hitbox.startX < hitbox.endX
// Check not jumping upward
val combinedVelY = actor.externalV.y + (actor.controllerV?.y ?: 0.0)
val notJumping = combinedVelY >= JUMP_THRESHOLD_Y
if (verticallyAligned && horizontalOverlap && notJumping) {
if (!actorsRiding.contains(actor.referenceID)) {
mount(actor)
}
newRiders.add(actor)
}
}
// Dismount actors that are no longer on top
val newRiderIds = newRiders.map { it.referenceID }.toSet()
for (riderId in actorsRiding.toList()) {
if (riderId !in newRiderIds) {
val rider = INGAME.getActorByID(riderId)
if (rider is ActorWithBody) {
dismount(rider)
}
else {
ridersToRemove.add(riderId)
}
}
}
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
}
}
/**
* Make the actor its externalV controlled by this platform
* Add an actor to the rider list.
*/
fun mount(actor: ActorWithBody) {
actorsRiding.add(actor.referenceID)
if (!actorsRiding.contains(actor.referenceID)) {
actorsRiding.add(actor.referenceID)
actor.platformsRiding.add(this.referenceID)
}
}
/**
* Make the actor its externalV no longer controlled by this platform
* Remove an actor from the rider list and apply dismount impulse.
*/
fun dismount(actor: ActorWithBody) {
actorsRiding.remove(actor.referenceID)
actor.platformsRiding.remove(this.referenceID)
// Conservation of momentum: add platform velocity as impulse
actor.externalV.x += platformVelocity.x
actor.externalV.y += platformVelocity.y
}
}
override fun drawBody(frameDelta: Float, batch: SpriteBatch) {
if (isVisible) {
ensureTexture()
platformTextureRegion?.let {
drawTextureInGoodPosition(frameDelta, it, batch)
}
}
}
override fun dispose() {
platformTexture?.dispose()
platformTexture = null
platformTextureRegion = null
super.dispose()
}
}

View File

@@ -331,7 +331,7 @@ open class ActorWithBody : Actor {
*
* Also see [net.torvald.terrarum.modulebasegame.gameactors.ActorMovingPlatform.actorsRiding]
*/
@Transient protected val platformsRiding = ArrayList<ActorID>()
@Transient internal val platformsRiding = ArrayList<ActorID>()
/**
* Gravitational Constant G. Load from gameworld.

View File

@@ -1480,8 +1480,16 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) {
actorNowPlaying?.update(delta)*/
}
else {
// Pass 1: update moving platforms first so riders get displaced before their own update
actorContainerActive.forEach {
if (it != actorNowPlaying) {
if (it is ActorMovingPlatform && it != actorNowPlaying) {
it.update(delta)
}
}
// Pass 2: update all non-platform actors with existing callbacks
actorContainerActive.forEach {
if (it !is ActorMovingPlatform && it != actorNowPlaying) {
it.update(delta)
if (it is Pocketed) {
@@ -1515,6 +1523,8 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) {
}
}
}
// Pass 3: update player
actorNowPlaying?.update(delta)
//AmmoMeterProxy(player, uiVitalItem.UI as UIVitalMetre)
}

View File

@@ -0,0 +1,32 @@
package net.torvald.terrarum.modulebasegame.console
import net.torvald.terrarum.INGAME
import net.torvald.terrarum.Terrarum
import net.torvald.terrarum.console.ConsoleAlias
import net.torvald.terrarum.console.ConsoleCommand
import net.torvald.terrarum.console.Echo
import net.torvald.terrarum.modulebasegame.gameactors.ActorTestPlatform
/**
* Created by minjaesong on 2026-02-08.
*/
@ConsoleAlias("spawnplatform")
internal object SpawnMovingPlatform : ConsoleCommand {
override fun execute(args: Array<String>) {
val mouseX = Terrarum.mouseX
val mouseY = Terrarum.mouseY
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)
INGAME.queueActorAddition(platform)
Echo("Spawned ActorTestPlatform at (${"%.1f".format(mouseX)}, ${"%.1f".format(mouseY)})")
}
override fun printUsage() {
Echo("usage: spawnplatform")
Echo("Spawns a test moving platform centred at the mouse cursor.")
}
}

View File

@@ -1,9 +1,77 @@
package net.torvald.terrarum.modulebasegame.gameactors
import net.torvald.terrarum.gameactors.ActorMovingPlatform
import kotlin.math.cos
import kotlin.math.sin
/**
* Test platform that randomly selects a movement pattern on spawn.
*
* Patterns:
* - 0: Horizontal pingpong (sine-eased)
* - 1: Vertical pingpong (sine-eased)
* - 2: Clockwise circular (constant speed)
* - 3: Counter-clockwise circular (constant speed)
*
* Created by minjaesong on 2022-03-02.
*/
class ActorTestPlatform : ActorMovingPlatform() {
}
class ActorTestPlatform : ActorMovingPlatform(8) {
/** Movement pattern index (0-3). */
private val pattern: Int = (0..3).random()
/** Speed in pixels per tick (2.0 to 4.0). */
private val speed: Double = 2.0 + Math.random() * 2.0
/** Current phase angle in radians. */
private var phase: Double = 0.0
/**
* Phase step per tick.
*
* For pingpong: peak speed = amplitude * phaseStep = speed
* period = 128 ticks (~2s), so phaseStep = 2*PI/128
* amplitude = speed / phaseStep
*
* For circular: speed = radius * phaseStep
* using same phaseStep, radius = speed / phaseStep
*/
@Transient private val PERIOD_TICKS = 128.0
@Transient private val phaseStep: Double = 2.0 * Math.PI / PERIOD_TICKS
/** Amplitude for pingpong patterns, radius for circular patterns. */
@Transient private val amplitude: Double = speed / phaseStep
override fun updateImpl(delta: Float) {
val oldPhase = phase
phase += phaseStep
when (pattern) {
0 -> {
// Horizontal pingpong: position = A * sin(phase)
// Velocity = finite difference to prevent float drift
val dx = amplitude * (sin(phase) - sin(oldPhase))
platformVelocity.set(dx, 0.0)
}
1 -> {
// Vertical pingpong: position = A * sin(phase)
val dy = amplitude * (sin(phase) - sin(oldPhase))
platformVelocity.set(0.0, dy)
}
2 -> {
// Clockwise circular: position on circle (cos, sin)
val dx = amplitude * (cos(phase) - cos(oldPhase))
val dy = amplitude * (sin(phase) - sin(oldPhase))
platformVelocity.set(dx, dy)
}
3 -> {
// Counter-clockwise circular: negate Y component
val dx = amplitude * (cos(phase) - cos(oldPhase))
val dy = -(amplitude * (sin(phase) - sin(oldPhase)))
platformVelocity.set(dx, dy)
}
}
super.updateImpl(delta)
}
}