Use them together when you want code to read like a small authored sequence:
- move here
- wait until the motion finishes
- scale up
- wait again
- continue with gameplay
This is much easier to maintain than spreading the same sequence across several booleans and timers.
If coroutines are new to you, read Coroutines and Timers first.
Wait for One Tween
Every tween creation method returns a TweenHandle.
Call wait() on that handle inside a coroutine:
use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\Vector3;
use Lenga\Engine\Tweening\EasingFunction;
use Lenga\Engine\Tweening\Tween;
use Lenga\Engine\Tweening\TweenOptions;
final class IntroMove extends Behaviour
{
public function start(): void
{
$this->startCoroutine($this->runIntro());
}
private function runIntro(): \Generator
{
$move = Tween::moveLocalTo(
$this->transform,
new Vector3(0.0, 2.0, 0.0),
0.5,
TweenOptions::make()->ease(EasingFunction::EaseOutCubic),
);
yield $move->wait();
$this->onIntroComplete();
}
private function onIntroComplete(): void
{
}
}
The script starts the tween, then pauses the coroutine until the tween stops waiting.
Build a Small Sequence
You can chain several tween steps in one coroutine.
private function playPickupReveal(): \Generator
{
$rise = Tween::moveLocalTo(
$this->transform,
new Vector3(0.0, 0.5, 0.0),
0.18,
TweenOptions::make()
->relative()
->ease(EasingFunction::EaseOutQuad),
);
yield $rise->wait();
$pulse = Tween::scaleTo(
$this->transform,
new Vector3(1.25, 1.25, 1.0),
0.12,
TweenOptions::make()->ease(EasingFunction::EaseOutCubic),
);
yield $pulse->wait();
Tween::scaleTo(
$this->transform,
new Vector3(1.0, 1.0, 1.0),
0.12,
TweenOptions::make()->ease(EasingFunction::EaseInOutSine),
);
}
The sequence is readable because each line says what the player should see next.
Run Tweens in Parallel
If you start two tweens before yielding, they run at the same time.
private function popIn(): \Generator
{
$move = Tween::moveLocalTo(
$this->transform,
new Vector3(0.0, 0.4, 0.0),
0.2,
TweenOptions::make()
->relative()
->ease(EasingFunction::EaseOutQuad),
);
Tween::scaleTo(
$this->transform,
new Vector3(1.2, 1.2, 1.0),
0.2,
TweenOptions::make()->ease(EasingFunction::EaseOutCubic),
);
yield $move->wait();
Tween::scaleTo(
$this->transform,
new Vector3(1.0, 1.0, 1.0),
0.12,
TweenOptions::make()->ease(EasingFunction::EaseInOutSine),
);
}
Here, movement and scaling begin together. The coroutine waits for the move handle before continuing.
If the tweens have different durations, wait on the handle that represents the sequence boundary you care about.
Use a Coroutine for Gameplay Beats
This example opens a door, waits for the door movement, then enables the next encounter.
use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\GameObject;
use Lenga\Engine\Core\Vector3;
use Lenga\Engine\Tweening\EasingFunction;
use Lenga\Engine\Tweening\Tween;
use Lenga\Engine\Tweening\TweenOptions;
final class DoorEncounter extends Behaviour
{
public ?GameObject $door = null;
public ?GameObject $enemyWave = null;
public function startEncounter(): void
{
$this->startCoroutine($this->runEncounter());
}
private function runEncounter(): \Generator
{
if ($this->door === null) {
return;
}
$openDoor = Tween::moveLocalTo(
$this->door->transform,
new Vector3(0.0, 3.0, 0.0),
0.45,
TweenOptions::make()
->relative()
->ease(EasingFunction::EaseOutCubic),
);
yield $openDoor->wait();
$this->enemyWave?->setActive(true);
}
}
The door animation is not the gameplay rule. The gameplay rule is "start the enemy wave after the door opens." The tween simply makes the transition visible.
Know What wait() Means
TweenHandle::wait() waits while the tween exists and is still playing.
The coroutine continues when the tween:
- completes
- is cancelled
- no longer exists
- is no longer playing
That is usually the right behaviour for gameplay sequences. If the motion is interrupted, the script should not hang forever.
If you need to distinguish "completed" from "cancelled", check the handle after waiting:
yield $move->wait();
if ($move->isComplete()) {
$this->finishCleanly();
} else {
$this->handleInterruptedMove();
}
Common Mistakes
Waiting on the wrong handle
If several tweens run in parallel, choose the handle that represents the intended boundary.
For example, wait on the longest tween when the next gameplay step should happen after the full effect settles.
Starting a second sequence without cancelling the first
If the same action can be triggered repeatedly, keep the current handle or a local state flag so two sequences do not fight over the same transform.
Hiding gameplay logic inside timing
Do not make important rules depend on a magic duration in a disconnected script. Keep the rule near the sequence:
yield $openDoor->wait();
$this->enemyWave?->setActive(true);
That is easier to understand than hoping another script knows the door takes 0.45 seconds.