Skip to content

Triggers and zones

Reacting to where people are, rather than to what they click.

Which one to use

EventFires whenGood for
on_regionsomebody enters or leaves a sphere around the objectgreeters, checkpoints, ambience, traps
on_contactsomebody's body touches the objectpressure plates, lava, hazards
on_touchsomebody clicks itdoors, buttons, pickups

on_region does not need the object to be solid, or even visible. on_contact does — it is real physical contact.

A greeter

luau
--!strict
local self = require("self")

self:on_region({
    radius = 6,
    near = function(p)
        p:show_hud({
            id = "welcome",
            anchor = "top",
            pages = { main = {
                { "text", "Welcome, " .. p.name, size = 18, align = "center" },
            } },
        })
    end,
    far = function(p)
        p:hide_hud("welcome")
    end,
})

Both are edges: each fires once per crossing, never repeatedly while somebody stands there.

Counting who is inside

luau
--!strict
local self = require("self")

local inside = 0

self:on_region({
    radius = 8,

    near = function()
        inside += 1
        if inside == 1 then
            self:sound("hum", { volume = 0.3, fade = 1.0 })
        end
    end,

    far = function()
        inside -= 1
        if inside <= 0 then
            inside = 0
            self:stop_sound()
        end
    end,
})

Clamp at zero. A player who disconnects inside still gets their far — every near gets its pair — and you would rather be safe than negative.

The sphere follows the object

A moving platform brings its trigger with it, at no extra cost:

luau
self:on_region({ radius = 3, near = function(p) ... end })
self:tween({ to = far_side, seconds = 10 })

A pressure plate

luau
--!strict
local self = require("self")
local events = require("events")

local on = 0

self:on_contact(function()
    on += 1
    if on == 1 then
        self:set({ color = "#55e38b", emissive = 1 })
        events:emit("plate", { down = true })
    end
end)

self:on_contact_end(function()
    on -= 1
    if on <= 0 then
        on = 0
        self:set({ color = "#3a3a3a", emissive = 0 })
        events:emit("plate", { down = false })
    end
end)

on_contact_end also fires when somebody leaves the world while standing on it, so the plate cannot get stuck down.

A hazard

luau
--!strict
local self = require("self")
local events = require("events")

local burning = {}

self:on_contact(function(p)
    if burning[p.id] then return end
    burning[p.id] = true

    local function tick()
        if not burning[p.id] then return end
        events:emit("combat:hit", { target = p.id, amount = 8 })
        self:after(0.5, tick)
    end

    tick()
end)

self:on_contact_end(function(p)
    burning[p.id] = nil
end)

The damage repeats on a chain of one-shot timers rather than a loop, so each slice stays short.

A checkpoint

luau
--!strict
local self = require("self")

self:on_region({
    radius = 4,
    near = function(p)
        local here = self:position()
        self.store:set("spawn." .. p.id,
            here.x .. "," .. here.y .. "," .. here.z)

        p:show_hud({
            id = "checkpoint",
            anchor = "top",
            pages = { main = { { "text", "Checkpoint reached", align = "center" } } },
        })

        self:after(2, function()
            p:hide_hud("checkpoint")
        end)
    end,
})

Key by player id, never by name.

Store the three numbers with a separator you can split on later. tostring on a point gives you something readable but not parseable back.

Asking who is nearby, instead of waiting

Sometimes you want the list, not an event:

luau
local players = require("players")
local events = require("events")

self:on_timer(2, function()
    local crowd = players:nearby(20)
    if #crowd >= 5 then
        events:emit("crowd", { n = #crowd })
    end
end)

nearby measures from the object this script is in, and reading it is free.

Ranges and behaviour

radius0.5 – 96 m
leavingtakes half a metre more than entering, so nobody on the line flickers
calling on_region againreplaces the volume and re-announces whoever is inside
radius = 0 or omittedremoves the volume

See also

Hungrit scripting documentation.