Entities and components
Finding entities
local player = Scene.Find("Player") -- the first with this name
local coins = Scene.FindByTag("Coin") -- every one with this tag
local lights = Scene.FindWithComponent("PointLight") -- every one with this component
local child = self.Entity:FindChild("Head") -- a direct child
local parent = self.Entity.Parent
local children = self.Entity.Children
export = DefineScript({
OnStart() {
const player = Scene.Find("Player"); // the first with this name
const coins = Scene.FindByTag("Coin"); // every one with this tag
const lights = Scene.FindWithComponent("PointLight"); // every one with this component
const child = this.Entity.FindChild("Head"); // a direct child
const parent = this.Entity.Parent;
const children = this.Entity.Children;
},
});
Scene.Find raises an error when there is no such entity, so a typo in a name is caught where it is made. Scene.FindByName returns nil in that case, for something that may or may not be there.
Finds return entities in hierarchy order, the same on every run.
Look entities up once in OnStart and keep them in self (this). Finding by name every frame is wasted work.
Components
A component is a field of its entity, under its own name:
local light = self.Entity.PointLight
if light then
light.Intensity = 4
light.Color = Vec3(1, 0.5, 0.2)
end
local body = self.Entity:AddComponent("Rigidbody")
self.Entity:RemoveComponent("SphereCollider")
export = DefineScript({
OnStart() {
const light = this.Entity.PointLight;
if (light) {
light.Intensity = 4;
light.Color = Vec3(1, 0.5, 0.2);
}
const body = this.Entity.AddComponent("Rigidbody");
this.Entity.RemoveComponent("SphereCollider");
},
});
The field is nil (undefined in TypeScript) when the entity has no such component. A name that is not a component at all, such as a typo, is an error.
Component names are the ones in the API reference: Camera, MeshRenderer, Rigidbody, CharacterController, UIElement and so on. entity:GetComponent("PointLight") does the same with the name as text, for when the name is in a variable.
Directions
An entity faces down its own -Z axis.
local forward = self.Entity.Forward
local right = self.Entity.Right
local up = self.Entity.Up
local world = self.Entity.WorldPosition -- with every parent applied
self.Entity:LookAt(player) -- face an entity or a point
self.Entity:Face(velocity:Flat()) -- face along a direction
declare const player: Entity;
declare const velocity: Vec3;
export = DefineScript({
OnUpdate() {
const forward = this.Entity.Forward;
const right = this.Entity.Right;
const up = this.Entity.Up;
const world = this.Entity.WorldPosition; // with every parent applied
this.Entity.LookAt(player); // face an entity or a point
this.Entity.Face(velocity.Flat()); // face along a direction
},
});
Position, Rotation and Scale are relative to the parent. Rotation is Euler angles in degrees.
Vectors
local a = Vec3(1, 2, 3)
local b = Vec3(2) -- (2, 2, 2)
local c = a + b * 0.5
local length = c:Length()
local direction = c:Normalized()
local between = Vec3.Lerp(a, b, 0.25)
local apart = a:Distance(b)
local onTheGround = a:Flat() -- the same with y set to 0
const a = Vec3(1, 2, 3);
const b = Vec3(2); // (2, 2, 2)
const c = a.add(b.mul(0.5)); // a + b * 0.5
const length = c.Length();
const direction = c.Normalized();
const between = Vec3.Lerp(a, b, 0.25);
const apart = a.Distance(b);
const onTheGround = a.Flat(); // the same with y set to 0
The directions have names: Vec3.Up, Vec3.Down, Vec3.Forward, Vec3.Back, Vec3.Left, Vec3.Right, and Vec3.Zero and Vec3.One.
Colours are Vec3 too, with names and two ways to make one from numbers you already have:
light.Color = Color.Orange
light.Color = Color.Hex("#ff8000") -- as the web writes it
light.Color = Color.RGB(255, 128, 0) -- as a paint program shows it
light.Color = Color.Red * 4 -- brighter than white, for a glow
declare const light: PointLight;
light.Color = Color.Orange;
light.Color = Color.Hex("#ff8000"); // as the web writes it
light.Color = Color.RGB(255, 128, 0); // as a paint program shows it
light.Color = Color.Red.mul(4); // brighter than white, for a glow
Numbers have helpers in Lua's own math table: math.clamp(value, min, max), math.lerp(a, b, t), math.sign(value) and math.approach(current, target, step).
Making and destroying
local entity = Scene.CreateEntity("Marker")
entity.Position = Vec3(0, 1, 0)
local renderer = entity:AddComponent("MeshRenderer")
renderer.Mesh = Assets.Cube
renderer:SetMaterial(1, Assets.Find("Assets/Materials/Red.cairnmat"))
local copy = Scene.Clone(entity)
local enemy = Scene.Instantiate(Assets.Find("Assets/Prefabs/Enemy.cairnprefab"), Vec3(5, 0, 0))
entity:Destroy()
copy:Destroy(2) -- two seconds from now
const entity = Scene.CreateEntity("Marker");
entity.Position = Vec3(0, 1, 0);
const renderer = entity.AddComponent("MeshRenderer");
renderer.Mesh = Assets.Cube;
renderer.SetMaterial(1, Assets.Find("Assets/Materials/Red.cairnmat"));
const copy = Scene.Clone(entity);
const enemy = Scene.Instantiate(Assets.Find("Assets/Prefabs/Enemy.cairnprefab"), Vec3(5, 0, 0));
entity.Destroy();
copy.Destroy(2); // two seconds from now
Destroy takes effect at the end of the current update, after OnDestroy has run. Until then the entity is still valid.
Switching things off and on
door.Active = false -- gone: not drawn, no collision, no sound, no updates
door.Active = true -- back, as it was
if enemy.ActiveInScene then -- on, and so is everything above it
end
declare const door: Entity;
declare const enemy: Entity;
door.Active = false; // gone: not drawn, no collision, no sound, no updates
door.Active = true; // back, as it was
if (enemy.ActiveInScene) { // on, and so is everything above it
}
Switching an entity off switches off everything under it. Active reads what was set on the entity itself. ActiveInScene says whether it is really on, which also needs every parent to be on.
A script on a switched-off entity gets no OnUpdate or OnFixedUpdate. Timers it started keep running.
Talking to other scripts
-- Call a function on one script of another entity.
local game = Scene.Find("Game"):GetScript("Game")
game:AddScore(10)
-- Call a function on every script of an entity that has it.
enemy:SendMessage("TakeDamage", 5)
declare const enemy: Entity;
// Call a function on one script of another entity.
const game = Scene.Find("Game").GetScript("Game");
game.AddScore(10);
// Call a function on every script of an entity that has it.
enemy.SendMessage("TakeDamage", 5);
Events
When a script should not need to know who is listening, emit an event. The coin says a coin was collected, and the score, the sound and the HUD each hear it:
-- In the coin's script
Events.Emit("CoinCollected", self.Entity.WorldPosition, 5)
-- In any script that cares
function Hud:OnStart()
Events.On("CoinCollected", function(position, points)
self.Score = self.Score + points
end, self.Entity)
end
// In the coin's script
export const Coin = DefineScript({
OnTriggerEnter() {
Events.Emit("CoinCollected", this.Entity.WorldPosition, 5);
},
});
// In any script that cares
export const Hud = DefineScript({
Score: 0,
OnStart() {
Events.On("CoinCollected", (position: Vec3, points: number) => {
this.Score = this.Score + points;
}, this.Entity);
},
});
The last argument of Events.On is the entity the listener belongs to. Give self.Entity (this.Entity): the listener is then dropped when that entity is destroyed, and waits while it is switched off. Every listener is dropped when the scene stops.
GetScript takes the script's file name without .lua or .ts, and returns the table its functions get as self. In TypeScript its type is any: TypeScript does not know another script's values. You can read its values and call its functions directly.
Assets
local material = Assets.Find("Assets/Materials/Gold.cairnmat")
local mesh = Assets.Find("Assets/Models/Ship.gltf", "Mesh/0") -- a part of a model
const material = Assets.Find("Assets/Materials/Gold.cairnmat");
const mesh = Assets.Find("Assets/Models/Ship.gltf", "Mesh/0"); // a part of a model
Assets.Cube, Assets.Sphere and Assets.Plane are the built-in meshes. Assets.Find returns the empty handle when there is no such asset. Check with handle:IsValid().