That means:
- you add collider components such as
BoxCollider3D,CapsuleCollider3D, andSphereCollider3D - you either move through
CharacterControlleror usePhysics3Dqueries from PHP - your gameplay code stays explicit about how movement and responses should work
This is a good fit for 3D gameplay that needs reliable blocking and scene awareness without a full rigidbody 3D physics stack.
Before you start writing movement code, it also helps to know the core scene API well:
Core 3D Collision Surface
BoxCollider3DCapsuleCollider3DSphereCollider3DCharacterControllerPhysics3D::overlapSphereAll(...)Physics3D::overlapBoxAll(...)Physics3D::overlapCapsuleAll(...)Physics3D::raycast(...)Physics3D::raycastAll(...)- collider
isTouching(...) - collider
getContacts(...) Collision3DonCollisionEnter/onCollisionStay/onCollisionExitonTriggerEnter/onTriggerStay/onTriggerExit
Basic Scene Setup
For a simple rolling or moving actor:
- Add a visible render component such as
SphereRendererorCubeRenderer. - Add a matching collider, usually
SphereCollider3D,CapsuleCollider3D, orBoxCollider3D. - Add colliders to the level geometry you want to block movement.
- Drive movement from PHP with
CharacterControllerwhen you want built-in blocking and slide handling, or with explicitPhysics3Dqueries when you want lower-level control.
For example:
- the player ball in
RollerWorldusesSphereCollider3D - the ground and lane walls use
BoxCollider3D - upright characters can use
CapsuleCollider3Dwhen a sphere is too blunt
Recommended Movement Pattern
For most gameplay movement, CharacterController is the recommended starting point:
use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\CharacterController;
use Lenga\Engine\Core\Vector3;
use Lenga\Engine\Core\Time;
final class RollerController extends Behaviour
{
private ?CharacterController $controller = null;
public float $moveSpeed = 8.0;
public function start(): void
{
$this->controller = $this->gameObject->getComponent(CharacterController::class);
}
public function update(): void
{
if (!$this->controller instanceof CharacterController) {
return;
}
$motion = new Vector3(0.0, 0.0, -1.0);
$motion = Vector3::scaleNew($motion, $this->moveSpeed * Time::deltaTime());
$this->controller->move($motion);
}
}
This gives you a practical kinematic movement workflow built around CharacterController.
RollerWorld is the shipped example of this pattern. Its player movement uses CharacterController for movement, and still uses Physics3D queries for obstacle probing and scene interaction.
When To Use Queries Directly
Use direct Physics3D queries when you want behavior beyond simple blocking and sliding, such as:
- anticipatory probes in front of a moving actor
- line-of-sight checks
- custom stepping, climbing, or grounding logic
- click picking and interaction
That lower-level query path is also how RollerWorld supplements its controller movement.
The key idea is:
- start with
CharacterControllerfor movement - use
Physics3Dqueries when you need custom behavior
Contact Reporting and Callbacks
You can also inspect contacts directly from 3D colliders:
$contacts = $this->collider?->getContacts(false) ?? [];
foreach ($contacts as $contact) {
Debug::info($contact->otherGameObject?->name ?? 'Unknown');
}
And you can respond to state changes with optional behaviour methods:
use Lenga\Engine\Core\Collision3D;
public function onCollisionEnter(Collision3D $collision): void
{
Debug::info('Touched ' . ($collision->otherGameObject?->name ?? 'Unknown'));
}
public function onTriggerEnter(Collision3D $collision): void
{
Debug::info('Entered trigger: ' . ($collision->otherGameObject?->name ?? 'Unknown'));
}
For familiarity, 3D uses the unsuffixed callback names, while 2D keeps the 2D suffix.
Raycasts
Raycasts are useful when you want to detect something in front of an object without asking for every overlapping collider in a volume.
Common uses:
- forward obstacle checks
- click or cursor picking
- line-of-sight checks
- ground probes from a character or vehicle
use Lenga\Engine\Core\Physics3D;
use Lenga\Engine\Core\Vector3;
$hit = Physics3D::raycast(
$this->transform->position,
$this->transform->forward,
5.0,
false
);
if ($hit !== null) {
Debug::info('Hit ' . ($hit->gameObject?->name ?? 'Unknown'));
}
If you want every hit along a line instead of only the first one, use raycastAll(...).
Recommended Next Steps
After you have a moving character or object:
- try the
RollerWorldsample and inspect its player and camera scripts - add lane walls or blockers with
BoxCollider3D - use separate overlap checks to support sliding
- keep your gameplay logic kinematic and explicit
- avoid baking backend assumptions directly into your game code
- keep the
Transformdirection helpers in mind for camera-relative movement, especiallyforward,right, andup