In short
Insert a ProximityPrompt into a Part (or an Attachment inside it), set ActionText and ObjectText, and in a Script in ServerScriptService connect prompt.Triggered, which passes the player who used it. Change HoldDuration for hold-to-use and MaxActivationDistance for how close players must be. E is the default key; Roblox shows the matching gamepad button and a tap target on phones.
- Handle
Triggeredon the server, so the result counts for everyone. - Treat a prompt like a RemoteEvent: Roblox's security guide says prompts can be abused, so check the player and add a cooldown.
- For many prompts, connect
ProximityPromptService.PromptTriggeredonce instead of one script per prompt.
Get the script
Press E to open chest script (ProximityPrompt)
One server Script gives every part tagged Chest a hold-E prompt that pays coins once per player, with distance and cooldown checks on the server.
-- Script in ServerScriptService named ChestPrompts.
-- Every part tagged "Chest" gets a hold-E prompt that pays coins once per player.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local CHEST_TAG = "Chest"
local CHEST_COINS = 25
local HOLD_SECONDS = 0.5
local MAX_DISTANCE = 10 -- studs, the prompt's MaxActivationDistance
local opened: { [Player]: { [Instance]: boolean } } = {}
local function giveCoins(player: Player, amount: number)
local leaderstats = player:FindFirstChild("leaderstats")
local coins = leaderstats and leaderstats:FindFirstChild("Coins")
if coins and coins:IsA("IntValue") then
coins.Value += amount
end
end
local function setUpChest(chest: Instance)
if not chest:IsA("BasePart") or chest:FindFirstChild("ChestPrompt") then
return
end
local prompt = Instance.new("ProximityPrompt")
prompt.Name = "ChestPrompt"
prompt.ActionText = "Open"
prompt.ObjectText = "Chest"
prompt.HoldDuration = HOLD_SECONDS
prompt.MaxActivationDistance = MAX_DISTANCE
prompt.Parent = chest
prompt.Triggered:Connect(function(player: Player)
-- Prompts can be abused like remotes, so check again on the server
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
local root = humanoid and humanoid.RootPart
if not (humanoid and root) or humanoid.Health <= 0 then
return
end
if (root.Position - chest.Position).Magnitude > MAX_DISTANCE + 4 then
return -- too far to have used the prompt honestly
end
local playerOpened = opened[player]
if not playerOpened or playerOpened[chest] then
return -- already opened by this player
end
playerOpened[chest] = true
giveCoins(player, CHEST_COINS)
end)
end
Players.PlayerAdded:Connect(function(player: Player)
opened[player] = {}
end)
for _, player in Players:GetPlayers() do
opened[player] = {}
end
Players.PlayerRemoving:Connect(function(player: Player)
opened[player] = nil
end)
for _, chest in CollectionService:GetTagged(CHEST_TAG) do
setUpChest(chest)
end
CollectionService:GetInstanceAddedSignal(CHEST_TAG):Connect(setUpChest)
RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts
Where to put it
ServerScriptService
ChestPrompts Script new
Workspace
Chest Part (tag: Chest)
Settings you can change
CHEST_COINSIn the script- Coins a player gets from each chest. Default:
25. HOLD_SECONDSIn the script- How long E must be held. 0 makes it a single press. Default:
0.5.
Coins go into a Coins IntValue in the player's leaderstats, which the leaderboard guide creates.
What a cheater can and cannot doThe server checks the player is alive and near the chest, and each player can open each chest once. Coins are only added on the server.
Type-checked with luau-lsp strict against Roblox's API definitions on 5 November 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.
Prompt for RoCode
Add treasure chests: every part tagged Chest gets a hold-E ProximityPrompt that says Open Chest. On the server, give the player 25 coins in leaderstats once per chest, check they are alive and within 12 studs, and hide the prompt for players who already opened it.
On this page
How a ProximityPrompt works
A ProximityPrompt sits inside the thing players interact with. When a player's character comes within MaxActivationDistance studs (10 by default) and the prompt is the closest one, Roblox shows it on their screen. When they press the key, or hold it for HoldDuration seconds, the prompt fires Triggered with that player, both on the server and on their client.
| Property | Default | What it does |
|---|---|---|
ActionText | Interact | The verb on the prompt: Open, Talk, Buy. |
ObjectText | empty | The thing: Chest, Shopkeeper, Door. |
HoldDuration | 0 | Seconds to hold before it triggers. 0 is a single press. |
MaxActivationDistance | 10 | How close, in studs, a player must be. |
KeyboardKeyCode | E | The key on a keyboard. GamepadKeyCode is the controller button (ButtonX). |
RequiresLineOfSight | true | Hide the prompt when something blocks the view of its part. |
Exclusivity | OnePerButton | How many prompts can show at once for the same key. |
Enabled | true | Turn the prompt off without deleting it. |
Where you put it matters. Inside a Part, the prompt floats at the part's centre. Inside an Attachment in that part, it floats at the attachment, which lets you place it on a handle or above an NPC's head.
Step 1: A prompt with no scripting
- Insert a Part in Workspace, name it
Chest, and tick Anchored. - Hover over the part, click + and insert a ProximityPrompt.
- In the Properties window set
ActionTexttoOpenandObjectTexttoChest. - Press Play and walk up to the part. The prompt appears within 10 studs. Pressing E does nothing yet: Step 2 adds what happens.
Players on a gamepad see the X button and players on a phone see a button they tap, with no extra work. Roblox picks the closest prompt when several are in range, and Exclusivity decides whether others for the same key can show at the same time.
Step 2: Do something when it is used
This script turns every part tagged Chest into a chest that pays coins once per player. Add the tag to the part (select it, scroll the Properties window to Tags, click +, type Chest). Delete the prompt you inserted in Step 1, because the script adds its own to each chest and two prompts would show. Then insert a Script in ServerScriptService named ChestPrompts.
-- Script in ServerScriptService named ChestPrompts.
-- Every part tagged "Chest" gets a hold-E prompt that pays coins once per player.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local CHEST_TAG = "Chest"
local CHEST_COINS = 25
local HOLD_SECONDS = 0.5
local MAX_DISTANCE = 10 -- studs, the prompt's MaxActivationDistance
local opened: { [Player]: { [Instance]: boolean } } = {}
local function giveCoins(player: Player, amount: number)
local leaderstats = player:FindFirstChild("leaderstats")
local coins = leaderstats and leaderstats:FindFirstChild("Coins")
if coins and coins:IsA("IntValue") then
coins.Value += amount
end
end
local function setUpChest(chest: Instance)
if not chest:IsA("BasePart") or chest:FindFirstChild("ChestPrompt") then
return
end
local prompt = Instance.new("ProximityPrompt")
prompt.Name = "ChestPrompt"
prompt.ActionText = "Open"
prompt.ObjectText = "Chest"
prompt.HoldDuration = HOLD_SECONDS
prompt.MaxActivationDistance = MAX_DISTANCE
prompt.Parent = chest
prompt.Triggered:Connect(function(player: Player)
-- Prompts can be abused like remotes, so check again on the server
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
local root = humanoid and humanoid.RootPart
if not (humanoid and root) or humanoid.Health <= 0 then
return
end
if (root.Position - chest.Position).Magnitude > MAX_DISTANCE + 4 then
return -- too far to have used the prompt honestly
end
local playerOpened = opened[player]
if not playerOpened or playerOpened[chest] then
return -- already opened by this player
end
playerOpened[chest] = true
giveCoins(player, CHEST_COINS)
end)
end
Players.PlayerAdded:Connect(function(player: Player)
opened[player] = {}
end)
for _, player in Players:GetPlayers() do
opened[player] = {}
end
Players.PlayerRemoving:Connect(function(player: Player)
opened[player] = nil
end)
for _, chest in CollectionService:GetTagged(CHEST_TAG) do
setUpChest(chest)
end
CollectionService:GetInstanceAddedSignal(CHEST_TAG):Connect(setUpChest)
The distance check uses 4 studs more than the prompt's range, for a player who moved while holding E. "Opened" is kept per player in a table on the server, so it resets when they leave; to remember it between sessions, save it with the DataStore tutorial's module. The CollectionService guide explains the tag pattern used here.
Step 3: One handler for every prompt
ProximityPromptService.PromptTriggered fires for every prompt in the game, with the prompt and the player. That lets one script route all prompts by an attribute, instead of connecting each one. Give each prompt a string attribute named Action (for example Heal or Sit) and handle it here:
-- Script in ServerScriptService named PromptRouter.
-- Handles every ProximityPrompt that has an "Action" attribute.
local ProximityPromptService = game:GetService("ProximityPromptService")
local handlers: { [string]: (player: Player, prompt: ProximityPrompt) -> () } = {
Heal = function(player: Player, _prompt: ProximityPrompt)
local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
if humanoid and humanoid.Health > 0 then
humanoid.Health = humanoid.MaxHealth
end
end,
Ring = function(_player: Player, prompt: ProximityPrompt)
local sound = prompt.Parent and prompt.Parent:FindFirstChildOfClass("Sound")
if sound then
sound:Play()
end
end,
}
ProximityPromptService.PromptTriggered:Connect(function(prompt: ProximityPrompt, player: Player)
local action = prompt:GetAttribute("Action")
local handler = if typeof(action) == "string" then handlers[action] else nil
if handler then
handler(player, prompt)
end
end)
Add the same distance and alive checks as in Step 2 to any handler that gives something of value. ProximityPromptService.MaxPromptsVisible limits how many prompts a player sees at once, which helps in a crowded shop.
Step 4: Prompts only some players see
A prompt's Enabled set by a LocalScript changes it for that player only. That is how you hide a chest's prompt for someone who already opened it, or show a "Sell" prompt only to players carrying something. The server stays in charge: it sets an attribute, and the LocalScript reads it.
-- LocalScript in StarterPlayerScripts named ChestPromptVisibility.
-- Hides a chest's prompt for this player once the server marks it opened.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local function refresh(chest: Instance)
local prompt = chest:FindFirstChild("ChestPrompt")
if prompt and prompt:IsA("ProximityPrompt") then
prompt.Enabled = player:GetAttribute("Opened_" .. chest.Name) ~= true
end
end
local function watch(chest: Instance)
chest.ChildAdded:Connect(function()
refresh(chest)
end)
player:GetAttributeChangedSignal("Opened_" .. chest.Name):Connect(function()
refresh(chest)
end)
refresh(chest)
end
for _, chest in CollectionService:GetTagged("Chest") do
watch(chest)
end
CollectionService:GetInstanceAddedSignal("Chest"):Connect(watch)
For this to work, add one line to ChestPrompts in Step 2, right after playerOpened[chest] = true: player:SetAttribute("Opened_" .. chest.Name, true). Give each chest a different name made of letters and numbers only, because attribute names cannot contain spaces. Attributes set on the server replicate to the player, so the LocalScript sees the change. Hiding a prompt is only a convenience: the server check in Step 2 is what really stops a second opening.
Step 5: Change how the prompt looks
Set Style to Custom and Roblox stops drawing the prompt; instead, ProximityPromptService.PromptShown fires on the client and you draw your own. This LocalScript shows a simple BillboardGui with the action text and hides it when the prompt hides.
-- LocalScript in StarterPlayerScripts named CustomPrompts.
-- Draws a simple label for prompts whose Style is Custom.
local ProximityPromptService = game:GetService("ProximityPromptService")
ProximityPromptService.PromptShown:Connect(function(prompt: ProximityPrompt, _inputType: Enum.ProximityPromptInputType)
if prompt.Style ~= Enum.ProximityPromptStyle.Custom then
return
end
local billboard = Instance.new("BillboardGui")
billboard.Size = UDim2.fromOffset(160, 44)
billboard.StudsOffset = Vector3.new(0, 2.5, 0)
billboard.AlwaysOnTop = true
billboard.Adornee = prompt.Parent :: any
local label = Instance.new("TextLabel")
label.Size = UDim2.fromScale(1, 1)
label.BackgroundColor3 = Color3.fromRGB(30, 24, 46)
label.TextColor3 = Color3.new(1, 1, 1)
label.Font = Enum.Font.GothamBold
label.TextScaled = true
label.Text = "[" .. prompt.KeyboardKeyCode.Name .. "] " .. prompt.ActionText
label.Parent = billboard
billboard.Parent = (game:GetService("Players").LocalPlayer :: Player):WaitForChild("PlayerGui")
prompt.PromptHidden:Once(function()
billboard:Destroy()
end)
end)
A custom prompt still triggers the normal way: the player presses the key, Triggered fires on the server, and your Step 2 or Step 3 handler runs. For touch players, also make the label a TextButton and call prompt:InputHoldBegin() and prompt:InputHoldEnd() from its events, since a label alone cannot be tapped. The overhead name tag guide covers BillboardGui sizing in more depth.
Common ProximityPrompt mistakes
| Mistake | What happens | Fix |
|---|---|---|
Handling Triggered in a LocalScript | The reward or change only exists on that player's screen. | Handle it in a server Script. |
No checks in Triggered | A cheater can fire prompts from far away or many times a second. | Check distance, health and a cooldown on the server. |
| Prompt inside a Model | Nothing shows: a prompt needs a Part or an Attachment as its parent. | Put it in a part of the model, or an Attachment. |
| Prompt hidden behind its own wall | With RequiresLineOfSight on, the part itself or a frame blocks the view. | Move it to an Attachment in front, or turn line of sight off. |
Using Touched for "press E" | Fires repeatedly and does not wait for a key. | Use a ProximityPrompt, or the debounce guide if it must be Touched. |
Common errors and fixes
Open the Output window (Window menu, then Output) before you press Play.
The prompt never appears
Check that its parent is a Part, MeshPart or Attachment, not a Model or Folder; that Enabled is ticked; that you are within MaxActivationDistance; and, if RequiresLineOfSight is on, that nothing blocks the view of its part. A prompt with Style set to Custom shows nothing until your own script draws it.
Pressing E does nothing
The prompt has nothing connected. Check that the Script is in ServerScriptService and that it found the prompt: a misspelt tag or name means Triggered was never connected. Add print(player.Name) as the first line of the handler.
It triggers but the player gets nothing
The handler returned early. Usually there is no leaderstats.Coins IntValue, or the distance check is too strict because the part is large: measure from the prompt's Attachment or raise the allowance.
Two prompts fight over the E key
Only the closest shows with the default Exclusivity. Give one of them another key with KeyboardKeyCode, or set Exclusivity to AlwaysShow.
Build it with RoCode
If you would rather not set this up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the scripts and instances in your Explorer, so there is no code to paste.
Give every part tagged Chest a hold-E prompt that says Open Chest. On the server, give 25 coins once per player per chest, check the player is alive and close, and hide the prompt for players who already opened that chest.
- Can look through your place first with Search Game, so it uses your existing leaderstats and chests instead of making new ones
- Writes the server Script and the LocalScript with Create Script, and can add the tag to your chest parts
- Is told to compile-check each script it writes with Check Script, which compiles it in Studio without running it
- Sets an undo point before and after each batch of changes, so Ctrl+Z steps back one batch at a time
With plugin 3.9.15 or newer RoCode can control a test character that walks and presses keys, but it checks only what it chose to test, so walk up to each prompt and hold E yourself. How RoCode connects to Studio.
Questions
How do I make a press E to interact in Roblox?
Insert a ProximityPrompt into a part and connect its Triggered event in a server Script. E is the default key, and Roblox shows the right button on controllers and phones.
How do I make a hold E prompt?
Set the prompt's HoldDuration to the number of seconds to hold, for example 1. Roblox draws a ring that fills while the key is held.
How do I change the key of a ProximityPrompt?
Set KeyboardKeyCode (for example to F) and GamepadKeyCode for controllers in the Properties window or from a script.
Should I handle Triggered on the server or the client?
On the server for anything that changes the game: coins, doors, items. The client version is only for effects on that player's screen.
Can I make a ProximityPrompt look different?
Yes. Set Style to Custom and draw your own UI from ProximityPromptService.PromptShown in a LocalScript, as in Step 5.
Sources and further reading
- Roblox Creator Docs: Proximity prompts. creating, customising and custom styles
- Roblox Creator Docs: ProximityPrompt. properties, Triggered and the hold methods
- Roblox Creator Docs: ProximityPromptService. PromptTriggered, PromptShown and MaxPromptsVisible
- Roblox Creator Docs: Securing the client-server boundary. prompts as an attack surface
How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 5 November 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.