Guides Tweening Control Tweens During Gameplay

Tweening 3 min read Updated Apr 2026

Control Tweens During Gameplay

Most tweens can be fire-and-forget. Some need to be controlled.

Keep a TweenHandle when gameplay may need to pause, resume, cancel, replace, or inspect a tween after it starts.

This guide focuses on practical control patterns.

Keep the Handle

Every tween creation method returns a TweenHandle.

use Lenga\Engine\Tweening\TweenHandle;

private ?TweenHandle $currentMove = null;

Store it when the tween represents an active gameplay state:

$this->currentMove = Tween::moveLocalTo(
    $this->transform,
    new Vector3(0.0, 2.0, 0.0),
    0.4,
    TweenOptions::make()->ease(EasingFunction::EaseOutCubic),
);

You do not need a field for every decorative tween. Keep handles for tweens that matter after the line that creates them.

Cancel Before Replacing

If a new command should override an old motion, cancel the old tween first.

private ?TweenHandle $currentMove = null;

public function moveToLane(float $laneY): void
{
    $this->currentMove?->cancel();

    $this->currentMove = Tween::moveTo(
        $this->transform,
        new Vector3($this->transform->position->x, $laneY, 0.0),
        0.18,
        TweenOptions::make()->ease(EasingFunction::EaseOutQuad),
    );
}

This pattern is useful for lane movement, menu focus movement, target reticles, and objects that receive repeated commands.

Without the cancel, multiple tweens may try to write to the same transform at the same time.

Pause and Resume a Tween

Use pause() and resume() when one effect should stop independently of the whole game.

public function pauseMove(): void
{
    $this->currentMove?->pause();
}

public function resumeMove(): void
{
    $this->currentMove?->resume();
}

This is different from engine pause.

  • engine pause freezes scaled time for the whole game
  • handle pause affects one tween
  • unscaled tweens can keep moving during engine pause

Respect or Ignore Game Pause

By default, tweens use scaled gameplay time. They pause naturally when the engine is paused or time scale is zero.

That is right for gameplay motion:

Tween::moveLocalTo($this->transform, new Vector3(0.0, 3.0, 0.0), 0.4);

Use unscaled() for effects that should continue while the game is paused.

Tween::scaleTo(
    $this->transform,
    new Vector3(1.08, 1.08, 1.0),
    0.2,
    TweenOptions::make()
        ->unscaled()
        ->ease(EasingFunction::EaseOutQuad),
);

Good uses for unscaled tweens:

  • pause menu movement
  • selection highlights
  • loading or waiting indicators
  • UI feedback while gameplay is frozen

Avoid unscaled tweens for normal gameplay objects unless that is very intentional.

Inspect State

TweenHandle exposes simple state checks:

if ($this->currentMove?->isPlaying()) {
    // The tween is currently active.
}

if ($this->currentMove?->isComplete()) {
    // The tween reached its end.
}

if (!$this->currentMove?->exists()) {
    // The tween is no longer active.
}

State checks are useful when code needs to avoid retriggering an effect or decide whether an interrupted sequence should continue.

Do not poll state every frame unless there is a clear reason. For ordered sequences, prefer Sequence Tweens with Coroutines.

A Complete Pattern: Hover Lift

This example lifts an object while hovered and returns it when hover ends. It cancels the previous tween before starting the next one so the transform has one active owner.

use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\Vector3;
use Lenga\Engine\Tweening\EasingFunction;
use Lenga\Engine\Tweening\Tween;
use Lenga\Engine\Tweening\TweenHandle;
use Lenga\Engine\Tweening\TweenOptions;

final class HoverLift extends Behaviour
{
    private ?TweenHandle $motion = null;
    private Vector3 $restPosition;

    public function awake(): void
    {
        $this->restPosition = $this->transform->localPosition->clone();
    }

    public function onHoverStart(): void
    {
        $this->motion?->cancel();

        $this->motion = Tween::moveLocalTo(
            $this->transform,
            new Vector3($this->restPosition->x, $this->restPosition->y + 0.25, $this->restPosition->z),
            0.12,
            TweenOptions::make()->ease(EasingFunction::EaseOutQuad),
        );
    }

    public function onHoverEnd(): void
    {
        $this->motion?->cancel();

        $this->motion = Tween::moveLocalTo(
            $this->transform,
            $this->restPosition->clone(),
            0.14,
            TweenOptions::make()->ease(EasingFunction::EaseInOutSine),
        );
    }
}

This keeps the important rule simple: hover owns the object's lift. The tween only handles the smoothing.

Handle Interrupted Motion Deliberately

When a tween is interrupted, decide what the object should do.

Good options:

  • cancel the old tween and start a new one from the current transform
  • ignore the new request until the current tween completes
  • snap to a known state before starting the new tween
  • let a coroutine branch based on isComplete()

Avoid letting several unrelated scripts tween the same transform without an ownership rule. That may look fine in a simple test scene and become confusing later.

Keep Ownership Clear

The safest tween code has one obvious owner.

Before you start a tween, ask:

  • which script owns this motion?
  • should a new request cancel the old motion or wait for it?
  • should this motion follow game pause or real time?
  • what state should the object be in if the motion is interrupted?

Answering those questions keeps tweening from becoming hidden state. The player sees smooth motion, and the code still tells the truth about the game rule behind it.

Read Next