Appearance
Rotations: Euler & quaternions
Galatrix speaks two rotation languages, and every rotation you'll ever touch is one of them:
- Euler angles — three numbers in degrees,
[x, y, z]: "tilt 30° on X, turn 45° on Y". This is the human language: the Inspector shows it, the terminal prints it, the animation timeline is authored in it, and[0, 90, 0]obviously means "quarter-turn about Y". - Quaternion — four numbers
[x, y, z, w]. This is the math the engine actually runs on: it composes cleanly, blends smoothly (slerp), and never gimbal-locks. It is whatentity.rotationreads back, what the map file stores, and what physics uses. The trade-off: the numbers mean nothing to a human — you never type one by hand, you build it with a helper.
You don't have to pick one. The API takes both and tells them apart by shape.
Writing a rotation: the 3-vs-4 rule
Every rotation write in the script API — entity.rotation, entity.eulerAngles, this.transform.rotation, Instantiate(...)'s rotation, physics.warpBody(...)'s rotation, camera.setRotation(...) — follows one rule:
| You write | It means |
|---|---|
3 values — [x, y, z], a Vector3, or {x, y, z} | Euler angles in degrees |
4 values — [x, y, z, w], a Quaternion, or {x, y, z, w} | a quaternion (re-normalized for you if its length drifted) |
js
this.entity.rotation = [0, 45, 0] // 3 values → Euler degrees: 45° about Y
this.entity.rotation = { x: 30, y: 0, z: 0 } // same idea, object form
this.entity.rotation = Quaternion.Euler(0, 45, 0) // 4 values → the same rotation, as a quaternion
this.entity.eulerAngles = [0, 90, 0] // explicit Euler property (eulerAngles)Three numbers are never a valid quaternion, so there's no ambiguity — the natural write does the natural thing. Euler angles apply in the standard order (Z, then X, then Y), so values copied from another engine rotate the same way here.
That order is the same everywhere: scripts, the Inspector's three Rotation fields, the rotationDeg field on the spawn / modify ops, and the rotation keys on the animation timeline all compose degrees identically, so the same three numbers always mean the same orientation — including in multiplayer, where the server samples your clips with the same order the editor plays them. (Single-axis turns are order-independent anyway; the order only shows up once you combine two or more axes.)
Setting a rotation from an op
The editor-side ops take either form, matching what scripts accept:
| Field | Meaning |
|---|---|
"rotationDeg": [x, y, z] | Euler degrees, in the order above. Prefer it for readable single-axis turns. |
"rotation": [x, y, z, w] | A quaternion — exact and order-free. Use it for a compound aim, or to copy an orientation from another object. |
json
[ { "op": "modify", "name": "Fan", "rotation": [0.3919, 0.2006, 0.3604, 0.8224] } ]Both are accepted by spawn and modify; if you send both, the quaternion wins.
Degrees, not radians
Everything Euler in Galatrix is degrees — script writes, the Inspector, the terminal, animation keys, rotationDeg in AI/terminal ops. If something turns ~57× too far, you passed radians.
Reading a rotation
Reads give you the quaternion — with an escape hatch back to degrees:
js
const q = this.entity.rotation // Quaternion [x, y, z, w] (indexable like an array)
const e = this.entity.eulerAngles // Vector3 of degrees, e.g. [0, 45, 0]Two things to know about reading Euler angles back:
- The degrees you read are equivalent, not always identical, to what was written — one rotation has many Euler spellings (
[180, 0, 180]is the same turn as[0, 180, 0]). Compare rotations withQuaternion.Angle(a, b), not by comparing Euler triples. - Often you don't want angles at all — you want a direction.
entity.forward/right/upare unit vectors already rotated into world space, ready for raycasts, aim checks, or movement:
js
const aim = this.entity.forward // where this entity points (-Z rotated)
const hit = this.physics.raycast(this.entity.position, aim, 50)this.transform.eulerAngles is the world rotation (it walks the parent chain); entity.rotation / entity.eulerAngles are the entity's own transform (parent-local on a child).
Turning things — the recipes
Spin forever — rotate() applies an Euler-degrees delta (world-relative), so scale it by deltaTime:
js
onUpdate() {
this.entity.rotate([0, 90 * this.time.deltaTime, 0]) // 90°/s about Y
}Snap to face a point — lookAt aims the entity's forward (-Z) at a world position:
js
this.entity.lookAt(this.game.findEntity('Player').position)(For "always face X" with no script at all, use the Look At component.)
Turn smoothly toward a target — build the goal rotation, then close the angle at a fixed degrees-per-second with RotateTowards (or ease with Slerp):
js
onUpdate() {
const to = this.game.findEntity('Player').position
const from = this.entity.position
const goal = Quaternion.LookRotation([to[0] - from[0], 0, to[2] - from[2]]) // yaw only: flatten Y
this.entity.rotation = Quaternion.RotateTowards(this.entity.rotation, goal, 180 * this.time.deltaTime)
}Face where you're moving — LookRotation on the velocity:
js
const v = this.physics.getVelocity(this.entity.id)
if (Math.hypot(v[0], v[2]) > 0.1) this.entity.rotation = Quaternion.LookRotation([v[0], 0, v[2]])Compose rotations — quaternion multiply applies right-to-left (a.mul(b) = "b first, then a"):
js
const lean = Quaternion.AngleAxis(15, [0, 0, 1]) // 15° roll
this.entity.rotation = this.entity.rotation.mul(lean) // lean on top of the current facingThe Quaternion toolbox
You never write [x, y, z, w] by hand — build and blend with these (all on the global Quaternion, with the usual names):
| Helper | Gives you |
|---|---|
Quaternion.Euler(x, y, z) | rotation from Euler degrees |
Quaternion.AngleAxis(deg, axis) | deg degrees around an axis |
Quaternion.LookRotation(forward, up?) | rotation whose forward is a direction |
Quaternion.FromToRotation(a, b) | the turn that carries direction a onto b |
Quaternion.Slerp(a, b, t) / Lerp | blend between two rotations (use for smoothing — never lerp raw Euler numbers across 180°) |
Quaternion.RotateTowards(a, b, maxDeg) | step toward b, at most maxDeg |
Quaternion.Angle(a, b) | degrees between two rotations |
Quaternion.Inverse(q) / Quaternion.identity | the opposite turn / no turn |
q.mulVec3(v) | rotate a vector (q * v) — turn local offsets into world ones |
q.eulerAngles | read any quaternion back as degrees |
Physics owns dynamic bodies
On an entity with a dynamic rigidBody, the simulation drives the transform between your writes — contacts and torque decide where it settles. A plain entity.rotation = … write does turn the body, keeping its angular momentum, but it is a teleport: fine for placing something, wrong for steering it every frame. To turn a dynamic body, go through physics:
js
this.physics.setAngularVelocity(this.entity.id, [0, 3, 0]) // spin it (rad/s)
this.physics.applyTorqueImpulse(this.entity.id, [0, 5, 0]) // kick it
this.entity.rotation = [0, 45, 0] // place it, momentum intact (3 values = Euler deg)
this.physics.warpBody(this.entity.id, [0, 3, 0], [0, 45, 0]) // place it AND stop it — a respawnKinematic and script-moved entities are yours: write entity.rotation freely and the collider follows.
Where each language shows up
| Surface | Speaks |
|---|---|
| Inspector rotation fields | Euler degrees |
Terminal — get Box.rotation | Euler degrees (labeled (euler deg)) |
Terminal — set Box.rotation = 0 45 0, rotate Box 0 45 0 | Euler degrees (absolute) |
AI / terminal ops (spawn, modify) | rotationDeg — Euler degrees |
| Animation timeline rotation tracks | Euler degrees per key |
Script reads (entity.rotation) | quaternion (.eulerAngles for degrees) |
| Script writes | both — 3 values = Euler degrees, 4 = quaternion |
Map file (components.transform.rotation) | quaternion — edit via the tools above, not by hand |
See also
entity,Quaternion, andphysicsin the Script API reference.- Transform constraints — face/follow a target with no script.
- Scripting basics — the Behaviour lifecycle these snippets run in.