Recipes
Short answers to common tasks. Each is a whole script or the part that matters.
Move with the keyboard
local Mover = {}
Mover.Fields = { Speed = 5 }
function Mover:OnUpdate(deltaTime)
local move = Vec3(Input.GetValue("MoveX"), 0, Input.GetValue("MoveZ"))
self.Entity:Move(move * self.Speed * deltaTime)
end
return Mover
export = DefineScript({
Fields: { Speed: 5 },
OnUpdate(deltaTime: number) {
const move = Vec3(Input.GetValue("MoveX"), 0, Input.GetValue("MoveZ"));
this.Entity.Move(move.mul(this.Speed * deltaTime));
},
});
This passes through walls. For something that collides, use a CharacterController and call Move on it.
Do something later, or again and again
Timer.After(2, function() Log.Info("two seconds on") end)
local ticking = Timer.Every(0.5, function() self:Tick() end)
Timer.Cancel(ticking)
export = DefineScript({
OnStart() {
Timer.After(2, () => Log.Info("two seconds on"));
const ticking = Timer.Every(0.5, () => this.Tick());
Timer.Cancel(ticking);
},
Tick() {
},
});
Timers follow game time, so they stop while the game is paused.
A sequence of steps
A coroutine reads like a list of things to do. In TypeScript, give Timer.Start an arrow function, so this is still the script:
function Door:Open()
Timer.Start(function()
Tween.To(self.Entity, "Position", self.OpenPosition, 1)
Timer.Wait(3)
Tween.To(self.Entity, "Position", self.ClosedPosition, 1)
end)
end
export = DefineScript({
OpenPosition: Vec3(0, 3, 0),
ClosedPosition: Vec3(),
Open() {
Timer.Start(() => {
Tween.To(this.Entity, "Position", this.OpenPosition, 1);
Timer.Wait(3);
Tween.To(this.Entity, "Position", this.ClosedPosition, 1);
});
},
});
Pick something up
Put a collider with Is Trigger on the pickup:
local Coin = {}
function Coin:OnTriggerEnter(other)
if self.Taken or other.Tag ~= "Player" then
return
end
self.Taken = true
Scene.Find("Game"):GetScript("Game"):AddCoin()
self.Entity:Destroy()
end
return Coin
export = DefineScript({
Taken: false,
OnTriggerEnter(other: Entity) {
if (this.Taken || other.Tag !== "Player") {
return;
}
this.Taken = true;
Scene.Find("Game").GetScript("Game").AddCoin();
this.Entity.Destroy();
},
});
The Taken flag matters: the entity lives until the end of the update, so the trigger can fire again before it is gone.
Spawn things over time
function Spawner:OnStart()
self.Enemy = Assets.Find("Assets/Prefabs/Enemy.cairnprefab")
Timer.Every(2, function()
local angle = math.random() * math.pi * 2
Scene.Instantiate(self.Enemy, Vec3(math.cos(angle) * 15, 0, math.sin(angle) * 15))
end)
end
export = DefineScript({
Enemy: {} as AssetHandle,
OnStart() {
this.Enemy = Assets.Find("Assets/Prefabs/Enemy.cairnprefab");
Timer.Every(2, () => {
const angle = Math.random() * Math.PI * 2;
Scene.Instantiate(this.Enemy, Vec3(Math.cos(angle) * 15, 0, Math.sin(angle) * 15));
});
},
});
Follow something with the camera
local Follow = {}
Follow.Fields = { Height = 8, Back = 10 }
function Follow:OnStart()
self.Target = Scene.Find("Player")
end
function Follow:OnUpdate()
self.Entity.Position = self.Target.Position + Vec3(0, self.Height, self.Back)
end
return Follow
export = DefineScript({
Fields: { Height: 8, Back: 10 },
Target: {} as Entity,
OnStart() {
this.Target = Scene.Find("Player");
},
OnUpdate() {
this.Entity.Position = this.Target.Position.add(Vec3(0, this.Height, this.Back));
},
});
The third person template has a fuller camera that turns with the mouse and comes closer when a wall is in the way.
Face the way something moves
if move:Length() > 0.1 then
self.Entity:Face(move)
end
declare const move: Vec3;
export = DefineScript({
OnUpdate() {
if (move.Length() > 0.1) {
this.Entity.Face(move);
}
},
});
To face a thing and not a direction, use self.Entity:LookAt(target), or this.Entity.LookAt(target) in TypeScript.
Find what the mouse is over
function Picker:OnStart()
self.Camera = Scene.Find("Main Camera").Camera
end
function Picker:OnUpdate()
if Input.WasPressed("Fire") then
local origin, direction = self.Camera:ScreenToRay(Input.GetViewPosition())
local hit = Physics.Raycast(origin, direction, 100)
if hit then
Log.Info("Clicked " .. hit.Entity.Name)
end
end
end
export = DefineScript({
Camera: {} as Camera,
OnStart() {
this.Camera = Scene.Find("Main Camera").Camera!;
},
OnUpdate() {
if (Input.WasPressed("Fire")) {
const [x, y] = Input.GetViewPosition();
const [origin, direction] = this.Camera.ScreenToRay(x, y);
const hit = Physics.Raycast(origin, direction, 100);
if (hit) {
Log.Info(`Clicked ${hit.Entity.Name}`);
}
}
},
});
For a game seen from above, ScreenToGround gives the place on the ground under the mouse, to walk to or aim at:
local ground = self.Camera:ScreenToGround(Input.GetViewPosition())
if ground then
self.Entity:Face((ground - self.Entity.Position):Flat())
end
declare const camera: Camera;
declare const entity: Entity;
const [x, y] = Input.GetViewPosition();
const ground = camera.ScreenToGround(x, y);
if (ground) {
entity.Face(ground.sub(entity.Position).Flat());
}
Put a marker over something
WorldToScreen says where a point of the world is in the picture, from (0, 0) at the top left to (1, 1) at the bottom right:
local x, y, visible = self.Camera:WorldToScreen(enemy.Position + Vec3(0, 2, 0))
self.Marker.Visible = visible
self.Marker:SetPosition(x * 1280, y * 720)
declare const camera: Camera;
declare const enemy: Entity;
declare const marker: UIElement;
const [x, y, visible] = camera.WorldToScreen(enemy.Position.add(Vec3(0, 2, 0)));
marker.Visible = visible;
marker.SetPosition(x * 1280, y * 720);
See what a script is doing
Debug draws lines in the editor's viewport while the game plays. Each lasts one frame, so draw it every update for as long as it should show:
function Guard:OnUpdate()
local origin = self.Entity.WorldPosition + Vec3.Up
local hit = Physics.Raycast(origin, self.Entity.Forward, self.Sight, self.Entity)
Debug.Ray(origin, self.Entity.Forward * self.Sight, hit and Color.Red or Color.Green)
Debug.Sphere(self.Entity.WorldPosition, self.HearingRange, Color.Yellow)
end
export = DefineScript({
Fields: { Sight: 12, HearingRange: 6 },
OnUpdate() {
const origin = this.Entity.WorldPosition.add(Vec3.Up);
const hit = Physics.Raycast(origin, this.Entity.Forward, this.Sight, this.Entity);
Debug.Ray(origin, this.Entity.Forward.mul(this.Sight), hit ? Color.Red : Color.Green);
Debug.Sphere(this.Entity.WorldPosition, this.HearingRange, Color.Yellow);
},
});
There are Debug.Line, Debug.Ray, Debug.Sphere and Debug.Box. In an exported game they do nothing, so they can stay in the script.
Pause the game
function Game:SetPaused(paused)
Time.Scale = paused and 0 or 1
self.PauseMenu.Visible = paused
end
export = DefineScript({
PauseMenu: {} as UIElement,
SetPaused(paused: boolean) {
Time.Scale = paused ? 0 : 1;
this.PauseMenu.Visible = paused;
},
});
At a time scale of 0, timers, tweens and fixed updates stop. The UI keeps working, so a pause menu's buttons can still be pressed.
Keep a high score
function Game:OnStart()
local best = SaveData.Load("Best")
self.Best = best and best.Score or 0
end
function Game:End()
if self.Score > self.Best then
SaveData.Save("Best", { Score = self.Score })
end
end
export = DefineScript({
Score: 0,
Best: 0,
OnStart() {
const best = SaveData.Load("Best");
this.Best = best ? best.Score : 0;
},
End() {
if (this.Score > this.Best) {
SaveData.Save("Best", { Score: this.Score });
}
},
});
Restart the level
if Input.WasPressed("Restart") then
Scene.Load(Scene.GetCurrent())
end
if (Input.WasPressed("Restart")) {
Scene.Load(Scene.GetCurrent());
}