Zones, touches, tags, prompts and clicks#
This page covers the ways a place notices something without you checking for it every frame: a part that reports what runs into it, a region that knows what stands in it, a label you attach to instances so you can find them all at once, a "press E" prompt, and a part a player can click. The last two sections cover watching a pair of instances and turning a distance into a strength.
touched and touchEnded#
Every part has a touched and a touchEnded signal. touched fires when another part starts
overlapping it, and touchEnded when that part stops. Both hand you the other part:
pad.touched:connect(function(other) end)
pad.touchEnded:connect(function(other) end)Touches are worked out once a tick, and only for parts that something listens on, so a part with no
connection costs nothing. A trigger is an ordinary part with collides = false and
transparency = 1: players walk through it and never see it, and it still reports what went past.
canTouch = false opts a part out.
A body walking along the edge of a part can start and end a touch several times a second, because
each tick is worked out on its own. Use a Zone when you need a steady answer to "is this player
in here".
Zones#
A Zone is an instance that knows what is inside a shape you give it. It asks whether a point is
inside that shape, which is cheaper and steadier than touched: a body standing on the edge does
not flicker in and out.
local safe = world:add("Zone", {
shape = "box", -- box, sphere, cylinder, polygon
size = vec3(20, 6, 20), -- sphere: diameter in x. cylinder: diameter in x, height in y
cframe = cframe(0, 65, 0),
trackPlayers = true, -- bodies players wear
trackTag = "", -- and anything with this tag
trackClass = "", -- and anything of this class
cooldown = 0.5, -- seconds before the same thing can enter or leave again
priority = 0,
})
safe.entered:connect(function(thing) end)
safe.left:connect(function(thing) end)
safe:players() -- the bodies inside
safe:occupants() -- everything inside
safe:contains(position)shapeandsizemake the region. A box is 20 by 6 by 20 metres here, centred on thecframe.- The three
trackproperties say what the zone watches. WithtrackPlayers = trueit follows the bodies players wear; filltrackTagortrackClassand it follows those as well. cooldownis how long the same thing has to wait before it can enter or leave again, which keeps a player hopping over the boundary from firing the signals repeatedly.enteredandleftfire with the thing that moved.players(),occupants()andcontains()answer the same question at the moment you ask it, which is what you want inside a loop or when a player presses a button.
Polygon zones use their Attachment children as the outline on the zone's floor, and are as
tall as size.y:
local yard = world:add("Zone", { shape = "polygon", size = vec3(0, 8, 0) })
yard:add("Attachment", { cframe = cframe(0, 0, 0) })
yard:add("Attachment", { cframe = cframe(12, 0, 0) })
yard:add("Attachment", { cframe = cframe(6, 0, 10) })The three attachments are the corners of a triangle, and size.y of 8 makes the zone eight metres
tall. The x and z of size are unused, because the outline decides the footprint.
Where zones overlap, game.zones:at(position) lists every zone there with the winner first:
highest priority, then the smallest.
A zone can change channels and sound without a script listening to it:
world:add("Zone", { size = vec3(10, 5, 10), textChannel = world:find("tavern"), soundId = "res://music/tavern.ogg", volume = 0.6 })- A player inside is put in
textChannel, and taken out when they leave. soundIdloops on that player's client while the zone is the winning one where they stand.
Tags#
A tag is a name you attach to an instance so you can find every instance wearing it later. You use one where a class or a parent will not do, such as marking twenty scattered parts as lava.
lava:addTag("lava")
lava:hasTag("lava") lava:removeTag("lava") lava:getTags()
for _, part in game.tags:tagged("lava") do end
game.tags:added("lava"):connect(function(instance) end)
game.tags:removed("lava"):connect(function(instance) end)
world:byTag("lava") -- only the tagged instances under this oneaddTag,removeTag,hasTagandgetTagswork on any instance.game.tags:tagged(name)gives every instance with the tag, wherever it is.game.tags:addedandgame.tags:removedfire when an instance gains or loses the tag, so a script can set a part up the moment it is marked instead of scanning for new ones.world:byTag(name)narrows the search to the instances under the one you called it on.
Tags replicate, so a tag the server adds reaches every client.
A client can only tag what it owns or made itself. Tagging anything else belongs on the server.
Proximity prompts#
A ProximityPrompt is an instance you put inside a part to get "Press E to open" with no interface
to write. The engine draws the prompt, handles the key and the hold, and tells you when it was
triggered.
local prompt = door:add("ProximityPrompt", {
actionText = "Open", objectText = "Door",
keys = "e", holdDuration = 0.5,
maxActivationDistance = 8, requiresLineOfSight = true,
offset = vec3(0, 1, 0),
backgroundColor = color(0.05, 0.05, 0.07), backgroundTransparency = 0.25,
textColor = color(1, 1, 1), keyColor = color(1, 1, 1),
})
-- server: fires after checking the player really is close
prompt.triggered:connect(function(body) door:destroy() end)
prompt.holdBegan:connect(function(body) end)
prompt.holdEnded:connect(function(body) end)
-- client
prompt.shown:connect(function() end)
prompt.hidden:connect(function() end)actionTextis what the player is offered to do andobjectTextnames what they are doing it to. The colour properties are what the box behind them looks like.holdDuration = 0.5means the player holds the key for half a second. The engine draws the progress bar while they hold, andtriggeredfires at the end of it.maxActivationDistance = 8is how far away the prompt still works, andrequiresLineOfSight = truehides it when something is in the way.offsetmoves the prompt off the centre of the part it is parented to, a metre up here.triggered,holdBeganandholdEndedall fire with the body that did it, on the server, after the engine has checked that the player really is close, and for a held prompt that they held it forholdDuration, give or take a quarter of a second for lag.shownandhiddenfire on the client, which is where you play a sound or change a cursor.
A client shows the one prompt nearest to its player, floating over what it is parented to, with a
progress bar while held. keys uses the same key names as InputAction, and like an action the
prompt does not hear its key while a screen, a focused text box or the editor has the keyboard.
The box names the first key in keys the way a player reads it on their keyboard: a letter or digit
shows as itself, since it is matched by what the key types on the player's layout, mousebutton2
shows as "Right click", mousebutton1 as "Click", leftshift as "Shift" and leftcontrol as
"Ctrl". A wide name makes the key box wider.
You can ask which prompt a body would use right now:
local prompt, distance = game.proximity:closestInteractable(body)Click detectors#
A ClickDetector inside a part hears the player click it:
-- server: a lever that opens and closes a door
local lever = world:find("Lever") :: Part
local door = world:find("Door") :: Part
local detector = lever:add("ClickDetector", { maxActivationDistance = 6 })
local open = false
detector.mouseClick:connect(function(player)
open = not open
door.collides = not open
door.transparency = if open then 0.8 else 0
lever.cframe = lever.cframe * cframe.angles(0, 0, if open then 0.8 else -0.8)
print(player.name, if open then "opened" else "closed", "the door")
end)- The detector goes inside the part you want clicked, so
lever:add("ClickDetector", ...)is what makes the lever clickable. mouseClickfires with the player who clicked, which is whatplayer.nameprints here.- Because this runs on the server, the door opens for everybody:
collides,transparencyandcframeare properties, and writing them replicates to every client.
| Property | |
|---|---|
maxActivationDistance |
how far the player's body may be, 32 by default |
cursorIcon |
the pointer while over it: empty for a hand, a cursor name like "crosshair", or "res://ui/lever.png" |
| Event | Fires with | |
|---|---|---|
mouseClick |
the player | a left click |
rightMouseClick |
the player | a right click |
mouseHoverEnter |
the player | the pointer comes onto it |
mouseHoverLeave |
the player | the pointer leaves it |
What decides whether a click counts:
- The player aims with the crosshair, or with the mouse while the pointer is free.
cursorIcononly shows while the pointer is free. - A detector inside a
Modelhears every part of the model. When detectors are nested, the one closest to the part that was aimed at wins. - Blocks in between stop it, and nothing inside a
ViewportFramecounts. - A click fires when the button is released over the same detector it was pressed on. The click
still reaches Minecraft; bind an action to
mousebutton1to keep it from the game. - Nothing is aimed at while a screen, the chat or the editor is open, so opening one fires
mouseHoverLeave. A click an interface took does not count. - Distance is counted from the player's body to the part, less half its size.
On the server each event fires after checking the player really is within maxActivationDistance,
with two blocks to spare for lag. In a LocalScript they fire at once with the local player, where
name, id, character and controls work, and spawn and kick are errors.
Hover events are the ones you usually want on the client, because a hint only that player should see belongs on their own screen:
-- client: a hint while the pointer is on the lever
local detector = (world:find("Lever") :: Part):find("ClickDetector") :: ClickDetector
detector.mouseHoverEnter:connect(function() hint.visible = true end)
detector.mouseHoverLeave:connect(function() hint.visible = false end)Watching two things#
A watcher tells you when two instances come within a distance of each other, without you measuring it yourself every tick:
local watcher = game.proximity:watch(player, chest, 6)
watcher.entered:connect(function(a, b) end)
watcher.left:connect(function(a, b) end)
watcher.near -- true while they are within range
watcher:stop()entered fires when they come within the six metres and left when they go back out, both with the
two instances. Read near when you want the answer now rather than a signal. Call stop() when you
are done with the watcher.
Falloff#
falloff turns a distance into a number between 1 and 0, which is what you multiply a volume or a
strength by so it fades with range:
local volume = game.proximity.falloff(distance, 4, 30, 1.5) -- 1 inside 4 m, 0 past 30 mThe first three arguments are the distance you measured, the range that still counts as full strength, and the range past which there is nothing left. The fourth shapes the curve between them.
