mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 12:21:52 +09:00
moving platform wip
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user