Skip to content

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 what entity.rotation reads 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 writeIt 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 (Unity's eulerAngles)

Three numbers are never a valid quaternion, so there's no ambiguity — the natural write does the natural thing. Euler angles apply in Unity's order (Z, then X, then Y), so values copied from a Unity project rotate the same way here.

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 with Quaternion.Angle(a, b), not by comparing Euler triples.
  • Often you don't want angles at all — you want a direction. entity.forward / right / up are 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, like Unity); entity.rotation / entity.eulerAngles are the entity's own transform (parent-local on a child).

Turning things — the recipes

Spin foreverrotate() 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 pointlookAt 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 movingLookRotation 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 facing

The Quaternion toolbox

You never write [x, y, z, w] by hand — build and blend with these (all on the global Quaternion, same names as Unity):

HelperGives 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) / Lerpblend 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.identitythe opposite turn / no turn
q.mulVec3(v)rotate a vector (q * v in Unity) — turn local offsets into world ones
q.eulerAnglesread any quaternion back as degrees

Physics owns dynamic bodies

On an entity with a dynamic rigidBody, the simulation drives the transform — a plain entity.rotation = … write is overwritten by the next physics step (same as Unity). Rotate a dynamic body through physics instead:

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.physics.warpBody(this.entity.id, [0, 3, 0], [0, 45, 0]) // teleport to a pose (3 values = Euler deg)

Kinematic and script-moved entities are yours: write entity.rotation freely and the collider follows.

Where each language shows up

SurfaceSpeaks
Inspector rotation fieldsEuler degrees
Terminal — get Box.rotationEuler degrees (labeled (euler deg))
Terminal — set Box.rotation = 0 45 0, rotate Box 0 45 0Euler degrees (absolute)
AI / terminal ops (spawn, modify)rotationDeg — Euler degrees
Animation timeline rotation tracksEuler degrees per key
Script reads (entity.rotation)quaternion (.eulerAngles for degrees)
Script writesboth — 3 values = Euler degrees, 4 = quaternion
Map file (components.transform.rotation)quaternion — edit via the tools above, not by hand

See also