Game - BenBerke/Tilky-Engine GitHub Wiki
Game
Game is the table for the level as a whole: creating and finding entities, finding the
sector at a point, casting rays, changing the sky, and moving to another level.
local player
function Start()
player = Game.FindEntity("Player")
end
| Function | Returns | |
|---|---|---|
CreateEntity(isUIEntity) |
Entity |
Adds a new entity to the level, or a copy of one (prefabs too). |
FindEntity(name) |
Entity or nil |
The first entity with exactly this name. |
FindEntities(name) |
list of Entity |
Every entity with exactly this name. |
FindEntitiesWithTag(tag) |
list of Entity |
Every entity with this tag. |
GetEntity(id) |
Entity or nil |
The entity with this ID. |
GetEntities() |
list of Entity |
Every entity in the level. |
GetSectorAt(position) |
Sector or nil |
The sector at a point on the map. |
Raycast(origin, direction, length, ignoredEntityID, requireCollider) |
table or nil |
What a line hits first. |
LineOfSight(from, to) |
boolean | Whether anything solid is between two points. |
LoadLevel(levelName) |
Switches to another level at the end of this frame. | |
SetSkyMode(mode), GetSkyMode() |
SkyMode |
How the sky image wraps around the camera. |
SetSkyTexture(path), GetSkyTexture() |
string | The image for Equirectangular and Cylinder skies. |
SetSkyCubemapFace(face, path), GetSkyCubemapFace(face) |
string | One image of a Cubemap sky. |
SetSkyRotation(degrees), GetSkyRotation() |
number | Turns the sky. |
SetSkyRotationSpeed(degreesPerSecond), GetSkyRotationSpeed() |
number | Keeps the sky turning. |
SetSkyTint(color), GetSkyTint() |
Vector3 |
Colors the sky image. |
SetSkyHorizonOffset(degrees), GetSkyHorizonOffset() |
number | Moves the horizon up or down. |
SetSkyFallbackColor(color), GetSkyFallbackColor() |
Vector3 |
Shown when there is no usable image. |
| Property | Type | ||
|---|---|---|---|
levelName |
string | read-only | The current level's name. |
CreateEntity
Game.CreateEntity() -- world entity
Game.CreateEntity(true) -- UI entity
Game.CreateEntity(enemy) -- a copy of an entity, or a new instance of a prefab
| Parameter | Type | |
|---|---|---|
isUIEntity |
boolean, optional | true makes a UI entity. Leave it out (or pass false) for a world entity. |
entity |
Entity | Instead of a boolean: the new entity is a copy of this one. See Copies and prefabs. |
Adds a new entity to the level right away and returns it. It is the same as placing a new entity in the editor:
- It is named
"Entity", has no tags and is enabled. - A world entity gets a Transform at the origin. A UI entity gets a UI Transform instead. It has no other components.
- It gets a new
id, soGame.FindEntity,Game.GetEntitiesand the other lookups find it from now on.
Give it more components with AddComponent, for
example "Sprite" to draw it or "Collider" to make it solid. Scripts can't be added, so an empty
created entity can't run a script of its own. A copy can (see below).
Copies and prefabs
Passing an entity makes a copy of it, every component and script included, and returns the copy.
Its scripts start on the next frame (their Start runs then).
The usual way is a prefab: drag a .prefab from the Asset Browser onto an Entity field, then
spawn it:
public Entity enemy -- a .prefab dropped here in the inspector
function Update()
if Input.GetKeyDown(Key.E) then
local e = Game.CreateEntity(enemy)
e.transform.position = entity.transform.position
end
end
An Entity field holding a prefab gives a copy of the prefab's entity that is not in the level:
you can read it (enemy.name, enemy.sprite...) and change it, which changes what this play
session spawns from it, but never the .prefab file. The new entity starts where the prefab's own
Transform is.
Passing an entity from the level copies it as it is right now, named with Copy added.
Entities created while the game runs are not saved into the level. Stopping the game in the editor removes them.
-- Drop a visible marker where the player is standing.
function Update()
if not Input.GetKeyDown(Key.M) then return end
local marker = Game.CreateEntity()
marker.name = "Waypoint"
marker.transform.position = entity.transform.position
marker:AddComponent(Component.Sprite).northTextureFileName = "Textures/flag.png"
end
Finding entities
Game.FindEntity(name) -- Entity or nil
Game.FindEntities(name) -- { Entity, ... }
Game.FindEntitiesWithTag(tag) -- { Entity, ... }
Game.GetEntity(id) -- Entity or nil
Game.GetEntities() -- { Entity, ... }
- Names and tags must match exactly, including upper and lower case.
- The lists are ordinary 1-based Lua tables. They are empty, never
nil, when nothing matches. - The result is a snapshot. Entities created or destroyed later don't appear in or disappear from
a list you already have, so check
e.isValidbefore using an entity you kept. - Every call loops over the whole level. Call them once in
Startand keep the result instead of calling them every frame. GetEntitytakes the number fromentity.idor from a raycast'sentityID.
-- Count the enemies left and open the exit when none remain.
public Sector exit = nil
function Update()
for _, e in ipairs(Game.FindEntitiesWithTag("enemy")) do
if e.isValid then return end
end
exit:MoveCeilingTo(1, exit.floorHeight + 40, 60)
end
(For a real level, find the enemies once in Start instead of every frame.)
GetSectorAt
Game.GetSectorAt(position) -- Sector or nil
| Parameter | Type | |
|---|---|---|
position |
Vector2 or Vector3 |
A Vector2 is a map position (.y is world z). For a Vector3, only x and z are used. |
Returns the sector that contains the point, or nil if the point is outside the map.
Height is ignored, and if sectors are nested the innermost one wins. It is the same rule that
decides which sector an entity is in, so Game.GetSectorAt(e.transform.position) gives the same
answer as e:GetSector().
-- Only drop a pickup where there is floor.
local spot = entity.transform.position + Vector3(mathT.RandomF(-32, 32), 0, mathT.RandomF(-32, 32))
local where = Game.GetSectorAt(spot)
if where ~= nil then pickup.transform.position = Vector3(spot.x, where.floorHeight, spot.z) end
Raycast
Game.Raycast(origin, direction, length, ignoredEntityID, requireCollider) -- table or nil
| Parameter | Type | |
|---|---|---|
origin |
Vector3 |
Where the ray starts. |
direction |
Vector3 |
Which way it goes. It doesn't need length 1. A zero vector never hits anything. |
length |
number | How far it reaches. |
ignoredEntityID |
integer | An entity the ray passes through, usually the caster's own entity.id. Pass 0 to ignore nothing. |
requireCollider |
boolean | See below. |
All five arguments are required.
The ray stops at the first thing it hits: a wall, an entity, a floor or a ceiling. If it hits
nothing within length, the result is nil. Otherwise it is a table:
| Field | Type | |
|---|---|---|
type |
string | "Entity", "Wall", "SectorFloor" or "SectorCeiling". |
typeID |
integer | The same as a number: 1 Entity, 2 Wall, 3 SectorFloor, 4 SectorCeiling. |
position |
Vector3 |
The point that was hit. |
distance |
number | From origin to position. |
entity |
Entity or nil |
Set when an entity was hit. |
wall |
Wall or nil |
Set when a wall was hit. |
sector |
Sector or nil |
Set when a floor or ceiling was hit. |
entityID, wallID, sectorID |
integer | The IDs of the above. When nothing of that kind was hit, they hold a large invalid number, so test hit.entity ~= nil rather than the ID. |
What the ray can hit
- Walls: every solid part of a wall, including the steps above and below an opening. The open part of a doorway lets the ray through.
- Floors and ceilings of every interval of every sector, slopes included: a sloped surface is hit where it actually is, as drawn.
- Entities, depending on
requireCollider:true: only entities with an active, non-trigger Collider. Sphere, capsule and box shapes are all used.false: every entity with a Transform, using a box the size of itsscale, standing on its position. Colliders are ignored. This also hits things like invisible markers, sotrueis usually what you want.
For a ray from a camera through the crosshair, use
camera:Raycast([length], [requireCollider]). It starts at the eye
(transform.position is at the feet), follows the camera's current yaw and pitch, ignores
the camera's own entity, and returns the same table. camera:ScreenToRay(x, y) gives the
origin, direction for any other point on screen.
-- Scripts/Shoot.lua (on the player): damage whatever is under the crosshair.
public number damage = 20
function Update()
if not Input.GetMouseButtonDown(Input.MouseLeft) then return end
local hit = entity.camera:Raycast(2000, true)
if hit == nil then return end
if hit.entity ~= nil then
local health = hit.entity:GetScript("Health")
if health.isValid then health:TakeDamage(damage) end
elseif hit.wall ~= nil then
Debug.Print("Hit a wall at", hit.position)
end
end
LineOfSight
Game.LineOfSight(from, to) -- boolean
| Parameter | Type | |
|---|---|---|
from |
Vector3 |
One end of the line. |
to |
Vector3 |
The other end. |
true when nothing solid is between from and to. Walls (their solid parts, the same as for
Raycast), floors and ceilings block the line. Entities never do, so there is no
entity to ignore. If from and to are the same point, the result is true.
Use it for "can this enemy see the player?" checks. It is much cheaper than calling Raycast
again and again to step past every entity standing in between.
-- Scripts/Guard.lua: notice the player when there is a clear line to their eyes.
public Entity player
public number eyeHeight = 12
function Update()
if player == nil then return end
local eyes = entity.transform.position + Vector3(0, eyeHeight, 0)
local target = player.transform.position + Vector3(0, eyeHeight, 0)
if Game.LineOfSight(eyes, target) then
Debug.Print("I see you")
end
end
LoadLevel
Game.LoadLevel(levelName)
| Parameter | Type | |
|---|---|---|
levelName |
string | A level's file name, with or without .bson: "Level2" or "Level2.bson". The file can be in any folder under Assets. |
Switches the game to another level. The level is loaded from its saved file, so in the editor, unsaved changes to that level aren't included. Save it before pressing Play if you want them.
When it happens. The call only queues the switch, the same way
entity:Destroy() is queued. The rest of the frame carries on in
the old level: code after the call still runs, and Game.levelName is still the old name. Once the
frame is over:
- Every script in the old level gets
OnDestroy. - The new level replaces the old one. Its entities, sectors, sounds and sky are all its own, exactly as saved.
- The new level starts just like the first one does: its camera and Player Controller are picked,
and every script gets
OnEnableandStart.
If LoadLevel is called several times in one frame, the last call wins. Calls made from
OnDestroy during a switch are ignored. Calls from the new level's Start are fine: they switch
again after the new level's first frame.
What carries over. Only the Global table. Everything else belongs to the level.
Each level has its own player entity, placed in the editor. Entities, sectors and walls you kept in
variables or in Global don't carry over; see Global.
Errors. A name that isn't a string, a level that doesn't exist, or a name that more than one level file has, raises an error at the call, and nothing is queued. If the file exists but can't be read, the error is logged and the game stays on the current level.
In the editor. Pressing Stop always puts you back on the level you pressed Play on, whatever level the game ended on.
Loading happens in one go, so the game may freeze for a moment while a big level loads.
-- Scripts/Flow/Exit.lua (trigger at the end of the level)
public string nextLevel = "Level2"
function OnTriggerEnter(other)
if other.hasPlayerController then Game.LoadLevel(nextLevel) end
end
-- Restart the current level when the player falls out of the map.
function Update()
if entity:GetSector() == nil then Game.LoadLevel(Game.levelName) end
end
levelName
Game.levelName -- "Level2"
The current level's name: its file name in the Levels folder, without .bson. It changes when
the new level starts, not when LoadLevel is called. Assigning to it raises an error.
Sky
function Start()
Game.SetSkyMode(SkyMode.Equirectangular)
Game.SetSkyTexture("Skies/sunset.png")
Game.SetSkyRotationSpeed(2)
end
Every level has one sky, drawn behind everything: wherever no wall, floor, ceiling or sprite covers the screen, you see the sky. To open a ceiling to it, make the ceiling see-through (alpha 0 in its color). The sky is set in the editor under World Settings > Rendering > Sky and saved with the level. Changes made from a script last until the level is loaded again, and are undone when you stop playing in the editor.
| Mode | Images | |
|---|---|---|
SkyMode.Cubemap |
six, set with SetSkyCubemapFace |
A cube around the camera. |
SkyMode.Equirectangular |
one 2:1 panorama, set with SetSkyTexture |
A sphere around the camera. The middle of the image is Front. |
SkyMode.Cylinder |
one strip, set with SetSkyTexture |
DOOM's sky. The image repeats every 90 degrees (a 256 pixel wide image), and its rows are tied to the screen: row 0 at the top, row 100 in the middle, 200 rows top to bottom. Looking up and down doesn't move it, just like in DOOM. |
Directions. Front is +Z (where a camera with yaw 0 looks), Right is -X, Left is +X, Up is +Y.
SkyFace has Right, Left, Up, Down, Front and Back. Every face is drawn as if you were
standing inside the cube looking at it, upright. Up has Front along its bottom edge, Down has
Front along its top edge.
-- A cubemap from Assets/Skies/Space.
local faces = {
[SkyFace.Right] = "right", [SkyFace.Left] = "left", [SkyFace.Up] = "up",
[SkyFace.Down] = "down", [SkyFace.Front] = "front", [SkyFace.Back] = "back",
}
function Start()
Game.SetSkyMode(SkyMode.Cubemap)
for face, name in pairs(faces) do
Game.SetSkyCubemapFace(face, "Skies/Space/" .. name .. ".png")
end
end
| Setting | Unit | |
|---|---|---|
| Rotation | degrees | Turns the sky the same way camera yaw turns. |
| Rotation speed | degrees per second | Added to the rotation every frame while the game runs. The editor view never spins. |
| Horizon offset | degrees | Positive raises the horizon. On Cubemap and Equirectangular skies it moves every point of the sky up by that angle; on a Cylinder sky it is the angle at the middle of the screen. |
| Tint | Vector3, 0..255 |
Multiplies the image. White (255, 255, 255) leaves it unchanged. |
| Fallback color | Vector3, 0..255 |
Fills the screen when the sky has no usable image: no texture, a Cubemap with a face missing, or an image that fails to load. It also shows through transparent pixels. |
-- Dusk: slowly turn the sky red.
local t = 0
function Update()
t = math.min(t + GameTime.deltaTime / 10, 1)
Game.SetSkyTint(Vector3(255, 255 - 135 * t, 255 - 135 * t))
end
SetSkyMode raises an error for a value that isn't in SkyMode, and the cube face functions do
the same for a value that isn't in SkyFace. Paths are relative to Assets; an empty string
clears the image.