Proximity Prompt Studio Beta

This feature is awesome, and I love all the customization you can do with this!

Here are some suggestions to improve this more:
I think there should be a fade in/out animation when getting close to the Prompt (should be just like the animation when you activate/deactivate the prompt)
I fell like there should also be the same animation when Enabling/Disabling the prompt (only if you are near it of course)

EDIT: Managed to make the prompt fade out when you go far enough with the default code provided in the post, cant make it fade in though, since the GUI is built with the looks when activated
EDIT 2: I did it, here is the code

Show code
local UserInputService = game:GetService("UserInputService")
local ProximityPromptService = game:GetService("ProximityPromptService")
local TweenService = game:GetService("TweenService")
local TextService = game:GetService("TextService")
local screenGui = script.Parent

local GamepadButtonImage = {
	[Enum.KeyCode.ButtonX] = "rbxasset://textures/ui/Controls/xboxX.png",
	[Enum.KeyCode.ButtonY] = "rbxasset://textures/ui/Controls/xboxY.png",
	[Enum.KeyCode.ButtonA] = "rbxasset://textures/ui/Controls/xboxA.png",
	[Enum.KeyCode.ButtonB] = "rbxasset://textures/ui/Controls/xboxB.png",
	[Enum.KeyCode.DPadLeft] = "rbxasset://textures/ui/Controls/dpadLeft.png",
	[Enum.KeyCode.DPadRight] = "rbxasset://textures/ui/Controls/dpadRight.png",
	[Enum.KeyCode.DPadUp] = "rbxasset://textures/ui/Controls/dpadUp.png",
	[Enum.KeyCode.DPadDown] = "rbxasset://textures/ui/Controls/dpadDown.png",
	[Enum.KeyCode.ButtonSelect] = "rbxasset://textures/ui/Controls/xboxmenu.png",
	[Enum.KeyCode.ButtonL1] = "rbxasset://textures/ui/Controls/xboxLS.png",
	[Enum.KeyCode.ButtonR1] = "rbxasset://textures/ui/Controls/xboxRS.png",
}

local function createMainFrame()
	local frame = Instance.new("Frame")
	frame.Name = "ProximityPrompts"
	frame.BackgroundTransparency = 1
	frame.Size = UDim2.fromScale(1, 1)
	frame.Position = UDim2.fromScale(0, 0)
	frame.Parent = screenGui
	return frame
end

local function createProgressBarGradient(parent, leftSide)
	local frame = Instance.new("Frame")
	frame.Size = UDim2.fromScale(0.5, 1)
	frame.Position = UDim2.fromScale(leftSide and 0 or 0.5, 0)
	frame.BackgroundTransparency = 1
	frame.ClipsDescendants = true
	frame.Parent = parent

	local image = Instance.new("ImageLabel")
	image.BackgroundTransparency = 1
	image.Size = UDim2.fromScale(2, 1)
	image.Position = UDim2.fromScale(leftSide and 0 or -1, 0)
	image.Image = "rbxasset://textures/ui/Controls/RadialFill.png"
	image.Parent = frame

	local gradient = Instance.new("UIGradient")
	gradient.Transparency = NumberSequence.new {
		NumberSequenceKeypoint.new(0, 0),
		NumberSequenceKeypoint.new(.4999, 0),
		NumberSequenceKeypoint.new(.5, 1),
		NumberSequenceKeypoint.new(1, 1)
	}
	gradient.Rotation = leftSide and 180 or 0
	gradient.Parent = image

	return gradient
end

local function createCircularProgressBar()
	local bar = Instance.new("Frame")
	bar.Name = "CircularProgressBar"
	bar.Size = UDim2.fromOffset(58, 58)
	bar.AnchorPoint = Vector2.new(0.5, 0.5)
	bar.Position = UDim2.fromScale(0.5, 0.5)
	bar.BackgroundTransparency = 1

	local gradient1 = createProgressBarGradient(bar, true)
	local gradient2 = createProgressBarGradient(bar, false)

	local progress = Instance.new("NumberValue")
	progress.Name = "Progress"
	progress.Parent = bar
	progress.Changed:Connect(function(value)
		local angle = math.clamp(value * 360, 0, 360)
		gradient1.Rotation = math.clamp(angle, 180, 360)
		gradient2.Rotation = math.clamp(angle, 0, 180)
	end)

	return bar
end

local function createPrompt(prompt, inputType)

	local tweensForButtonHoldBegin = {}
	local tweensForButtonHoldEnd = {}
	local tweensForTriggered = {}
	local tweensForTriggerEnded = {}
	local tweenInfoInFullDuration = TweenInfo.new(prompt.HoldDuration, Enum.EasingStyle.Linear, Enum.EasingDirection.Out)
	local tweenInfoOutHalfSecond = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
	local tweenInfoFast = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
	local tweenInfoQuick = TweenInfo.new(0.06, Enum.EasingStyle.Linear, Enum.EasingDirection.Out)

	local actionTextSize = TextService:GetTextSize(prompt.ActionText, 19, Enum.Font.GothamSemibold, Vector2.new(1000, 1000))
	local objectTextSize = TextService:GetTextSize(prompt.ObjectText, 14, Enum.Font.GothamSemibold, Vector2.new(1000, 1000))
	local maxTextWidth = math.max(actionTextSize.X, objectTextSize.X)
	local promptHeight = 72
	local promptWidth = 72
	local textPaddingLeft = 72
	if prompt.ActionText ~= nil and prompt.ActionText ~= '' then
		promptWidth = maxTextWidth + textPaddingLeft + 24
	end

	local promptUI = Instance.new("BillboardGui")
	promptUI.Name = "Prompt"
	promptUI.Adornee = prompt.Parent
	promptUI.AlwaysOnTop = true
	promptUI.Size = UDim2.fromOffset(promptWidth, promptHeight)
	promptUI.SizeOffset = Vector2.new(prompt.UIOffset.X / promptUI.Size.Width.Offset, prompt.UIOffset.Y / promptUI.Size.Height.Offset)

	local frame = Instance.new("Frame")
	frame.Size = UDim2.fromScale(1, 1)
	frame.BackgroundTransparency = 0.3
	frame.BackgroundColor3 = Color3.fromRGB(0,0,0)
	frame.Parent = promptUI
	frame.Size = UDim2.fromScale(0.5, 1)
	frame.BackgroundTransparency = 1

	local roundedCorner = Instance.new("UICorner")
	roundedCorner.Parent = frame

	local inputFrame = Instance.new("Frame")
	inputFrame.Name = "InputFrame"
	inputFrame.Size = UDim2.fromScale(1, 1)
	inputFrame.BackgroundTransparency = 1
	inputFrame.SizeConstraint = Enum.SizeConstraint.RelativeYY
	inputFrame.Parent = frame

	local resizeableInputFrame = Instance.new("Frame")
	resizeableInputFrame.Size = UDim2.fromScale(1, 1)
	resizeableInputFrame.Position = UDim2.fromScale(0.5, 0.5)
	resizeableInputFrame.AnchorPoint = Vector2.new(0.5, 0.5)
	resizeableInputFrame.BackgroundTransparency = 1
	resizeableInputFrame.Parent = inputFrame

	local inputFrameScaler = Instance.new("UIScale")
	inputFrameScaler.Parent = resizeableInputFrame

	local inputFrameScaleFactor = inputType == Enum.ProximityPromptInputType.Touch and 1.6 or 1.33
	table.insert(tweensForButtonHoldBegin, TweenService:Create(inputFrameScaler, tweenInfoFast, { Scale = inputFrameScaleFactor }))
	table.insert(tweensForButtonHoldEnd, TweenService:Create(inputFrameScaler, tweenInfoFast, { Scale = 1 }))

	if prompt.ActionText ~= nil and prompt.ActionText ~= '' then
		local actionText = Instance.new("TextLabel")
		actionText.Name = "ActionText"
		actionText.Position = UDim2.new(0.5, textPaddingLeft - promptWidth/2, 0, 0)
		actionText.Size = UDim2.fromScale(1, 1)
		actionText.Font = Enum.Font.GothamSemibold
		actionText.TextSize = 19
		actionText.BackgroundTransparency = 1
		actionText.TextColor3 = Color3.new(1, 1, 1)
		actionText.TextXAlignment = Enum.TextXAlignment.Left
		actionText.Text = prompt.ActionText
		actionText.Parent = frame
		actionText.TextTransparency = 1
		table.insert(tweensForButtonHoldBegin, TweenService:Create(actionText, tweenInfoFast, { TextTransparency = 1 }))
		table.insert(tweensForButtonHoldEnd, TweenService:Create(actionText, tweenInfoFast, { TextTransparency = 0 }))
		table.insert(tweensForTriggered, TweenService:Create(actionText, tweenInfoFast, { TextTransparency = 1 }))
		table.insert(tweensForTriggerEnded, TweenService:Create(actionText, tweenInfoFast, { TextTransparency = 0 }))

		if prompt.ObjectText ~= nil and prompt.ObjectText ~= '' then
			actionText.Position = UDim2.new(0.5, textPaddingLeft - promptWidth/2, 0, 9)

			local objectText = Instance.new("TextLabel")
			objectText.Name = "ObjectText"
			objectText.Position = UDim2.new(0.5, textPaddingLeft - promptWidth/2, 0, -10)
			objectText.Size = UDim2.fromScale(1, 1)
			objectText.Font = Enum.Font.GothamSemibold
			objectText.TextSize = 14
			objectText.BackgroundTransparency = 1
			objectText.TextColor3 = Color3.new(0.7, 0.7, 0.7)
			objectText.TextXAlignment = Enum.TextXAlignment.Left
			objectText.Text = prompt.ObjectText
			objectText.Parent = frame
			objectText.TextTransparency = 1
			table.insert(tweensForButtonHoldBegin, TweenService:Create(objectText, tweenInfoFast, { TextTransparency = 1 }))
			table.insert(tweensForButtonHoldEnd, TweenService:Create(objectText, tweenInfoFast, { TextTransparency = 0 }))
			table.insert(tweensForTriggered, TweenService:Create(objectText, tweenInfoFast, { TextTransparency = 1 }))
			table.insert(tweensForTriggerEnded, TweenService:Create(objectText, tweenInfoFast, { TextTransparency = 0 }))
		end

		table.insert(tweensForButtonHoldBegin, TweenService:Create(frame, tweenInfoFast, { Size = UDim2.fromScale(0.5, 1), BackgroundTransparency = 1 }))
		table.insert(tweensForButtonHoldEnd, TweenService:Create(frame, tweenInfoFast, { Size = UDim2.fromScale(1, 1), BackgroundTransparency = 0.2 }))
		table.insert(tweensForTriggered, TweenService:Create(frame, tweenInfoFast, { Size = UDim2.fromScale(0.5, 1), BackgroundTransparency = 1 }))
		table.insert(tweensForTriggerEnded, TweenService:Create(frame, tweenInfoFast, { Size = UDim2.fromScale(1, 1), BackgroundTransparency = 0.2 }))
	else
		table.insert(tweensForButtonHoldBegin, TweenService:Create(frame, tweenInfoFast, { BackgroundTransparency = 1 }))
		table.insert(tweensForButtonHoldEnd, TweenService:Create(frame, tweenInfoFast, { BackgroundTransparency = 0.2 }))
		table.insert(tweensForTriggered, TweenService:Create(frame, tweenInfoFast, { BackgroundTransparency = 1 }))
		table.insert(tweensForTriggerEnded, TweenService:Create(frame, tweenInfoFast, { BackgroundTransparency = 0.2 }))
	end

	local roundFrame = Instance.new("Frame")
	roundFrame.Name = "RoundFrame"
	roundFrame.Size = UDim2.fromOffset(48, 48)

	roundFrame.AnchorPoint = Vector2.new(0.5, 0.5)
	roundFrame.Position = UDim2.fromScale(0.5, 0.5)
	roundFrame.BackgroundTransparency = 0.5
	roundFrame.Parent = resizeableInputFrame
	roundFrame.BackgroundTransparency = 1

	local roundedFrameCorner = Instance.new("UICorner")
	roundedFrameCorner.CornerRadius = UDim.new(0.5, 0)
	roundedFrameCorner.Parent = roundFrame

	table.insert(tweensForTriggered, TweenService:Create(roundFrame, tweenInfoQuick, { BackgroundTransparency = 1 }))
	table.insert(tweensForTriggerEnded, TweenService:Create(roundFrame, tweenInfoQuick, { BackgroundTransparency = 0.5 }))

	if inputType == Enum.ProximityPromptInputType.Gamepad then
		if GamepadButtonImage[prompt.GamepadKeyCode] then
			local icon = Instance.new("ImageLabel")
			icon.Name = "ButtonImage"
			icon.AnchorPoint = Vector2.new(0.5, 0.5)
			icon.Size = UDim2.fromOffset(24, 24)
			icon.Position = UDim2.fromScale(0.5, 0.5)
			icon.BackgroundTransparency = 1
			icon.Image = GamepadButtonImage[prompt.GamepadKeyCode]
			icon.Parent = resizeableInputFrame
			icon.ImageTransparency = 1
			table.insert(tweensForTriggered, TweenService:Create(icon, tweenInfoQuick, { ImageTransparency = 1 }))
			table.insert(tweensForTriggerEnded, TweenService:Create(icon, tweenInfoQuick, { ImageTransparency = 0 }))
		end
	elseif inputType == Enum.ProximityPromptInputType.Touch then
		local buttonImage = Instance.new("ImageLabel")
		buttonImage.Name = "ButtonImage"
		buttonImage.BackgroundTransparency = 1
		buttonImage.Size = UDim2.fromOffset(25, 31)
		buttonImage.AnchorPoint = Vector2.new(0.5, 0.5)
		buttonImage.Position = UDim2.fromScale(0.5, 0.5)
		buttonImage.ImageTransparency = 1
		buttonImage.Image = "rbxasset://textures/ui/Controls/TouchTapIcon.png"
		buttonImage.Parent = resizeableInputFrame

		table.insert(tweensForTriggered, TweenService:Create(buttonImage, tweenInfoQuick, { ImageTransparency = 1 }))
		table.insert(tweensForTriggerEnded, TweenService:Create(buttonImage, tweenInfoQuick, { ImageTransparency = 0 }))
	else
		local buttonImage = Instance.new("ImageLabel")
		buttonImage.Name = "ButtonImage"
		buttonImage.BackgroundTransparency = 1
		buttonImage.Size = UDim2.fromOffset(28, 30)
		buttonImage.AnchorPoint = Vector2.new(0.5, 0.5)
		buttonImage.Position = UDim2.fromScale(0.5, 0.5)
		buttonImage.ImageTransparency = 1
		buttonImage.Image = "rbxasset://textures/ui/Controls/key_single.png"
		buttonImage.Parent = resizeableInputFrame
		table.insert(tweensForTriggered, TweenService:Create(buttonImage, tweenInfoQuick, { ImageTransparency = 1 }))
		table.insert(tweensForTriggerEnded, TweenService:Create(buttonImage, tweenInfoQuick, { ImageTransparency = 0 }))

		local buttonText = Instance.new("TextLabel")
		buttonText.Name = "ButtonText"
		buttonText.Position = UDim2.fromOffset(-1, -1)
		buttonText.Size = UDim2.fromScale(1, 1)
		buttonText.Font = Enum.Font.GothamSemibold
		buttonText.TextSize = 14
		buttonText.BackgroundTransparency = 1
		buttonText.TextTransparency = 1
		buttonText.TextColor3 = Color3.new(1, 1, 1)
		buttonText.TextXAlignment = Enum.TextXAlignment.Center
		buttonText.Text = UserInputService:GetStringForKeyCode(prompt.KeyboardKeyCode)
		buttonText.Parent = resizeableInputFrame
		table.insert(tweensForTriggered, TweenService:Create(buttonText, tweenInfoQuick, { TextTransparency = 1 }))
		table.insert(tweensForTriggerEnded, TweenService:Create(buttonText, tweenInfoQuick, { TextTransparency = 0 }))
	end

	if inputType == Enum.ProximityPromptInputType.Touch or prompt.ClickablePrompt then
		local button = Instance.new("TextButton")
		button.BackgroundTransparency = 1
		button.TextTransparency = 1
		button.Size = UDim2.fromScale(1, 1)
		button.Parent = promptUI

		button.InputBegan:Connect(function(input)
			if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
				prompt:InputHoldBegin()
			end
		end)
		button.InputEnded:Connect(function(input)
			if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
				prompt:InputHoldEnd()
			end
		end)

		promptUI.Active = true
	end

	if prompt.HoldDuration > 0 then
		local circleBar = createCircularProgressBar()
		circleBar.Parent = resizeableInputFrame
		table.insert(tweensForButtonHoldBegin, TweenService:Create(circleBar.Progress, tweenInfoInFullDuration, { Value = 1 }))
		table.insert(tweensForButtonHoldEnd, TweenService:Create(circleBar.Progress, tweenInfoOutHalfSecond, { Value = 0 }))
	end

	return promptUI, tweensForButtonHoldBegin, tweensForButtonHoldEnd, tweensForTriggered, tweensForTriggerEnded
end

local function onLoad()

	local gui = createMainFrame()

	ProximityPromptService.PromptShown:Connect(function(prompt, inputType)
		local promptUI, tweensForButtonHoldBegin, tweensForButtonHoldEnd, tweensForTriggered, tweensForTriggerEnded = createPrompt(prompt, inputType)
		promptUI.Parent = gui
		
		for _, tween in ipairs(tweensForTriggerEnded) do
			tween:Play()
		end
		
		local holdBeganConnection
		local holdEndedConnection
		local triggeredConnection
		local triggerEndedConnection

		if prompt.HoldDuration > 0 then
			holdBeganConnection = prompt.PromptButtonHoldBegan:Connect(function()
				for _, tween in ipairs(tweensForButtonHoldBegin) do
					tween:Play()
				end
			end)

			holdEndedConnection = prompt.PromptButtonHoldEnded:Connect(function()
				for _, tween in ipairs(tweensForButtonHoldEnd) do
					tween:Play()
				end
			end)
		end

		triggeredConnection = prompt.Triggered:Connect(function()
			for _, tween in ipairs(tweensForTriggered) do
				tween:Play()
			end
		end)

		triggerEndedConnection = prompt.TriggerEnded:Connect(function()
			for _, tween in ipairs(tweensForTriggerEnded) do
				tween:Play()
			end
		end)

		prompt.PromptHidden:Wait()

		if holdBeganConnection then
			holdBeganConnection:Disconnect()
		end

		if holdEndedConnection then
			holdEndedConnection:Disconnect()
		end

		triggeredConnection:Disconnect()
		triggerEndedConnection:Disconnect()
		
		for _, tween in ipairs(tweensForTriggered) do
			tween:Play()
		end
		
		wait(1)

		promptUI.Parent = nil
	end)
end

onLoad()
3 Likes

This is genuinely extremely helpful for a lot of newer developers who aren’t experienced enough to make this on their own. It even is good for more experienced developers as they can customize the prompt.

There is one issue I want to address though and that’s the fact that, along with everything else, when things get overused a ton they tend to lose value. What I’m saying here is if that a bunch of low-quality games start using this feature, it might bring the reputation of this feature down, as it seems extremely easy to implement.

If games like adopt me or jailbreak attempt to use this feature, it might make it seem like those games themselves are low-effort for using the feature in the first place. I don’t know if I’m the only one who thinks this, but that’s my opinion.

3 Likes

There is a “player” argument, I think it’s that

EDIT: O I accidentally replied to you @WoofWoofWatermelonYT this was ment for @comsurg

3 Likes

This feature will only currently work in studio. It isn’t fully released yet.

1 Like

If you remove both ObjectText and ActionText it will look like this:

image

3 Likes

More popular games can use their own UI to still benefit of this feature (since you don’t have to handle client-server communication for example).

And, I don’t think we should worry about “lower quality” games using this feature. We actually want to get those games receive more quality, as that would improve the reputation of Roblox, which may keep players playing games on Roblox longer, resulting they might end up coming to your game :upside_down_face:

2 Likes

I have a feeling this will replace ClickDetectors in most people’s code. It’s much easier for the user to use something like this instead of ClickDetectors, and it’s much easier for developers to implement this instead of going the old Magnitude way, which with a lot of assets could really create lag. Also seems to standardize and solidify the new user interface style, which is always nice to see. Of course though, there are going to be users who would want to customize the interface, so I suggest to add in support for custom styles if possible in the future. (Already in)

Here is a visualization of the script provided by OP, for a Seat.

example

Overall, thank you for this unexpected amazing feature. Can’t wait for it to be released!

8 Likes

I can publish my games with this feature or not because I am on beta?

2 Likes

The UI looks really great! This is a nice update, thanks Roblox!

3 Likes

I did notice one thing, if you were to get up close and personal to a object and would rotate your camera directly inside the part, or at least to a certain degree the prompt would not pop up and would not trigger what so ever. even when ticking RequiresLineOfSight to be false.

2 Likes

a very great feature!

i wonder what other developers will do with proximityprompts :eyes:

1 Like

Features like this which are in Studio Beta should not be used in production games. They’re released in this way such that you can get a head-start developing against them and provide feedback as you run into issues.

You have to wait for the full release announcement to use them in live games.

8 Likes

This is amazing! It makes it super simple to make games work cross platform with the way it is setup.

This opens up a new world of opportunity with these properties.
Something I would like to see is making all UI buttons with these properties. This would be helpful for Xbox especially if you could designate an action to be ‘X’ on gamepad but a different keybind/button on PC.
Would be a step in the right direction for the hack-y work around to detect what device you are on.

2 Likes

The mechanic already Exists. There is a property called GamepadKeycode.

Not sure if anybody has reported this bug (appologies if they have), but when using ProximityPrompts in a Tool, unequipping the tool will cause the Prompt GUI to stay fixed in the location where the tool was unequipped.

I took a video which is shown below:
https://gyazo.com/b3d50747cd4afe5184b5e320202cda3c

Thanks for the report! I pasted in your code and was unable to reproduce the issue. Here’s the file:

ProximityDevforumBug.rbxl (20.6 KB)

Do you happen to have a rbxl file that demonstrates the issue? Thanks!

4 Likes

It seems to be working fine now, so you don’t have to worry about it. :slight_smile:

After trying out this feature I have decided I really like it and I think it will be a great replacement to things such as click detectors. Funny thing is I made something like this just a few days before the announcement oh well.

3 Likes

Thanks for replying :slight_smile: and I hope release this feature near time

Loving the feature so far and I can’t wait for it to be available in the player!
I was wondering if you are planning on making it so that if there are multiple ProximityPrompts parented to the same part, if they would stack on each other, like this
image
as currently they are stacking like this:
image

Edit: @Crystalflxme has said a bit more clearly what I am trying to say.

4 Likes