Scripting Lua

ProximityPrompt: interação pronta para teclado, controle e toque, sem escrever UI

Porta, interruptor e botão não precisam de interface própria. O ProximityPrompt já mostra a tecla certa em cada tipo de entrada — e o ProximityPromptService deixa o jogo inteiro responder num lugar só.

por Gustavo Cantino9 min de leituraIniciante

Produzido com IA a partir das fontes citadas abaixo, e conferido por checagens automáticas contra essas mesmas fontes. Nenhuma pessoa leu este texto antes da publicação. Política Editorial

Placa tipográfica com a categoria Scripting Lua em destaque, o nível iniciante abaixo, e um campo de barras verticais verde-limão sobre fundo escuro.

Toda porta que abre, interruptor que acende e alavanca que puxa precisa da mesma coisa: avisar o jogador de que dá para interagir, dizer o que vai acontecer, e mostrar qual tecla apertar. E a tecla muda conforme a pessoa está no teclado, no controle ou no celular.

Esse é exatamente o trabalho que o ProximityPrompt já faz. Quem desenha a própria interface para isso está reescrevendo — em geral pior, e sem o suporte a todos os tipos de entrada.

O problema

O ProximityPrompt convida o jogador a interagir ao se aproximar de objetos como portas, interruptores e botões. Na lista do que ele resolve, dois itens são os que custam caro quando feitos à mão.

O primeiro: indicar com quais objetos dá para interagir. Num cenário cheio, saber o que é decoração e o que é interativo é metade da legibilidade do jogo, e essa informação precisa aparecer e sumir na hora certa.

O segundo, e é o decisivo: mostrar a entrada correta para cada tipo — teclado, controle e toque. Um botão desenhado à mão que diz "aperte E" está errado para quem está no controle e errado para quem está no celular. E a maior parte do público do Roblox está no celular.

Há ainda um problema menos visível, que aparece quando o jogo cresce: se cada objeto interativo tiver o próprio script, a lógica de interação fica espalhada por dezenas de lugares. É o que o ProximityPromptService resolve, e é o que o artefato deste guia usa.

Vale medir o tamanho do que se está evitando. Um botão de interação feito à mão precisa de um BillboardGui posicionado, de uma medida de distância que roda a cada quadro, de detecção de qual tipo de entrada o jogador está usando, de um ícone por tecla, de tratamento de toque, de uma regra para quando dois botões aparecem juntos, e de uma barra de progresso para o caso de segurar. São sete problemas, e cada um deles tem um caso de borda que só aparece num aparelho que você não tem.

O ProximityPrompt chega com os sete resolvidos e com uma vantagem que nenhuma implementação própria alcança: ele parece igual ao de outros jogos. O jogador que já viu um prompt em qualquer outro lugar da plataforma sabe o que fazer sem ler nada — e familiaridade é uma coisa que não se constrói sozinho.

Passo a passo

1. Ponha o prompt no lugar certo. Ele precisa ter como pai a parte, o modelo ou o attachment com que se vai interagir. Não é detalhe: a posição do prompt sai do pai, e um prompt solto na workspace não tem a que se referir.

No Explorer, passe o mouse sobre o objeto, clique no botão de adicionar e insira um ProximityPrompt.

2. Diga o que ele é. O prompt precisa comunicar três coisas: qual é o objeto, qual é a ação, e qual é a tecla. São três propriedades:

  • ObjectText — o nome opcional do objeto. "Porta da cozinha".
  • ActionText — o nome opcional da ação. "Abrir".
  • KeyboardKeyCode — a tecla que o jogador aperta ou segura.

Existe também o GamepadKeyCode, para a tecla do controle. Os dois textos são opcionais, mas prompt sem eles é um ícone de tecla flutuando sem contexto.

3. Controle quando ele aparece. Duas propriedades fazem o grosso.

O MaxActivationDistance define o raio em torno do prompt que ativa a visibilidade dele: quando o personagem entra nesse raio, o prompt aparece.

O RequiresLineOfSight só deixa o prompt visível quando há caminho livre da câmera até ele — e vem ligado por padrão. Isso é bom na maior parte do tempo e é a causa do bug descrito mais adiante.

4. Resolva a briga por tecla. Quando o personagem está no alcance de vários prompts, a propriedade Exclusivity decide o que aparece. No modo OnePerButton, só um prompt fica visível por tecla: prompts com teclas diferentes aparecem todos, mas com a mesma tecla só um fica visível por vez, conforme a direção da câmera.

É o comportamento que se quer numa sala com várias portas — a câmera escolhe.

5. Escolha como se aciona. O HoldDuration define quantos segundos a tecla precisa ficar apertada antes de a ação disparar; com valor zero, a ação dispara imediatamente. Segurar é o que se usa para ação destrutiva ou irreversível, porque evita o acionamento sem querer.

O ClickablePrompt define se dá para clicar no prompt. Desligado, só a tecla dispara. E existe uma exceção importante: no celular e no tablet o jogador sempre consegue clicar no prompt, independente do valor dessa propriedade.

6. Reaja no script. Dá para escutar os eventos no próprio objeto ou globalmente pelo ProximityPromptService — e a documentação recomenda o segundo: ele permite gerenciar todo o comportamento de um lugar só, sem código duplicado.

Os eventos principais: PromptTriggered, quando o jogador interage; PromptTriggerEnded, quando ele para; PromptButtonHoldBegan, quando começa a segurar num prompt com HoldDuration diferente de zero; e, do lado do cliente, PromptShown e PromptHidden, que disparam em LocalScript quando o prompt fica visível ou some.

Artefato

O script abaixo é um handler único para todos os prompts do jogo. Cada prompt declara o que ele é por um atributo, e o handler decide o que fazer — acrescentar um tipo de interação passa a ser acrescentar uma função, não espalhar um script novo dentro de mais um objeto.

E ele faz a parte que o prompt não faz: validar. O prompt é um caminho pelo qual o cliente aciona o servidor, exatamente como um RemoteEvent, e a distância que o cliente respeitou é a distância do cliente. O handler confere a distância no servidor, com uma folga deliberada — latência e movimento fazem os dois números diferirem um pouco, e sem folga o jogador honesto é recusado de vez em quando.

Erro comum

O erro mais comum é de posicionamento, e o sintoma é sempre o mesmo: o prompt não aparece, e não há erro nenhum.

A causa costuma ser o RequiresLineOfSight, que vem ligado por padrão. Ele exige caminho livre da câmera até o objeto. Um prompt colocado numa parte que está dentro de outra — a maçaneta dentro do batente, o botão embutido no painel, o prompt no centro de um modelo grande — não tem linha de visão a partir de lugar nenhum. O jogador chega perto e não vê nada.

O diagnóstico é rápido: desligue o RequiresLineOfSight. Se o prompt aparecer, o problema é geométrico, e a solução certa não é deixar desligado — é mover o prompt para um attachment na superfície visível do objeto. Desligar de vez faz o prompt atravessar paredes, que é o problema oposto.

O segundo erro é de escala do MaxActivationDistance. Valor generoso parece simpático e produz uma tela poluída, com prompts de meio cenário competindo por atenção. Vale lembrar que o raio interage com a Exclusivity: aumentar a distância aumenta quantos prompts ficam no alcance ao mesmo tempo, e é aí que a briga por tecla começa.

O terceiro é confiar no prompt como se ele fosse uma trava. Ele some da tela fora do alcance, e sumir da tela não impede nada — quem quiser acionar de longe aciona. Toda ação que mexe em estado do jogo precisa da mesma validação de servidor que um remote exigiria.

E há um quarto, específico do celular: montar a interação inteira em cima de "apertar e segurar" sem testar no toque. Com o clique sempre disponível no celular e no tablet, um HoldDuration alto vira uma interação que o jogador de telefone não entende como executar.

Próximo passo

Com os prompts funcionando, o ganho seguinte é de consistência. Escolha duas ou três teclas para o jogo inteiro e use sempre as mesmas para o mesmo tipo de ação — uma para interagir, outra para ação destrutiva com HoldDuration. O jogador aprende uma vez.

Depois, vale usar PromptShown e PromptHidden no cliente para algo além do prompt: destacar o objeto, mudar o cursor, tocar um som discreto. Eles disparam em LocalScript e são o gancho certo para o retorno visual que o prompt sozinho não dá.

E, por último, passe os olhos nos prompts existentes procurando os que disparam ação sem validação no servidor. A lista costuma ser mais curta do que parece, e cada item dela é uma porta que se abre de longe.

Fontes

Como corrigimos erros

Artefato pronto pra usar

Um só handler para todos os prompts do jogo, com a validação que o prompt não faz (ServerScriptService)
lua
-- ServerScriptService: UM handler para todos os ProximityPrompt do jogo.
--
-- O ProximityPromptService existe exatamente para isso: gerenciar o
-- comportamento de um lugar só, sem código duplicado. Em vez de um
-- script dentro de cada porta, o prompt declara o que ele é por um
-- atributo, e este arquivo decide o que fazer.
--
-- E ele faz a parte que o prompt NÃO faz: validar. O prompt é um
-- caminho pelo qual o cliente aciona o servidor, como um remote. A
-- distância que o cliente respeitou é a do cliente.

local ProximityPromptService = game:GetService("ProximityPromptService")
local Players = game:GetService("Players")

-- atributo "Acao" no prompt -> o que fazer
local ACOES = {}

function ACOES.abrir_porta(player, prompt)
	local porta = prompt.Parent
	if not porta or not porta:IsA("BasePart") then
		return
	end
	porta.CanCollide = false
	porta.Transparency = 0.7
	task.delay(3, function()
		if porta.Parent then
			porta.CanCollide = true
			porta.Transparency = 0
		end
	end)
end

function ACOES.acender_luz(player, prompt)
	local luz = prompt.Parent and prompt.Parent:FindFirstChildOfClass("PointLight")
	if luz then
		luz.Enabled = not luz.Enabled
	end
end

-- ── a validação que o prompt não faz ────────────────────────────────
-- O prompt some da TELA fora do alcance, mas sumir da tela não é uma
-- trava. Confira no servidor.
local function podeAcionar(player, prompt)
	local character = player.Character
	local humanoid = character and character:FindFirstChildOfClass("Humanoid")
	if not humanoid or humanoid.Health <= 0 then
		return false
	end

	local raiz = character:FindFirstChild("HumanoidRootPart")
	local alvo = prompt.Parent
	if not raiz or not alvo then
		return false
	end

	local posicao = alvo:IsA("BasePart") and alvo.Position
		or (alvo:IsA("Model") and alvo:GetPivot().Position)
		or (alvo:IsA("Attachment") and alvo.WorldPosition)
	if not posicao then
		return false
	end

	-- folga sobre o MaxActivationDistance: latência e movimento fazem a
	-- distância do servidor diferir um pouco da do cliente. Sem folga,
	-- jogador honesto é recusado de vez em quando.
	local limite = prompt.MaxActivationDistance + 5
	return (raiz.Position - posicao).Magnitude <= limite
end

-- ── anti-repetição ──────────────────────────────────────────────────
local ultimo = {}
local INTERVALO = 0.5

local function rapidoDemais(player)
	local agora = os.clock()
	if ultimo[player] and (agora - ultimo[player]) < INTERVALO then
		return true
	end
	ultimo[player] = agora
	return false
end

ProximityPromptService.PromptTriggered:Connect(function(prompt, player)
	local nome = prompt:GetAttribute("Acao")
	local acao = nome and ACOES[nome]
	if not acao then
		warn("prompt sem acao valida: " .. prompt:GetFullName())
		return
	end

	if rapidoDemais(player) or not podeAcionar(player, prompt) then
		return
	end

	local ok, erro = pcall(acao, player, prompt)
	if not ok then
		warn("acao " .. nome .. " falhou: " .. tostring(erro))
	end
end)

Players.PlayerRemoving:Connect(function(player)
	ultimo[player] = nil
end)
Neste artigo

Continue por aqui