DEMIENGINEDOCUMENTATION Everything is free
Lua API reference · Create

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
CallTakesReturns
Transform3D.get_position(id)Entity handle or IDThree 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 scalarstrue when applied, false when the entity/component is missing
Transform3D.add_position(id, dx, dy, dz)Handle plus three scalarstrue when applied — moves relative to the current local position
Transform3D.get_rotation(id)HandleThree Euler scalars in radians, or three nils
Transform3D.set_rotation(id, rx, ry, rz)Handle plus Euler radianstrue when applied
Transform3D.get_scale(id)HandleThree scalars x, y, z, or three nils
Transform3D.set_scale(id, sx, sy, sz)Handle plus three scalarstrue 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 +Z forward axis to face the given world point, compensating for parents. Returns true when 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 three nils 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 scalars x, y, or two nils.
  • Transform2D.get_rotation(id) → one scalar in radians, or nil.
  • Transform2D.get_scale(id) → two scalars, or two nils.
  • Matching set_position, set_rotation, set_scale write those values and return true when applied, false when 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