venusian / terran
A Godot-shaped game engine for Venusian Framework applications: Scenes, a World, Viewports over any Surface output target, physics on the loop and rendering on the sketch.
Requires
- php: ^8.4|^8.5
- venusian-surface/bridge: ^0.10.0
- venusian-surface/contracts: ^0.10.0
- venusian-surface/drawing: ^0.10.0
- venusian-surface/fonts: ^0.10.0
- venusian-surface/framebuffers: ^0.10.0
- venusian-surface/human-input: ^0.10.0
- venusian-surface/images: ^0.10.0
- venusian-surface/nuts-and-bolts: ^0.10.0
- venusian-surface/windows: ^0.10.0
- venusian-voyager/console: ^0.10.0
- venusian-voyager/contracts: ^0.10.0
- venusian-voyager/io-pools: ^0.10.0
- venusian-voyager/nuts-and-bolts: ^0.10.0
- venusian-voyager/signals: ^0.10.0
- venusian-voyager/sketches: ^0.10.0
Requires (Dev)
- mockery/mockery: ^1.6
- pestphp/pest: ^4
- venusian-voyager/config: ^0.10.0
- venusian-voyager/vessel: ^0.10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-10 13:19:32 UTC
README
Godot-shaped game engine for Venusian Framework applications. Scenes in a tree, a World that owns it, one Viewport per output target. Physics ticks on the event loop at a fixed step; rendering ticks on the sketch at the refresh rate. Draws through Surface's rendering engines onto a staged window, a toolkit canvas or an embedded panel, and never names which.
Install
composer require venusian/terran php computer vendor:publish --tag=terran-config
Until the 0.10 venusian-surface/* splits publish, composer.json carries path repositories to the framework and surface checkouts beside this one.
Run
php rocket game
Set terran.main_scene (or TERRAN_MAIN_SCENE) to a Scene class. The default target is a staged window, 960×540, on the Velvet software engine.
use Terran\Scene; use Surface\Contracts\Drawing\RenderingEngine; use Surface\NutsAndBolts\Color; final class Bounce extends Scene { private float $x = 0.0; public function physicsProcess(float $delta): void { $this->x += 120 * $delta; } public function draw(RenderingEngine $ge): void { $ge->fillRect($this->x, 40, 40, 40, Color::hex('#FF8800')); } }
Hooks
| Hook | When |
|---|---|
ready() |
once, when the Scene's tree goes live |
physicsProcess(float $step) |
every fixed step on the GameLoop; $step is 1 / terran.physics_hz, constant |
process(float $delta) |
every rendered frame, with the real frame delta |
input(InputEvent $event): bool |
every input event this frame, until a Scene returns true |
draw(RenderingEngine $ge) |
when a Viewport renders this Scene |
exit() |
once, when removed or the World is destroyed |
A step runs at most terran.max_physics_steps (8) times per advance; a longer stall is dropped, not replayed. A hook that waits on a promise throws; start I/O with $this->world()->async(...) and take the result in ->then().
Interrupts
#[Interrupt('hit', [Scene::class, 'int'])] class Paddle extends Scene {} $paddle->connect('hit', $score, 'onHit'); // or a callable: $score->onHit(...) $paddle->emit('hit', $ball, 10); // connections run; app('signals') hears terran.<path>.hit
Declarations inherit. A freed Scene's connections go with it, and connections to it are dropped.
Autoloads
#[Autoload(ScoreKeeper::class)] class Level extends Scene { public function ready(): void { $this->autoload(ScoreKeeper::class)->reset(); } }
One instance per World, inherited by subclasses; terran.autoloads lists world-wide ones. A Scene class autoloads as a child of the root.
Input
// config/terran.php 'input' => ['actions' => ['jump' => ['key:space', 'pad:south'], 'left' => ['key:a', 'axis:left_x:-']]], public function physicsProcess(float $delta): void { if ($this->actions()->isPressed('jump')) { … } // each tap reaches one step $this->x += 200 * $delta * $this->actions()->axis('left', 'right'); }
Bindings: key:<Key>, mouse:<MouseButton>, pad:<GamepadButton>, axis:<GamepadAxis>:+|-[:dead zone] (default 0.15), values as HumanInput's enums spell them. Terran reads HumanInput once a frame and only the devices the game uses: bound ones, plus terran.input.events (keyboard, mouse) while a live Scene overrides input(). Mouse event x, y and motion dx, dy are the Viewport's framebuffer pixels. A second binding going down under a held action is no new press.
Seats
'input' => ['seats' => 4, 'keyboard_seat' => 1, 'seat_on_connect' => true, …], $p2 = $this->actions(2); // seat 2's pad only $this->x += 200 * $delta * $p2->axis('left', 'right');
Pads take the lowest free seat as they connect and are lit as that player. A pad that leaves keeps its seat reserved and gets it back on reconnect: by hardware id, or by name for a pad with none. With no free seat a new pad takes over a reservation, else stays unseated (its presses carry seat null). Keyboard and mouse belong to keyboard_seat. actions() with no seat reads every source. Pad and action events carry seat. For a lobby set seat_on_connect to false and seat pads yourself with app('terran.seats')->assign($seat, $event->pad) / release($seat). seats 0 turns seating off.
A Viewport whose target changes size re-mints its engine before drawing; a zero size skips the frame.
Scenes
Primitives, as Godot's node types: extend them.
$sheet = SpriteSheet::grid(Texture::load(base_path('assets/ship.png')), 4, 1); $ship = new Sprite2D('ship', $sheet); $ship->add((new Camera2D('camera'))->makeCurrent()); // the camera follows its parent $hud = (new Panel('hud'))->setMargin(16, 16, 236, 150); $stack = (new VBox('stack'))->setAnchor(Anchor::FULL_RECT)->setMargin(8, 8, -8, -8); $button = new Button('start', 'START'); $button->connect('pressed', fn () => $this->begin()); $hud->add($stack->add($button));
| 2D | UI | 3D |
|---|---|---|
Scene2D, Sprite2D, AnimatedSprite2D, TileMap, Camera2D, Label2D, Shape2D |
UIControl, Panel, Label, Button, ProgressBar, VBox, HBox |
Scene3D, Camera3D, MeshInstance, DirectionalLight |
The Viewport draws 2D Scenes by z through the current Camera2D, then UI in viewport pixels on top. 3D Scenes transform now and draw from slice 6. Mouse events reach UI controls under the pointer first; keys reach the focused control first; define ui_next, ui_prev, ui_accept actions for pad and keyboard menus. A subclass that overrides a hook calls the parent's.
php rocket make:scene Level # a plain Scene php rocket make:scene:sprite2d Ship # app/Scenes/Ship.php extends Sprite2D php rocket make:scene:button StartButton
A package adds its own primitive, and gets its make:scene:<slug>, from its provider's register():
$this->callAfterResolving('terran.primitives', fn (ScenePrimitives $p) => $p->register('tetromino', Tetromino::class));
Own your window
Extend GameEngine, open the window in openTargets(), return its canvas as a canvas target, and return your root from mainScene().
Tests
php -d memory_limit=128M vendor/bin/pest
Hardware-free: a fake output target, Velvet over native framebuffers, a real EventLoop over stream_select.