2D/3D position, rotation, scale, directions, look_at.
Transform API
Every entity that appears in the world has a transform: where it sits, how it is turned, and how large it is. Two modules cover this — demi.transform3d for 3D and 2.5D games, demi.transform2d for flat sprites and tiles.
What a transform is
A transform is position + rotation + scale, in that order of usefulness. Position is a point in space, rotation is an orientation, scale is a size multiplier. Each value is either local (relative to the parent entity) or world (absolute in the scene). Local is what you edit; world is what you ask for when you need a screen position.
Rotations in both modules are radians. If you think in degrees, convert with math.rad(degrees).
Transform3D
local Transform3D = require("demi.transform3d")
local x, y, z = Transform3D.get_position("skyforge_player")
-- x, y, z: local position scalars (all nil when the entity has no transform)
Transform3D.set_position("skyforge_player", x + 1, y, z)
Transform3D.add_position("skyforge_player", 0, 0, -2) -- relative move
| Call | Takes | Returns |
|---|---|---|
Transform3D.get_position(id) | Entity handle or ID | Three scalars x, y, z in local space, or three nils when there is no transform |
Transform3D.set_position(id, x, y, z) | Handle plus three scalars | true when applied, false when the entity/component is missing |
Transform3D.add_position(id, dx, dy, dz) | Handle plus three scalars | true when applied — moves relative to the current local position |
Transform3D.get_rotation(id) | Handle | Three Euler scalars in radians, or three nils |
Transform3D.set_rotation(id, rx, ry, rz) | Handle plus Euler radians | true when applied |
Transform3D.get_scale(id) | Handle | Three scalars x, y, z, or three nils |
Transform3D.set_scale(id, sx, sy, sz) | Handle plus three scalars | true when applied |
Getters return one scalar per coordinate as separate Lua results. Assign exactly three names. If the entity has no transform, each result is nil — check if x == nil then return end before doing arithmetic on the values.
Directions and facing
Turning an entity and asking which way it points are different operations. The direction helpers always return world-space unit vectors (length 1) so you can scale them into movement.
local Transform3D = require("demi.transform3d")
Transform3D.look_at("skyforge_turret", 10, 0, 4) -- face a world point
local fx, fy, fz = Transform3D.forward("skyforge_turret")
local rx, ry, rz = Transform3D.right("skyforge_turret")
local ux, uy, uz = Transform3D.up("skyforge_turret")
Transform3D.look_at(id, x, y, z)— rotates the entity's+Zforward axis to face the given world point, compensating for parents. Returnstruewhen applied,false(without mutation) when the transform chain cannot be resolved.Transform3D.forward(id)/Transform3D.right(id)/Transform3D.up(id)→ three scalars of a world-space unit vector, or threenils when there is no transform.
A typical Skyforge turret turns with look_at, then fires along forward. Multiply the forward vector by a speed to get a velocity.
Transform2D
The 2D module is the same idea with one fewer coordinate. Positions are x, y, rotation is a single angle in radians, scale is x, y.
local Transform2D = require("demi.transform2d")
local x, y = Transform2D.get_position("skyforge_ship")
local r = Transform2D.get_rotation("skyforge_ship") -- radians
local sx, sy = Transform2D.get_scale("skyforge_ship")
Transform2D.set_position("skyforge_ship", x + 4, y)
Transform2D.set_rotation("skyforge_ship", math.rad(90))
Transform2D.set_scale("skyforge_ship", 2, 2)
Transform2D.get_position(id)→ two scalarsx, y, or twonils.Transform2D.get_rotation(id)→ one scalar in radians, ornil.Transform2D.get_scale(id)→ two scalars, or twonils.- Matching
set_position,set_rotation,set_scalewrite those values and returntruewhen applied,falsewhen the entity/component is missing.
Rotation is one number, not a table. 90 degrees is math.rad(90), which is about 1.5708.
Local versus world
Setters on both modules write local space. If a rune is parented under the Skyforge player, moving the rune 1 unit in x moves it 1 unit relative to the player, not 1 unit in the level. For a world-space read of an entity's location use Entity.world_position from Entity API; for placing things at world points, unparent first or convert yourself.
local Entity = require("demi.entity")
local Transform3D = require("demi.transform3d")
local rune = Entity.find("skyforge_rune")
local lx, ly, lz = Transform3D.get_position(rune) -- local
local wx, wy, wz = Entity.world_position(rune) -- world
Both calls return three scalars (or three nils). Use local values when animating something that should ride along with its parent, and world values when spawning effects at a visible location.
Next steps
- Entity API — parenting and handles.
- Math & debug — vector helpers for the direction numbers these calls return.
- Script lifecycle — moving entities every frame from
on_update.