Repository navigation
Interactions
Two useful engine features for interaction:
-
Game.Raycast(origin, direction, length, ignoredEntityID, requireCollider)shoots a ray and returnsnilon a miss, or a table describing what it hit:Key Meaning type"Entity","Wall","SectorFloor", or"SectorCeiling"distanceHow far away the hit was positionWorld position of the hit ( Vector3)entity,entityIDThe Entity that was hit ( nil/ an invalid ID if none)wall,wallIDThe Wallthat was hitsector,sectorIDThe SectorinvolvedWith
requireCollider = trueonly entities that have an active, non-triggerCollidercan be hit. Withfalseany entity can be hit, using a box the size of its transform'sscale. Passing the shooter's ownidasignoredEntityIDstops the ray from hitting them. -
camera:Raycast(length, requireCollider)is the same ray, already aimed for you: it starts at the camera's eyes, goes through the middle of the screen, and ignores the camera's own entity. It returns the same table. Both scripts below use it. -
Any script can be called by another script (
someScript:FunctionName()), which lets you build a tiny "interface" system: anything that definesInteractcan be used.
An Entity's transform.position is at its feet. The player's eyes (and camera) sit
playerController.eyeHeight above that. camera:Raycast already starts there
(camera:GetEyePosition()), so you only need the eye height when building a ray yourself with
Game.Raycast.
Attach to: the player (needs a Camera).
Casts a ray from the player's eyes every frame. If the entity it hits has a script that defines an
Interact function, pressing the key calls it. An optional UI label shows a prompt such as
[E] Pull lever.
-- Scripts/Interaction/Interactor.lua (entity script)
public number range = 48
public Key interactKey = Key.E
public Entity promptLabel = nil
local camera, label
function Start()
camera = entity.camera
if promptLabel ~= nil then label = promptLabel.uiText end
if camera == nil then
Debug.LogError("Interactor: " .. entity.name .. " needs a Camera")
end
end
local function SetPrompt(text)
if label ~= nil and label.text ~= text then label.text = text end
end
-- Looks for a script on `entity` that defines Interact(). If there is one, optionally uses it,
-- and returns the prompt text to show. Returns nil if nothing there can be interacted with.
local function Examine(target, use)
local scripts = target:GetScripts()
for i = 1, #scripts do
local script = scripts[i]
if type(script.Interact) == "function" then
if use then script:Interact(entity) end -- `entity` is the player (this script's Entity)
-- The target script can provide a `prompt` string of its own.
return "[" .. Input.GetKeyName(interactKey) .. "] " .. (script.prompt or "Interact")
end
end
return nil
end
function Update()
if camera == nil then return end
-- requireCollider = true: only things with a Collider can be interacted with.
local hit = camera:Raycast(range, true)
local prompt = nil
if hit ~= nil and hit.entity ~= nil then
prompt = Examine(hit.entity, Input.GetKeyDown(interactKey))
end
SetPrompt(prompt or "")
endNotes
-
script.Interactandscript.promptare read straight out of the target script. If the target doesn't define them the value isnil, which is what makes this check safe. - The player is passed to
Interactso the target knows who used it. -
GetScripts()returns a list that only lives for the moment it's used, so the script is used inside the loop and never stored in a variable that outlives it. - The target entity needs a
Collider(sincerequireCollideristrue).
Attach to: an Entity with a Collider. Works with the Interactor above.
Toggles a channel that doors, alarms, and lights can listen to (see 05_doors.md and 07_lighting.md).
-- Scripts/Interaction/Lever.lua (entity script)
public string channel = "door1"
public string prompt = "Pull lever"
local isOn = false
-- Called by the Interactor. `self` is this script, `who` is the Entity that used it.
function Interact(self, who)
isOn = not isOn
Global.channels = Global.channels or {}
Global.channels[channel] = isOn
Debug.Print(who.name .. " turned '" .. channel .. "' " .. (isOn and "on" or "off"))
endNotes
- Functions that other scripts call with a colon must take
selfas their first parameter:interactable:Interact(entity)runsInteract(interactable, entity). -
promptis a normal public field, so the Interactor can read it, and designers can edit it.
Attach to: the player (needs a Camera).
Left mouse fires an instant ray from the player's eyes. It supports fire rate, ammo, and reloading, and damages
anything hit that has a Health script
(10_health_and_damage.md).
-- Scripts/Interaction/Hitscan.lua (entity script)
public number damage = 20
public number range = 400
public number fireDelay = 0.25
public int magazineSize = 8
public number reloadTime = 1.5
public Entity ammoLabel = nil
local camera, label
local ammo = 0
local cooldown = 0.0
local reloading = 0.0
local shownText = ""
local function UpdateLabel()
if label == nil then return end
local text = reloading > 0 and "Reloading..." or (ammo .. " / " .. magazineSize)
if text ~= shownText then
shownText = text
label.text = text
end
end
local function Fire()
ammo = ammo - 1
cooldown = fireDelay
local hit = camera:Raycast(range, true)
if hit == nil then return end
if hit.entity ~= nil and hit.entity:HasScriptNamed("Health") then
hit.entity:GetScript("Health"):TakeDamage(damage)
Debug.Print("Hit " .. hit.entity.name)
end
end
function Start()
camera = entity.camera
ammo = magazineSize
if ammoLabel ~= nil then label = ammoLabel.uiText end
if camera == nil then
Debug.LogError("Hitscan: " .. entity.name .. " needs a Camera")
end
end
function Update()
if camera == nil then return end
local dt = GameTime.deltaTime
cooldown = math.max(0, cooldown - dt)
if reloading > 0 then
reloading = reloading - dt
if reloading <= 0 then ammo = magazineSize end
else
local wantsReload = Input.GetKeyDown(Key.R) and ammo < magazineSize
if wantsReload or (ammo == 0 and Input.GetMouseButtonDown(Input.MouseLeft)) then
reloading = reloadTime
elseif cooldown <= 0 and ammo > 0 and Input.GetMouseButton(Input.MouseLeft) then
Fire()
end
end
UpdateLabel()
endNotes
-
GetMouseButtonis true while held (automatic fire). UseGetMouseButtonDowninstead for semi-automatic. - The weapon skips the shooter itself by passing
entity.idas the ignored entity. - Enemies need a
Colliderto be hit, sincerequireCollideristrue.