Using UserInputService to make an 'E-To-Open Door' function (All platforms supported.)

Hey! So I’ve seen a lot of people ask about making things that take a key to open. So, I’ve decided to make an in-depth (yet understandable) tutorial on this.

Things to Know

Alright, so before we start, I’m going to need you to know some things. Here they are:

UserInputService

The UserInputService on Roblox is a service that registers user input. Literally the most self-explanatory resource on Roblox.

Anyways, UserInputService has 2 key functions that we’ll be using in this tutorial:

  • InputBegan
  • InputEnded

InputBegan fires when the user has done something. Even WASD, arrow keys, number pad, Xbox, and more can be registered!

InputEnded fires when the user releases a key. Say you’re pressing W to move forward, and you stop moving to go AFK. As soon as the key pops back up, InputEnded is fired.

Enum

Alrighty, Enum is a really cool feature too. The way I think of whether an item is an EnumItem or not is:
Anything that, in Properties, can be changed through a dropdown menu is an EnumItem. This may be wrong, but everything I’ve ever had to code follows this principle.

Enum is also accessible from anywhere. No need to define anything to get access, all you need to type is: “Enum”.

TweenService

TweenService is a service accessed by the game:GetService() function. TweenService moves parts based on three properties: the floatTime (how long it’ll take to get from a to b in seconds), EasingStyle(the way the tween will look), and EasingDirection(how the speed will be divided.) EasingStyle and EasingDirection are both EnumItems.

Alright, we should be good!

Getting Started

Okay, so for this tutorial, we’ll be using the example of a door. Doors are cool to have an interaction function with, because they can give us the ability to have interiors that the players can explore.

HOWEVER, you can also do this with quests. Maybe, if this gets enough attention, I’ll make one on quests sometime. You can also use this tutorial to get an understanding of essential resources.

Building the Door

So, first we need to build our door. Now, I suck at building and I don’t want to build a whole new door. I made this:

I’m going to duplicate this (CTRL + D / CMD + D) so that I can show you guys how to use one BillboardGui for all doors.

Done! By the way, this is my Explorer right now:

image

Okie dokie, so now that we have our doors, let’s make a BillboardGui.

Making the BillboardGui

Okay, let’s make this!

So, put a BillboardGui into the StarterGui in Explorer. This will make it local to every player, so each player has their own Gui. Call it “Door” and insert a frame. Now, you can’t see it, right? Well, you need to set the Adornee. Go into Properties and click to the right of where it says “Adornee.” Now, click on your door. Only one. It doesn’t matter which.

Now, while you’re in the Properties, set this three properties:

MaxDistance to 15 (or another value, depending on how far you want your players to access it.)
AlwaysOnTop to true.
Size to {1.5, 0, 1.5, 0}

Okay, now you can see it! So go into the frame, and change it’s size to {1, 0, 1, 0}. This makes it compatible with all screen sizes.

Alright, now let’s insert a TextLabel and a TextButton. Call the TextButton “Key” and the TextLabel “Function.” Set the size of the “Key” TextButton to {1, 0, 0.75, 0}. Set the Function TextLabel’s size to {1, 0, 0.25, 0}, and it’s position to {0, 0, .75, 0}.

Are you following? Great!

Now we can customize the TextLabels in Properties. I’ll be using Stelrex’s uiDesign plugin to make mine look great.

uiDesign Plugin

uiDesign has a free version and a paid version. The paid version is 100 R$.

Free: https://www.roblox.com/library/4641259605/uiDesign-Lite
Paid: https://www.roblox.com/library/4474084133/uiDesign-Plus

Done mine!

Now, let’s start coding!

Coding the E-to-Interact

Okay, coding is my thing. I do primarily coding, hence why my builds were garbage.

Go into the BillboardGui, and into the Frame, then insert a LocalScript.

Put this in there:

local UserInputService = game:GetService("UserInputService") -- Defining the UserInputService.
local Key = Enum.KeyCode[script.Parent.Key.Text] -- The key we'll be using.  All you have to do is edit the "Key" TextLabel's text to change the Key.
local Extras = {
	Mobile = {Text = "Tap", Button = nil}; -- Tap, but we'll be using the TextButton for that.
	Xbox = {Text = "RB", Button = Enum.KeyCode.ButtonR1}; -- R1 is the Right Bumper.
}

UserInputService.InputBegan:Connect(function(input, processed) -- Bind our function to the InputBegan event.
	if processed then return end -- If the game has already processed the input, don't do anything.  InputBegan fires 2-3 times each input, for some reason.
	
	if input.UserInputType == Enum.UserInputType.Keyboard then -- If the action was on a keyboard, continue.
		if input.KeyCode == Key then -- if the key matches then continue
			InputCorrect("Closest")
		end
	elseif input.UserInputType == Enum.UserInputType.Gamepad1 then -- If you don't have Console Support, stop here, and don't write anything else in this section. | if the action was on a gamepad (Xbox controller), continue.
		if input.KeyCode == Extras.Xbox.Button then -- If the button pressed was the button assigned, continue.
			InputCorrect("Closest")
		end
	end
end)

So now we are registering the input. Now, code this ABOVE the InputBegan function, but below the the variables.

local function InputCorrect(action)
	if action == "Open Door" then -- Choose your action's name.
		-- HERE YOU CODE WHATEVER YOU WANT TO.  QUESTS, PICK UPS, DOORS, ETC.
		game.ReplicatedStorage.Doors.OpenDoor:FireServer("Closest") -- I'm transfering responsibility to the Server, so that all players can see the door being opened.
		--[[
			"Closest" will make the Server figure out the door itself.
			Defining a physical door object will make the Server use that.
		]]--
	end
end

You’re done coding the client for now. Your LocalScript should look like this:

local UserInputService = game:GetService("UserInputService") -- Defining the UserInputService.
local Key = Enum.KeyCode[script.Parent.Key.Text] -- The key we'll be using.  All you have to do is edit the "Key" TextLabel's text to change the Key.
local Extras = {
	Mobile = {Text = "Tap", Button = nil}; -- Tap, but we'll be using the TextButton for that.
	Xbox = {Text = "RB", Button = Enum.KeyCode.ButtonR1}; -- R1 is the Right Bumper.
}

local function InputCorrect(action)
	if action == "Open Door" then -- Choose your action's name.
		-- HERE YOU CODE WHATEVER YOU WANT TO.  QUESTS, PICK UPS, DOORS, ETC.
		game.ReplicatedStorage.Doors.OpenDoor:FireServer("Closest") -- I'm transfering responsibility to the Server, so that all players can see the door being opened.
		--[[
			"Closest" will make the Server figure out the door itself.
			Defining a physical door object will make the Server use that.
		]]--
	end
end

UserInputService.InputBegan:Connect(function(input, processed) -- Bind our function to the InputBegan event.
	if processed then return end -- If the game has already processed the input, don't do anything.  InputBegan fires 2-3 times each input, for some reason.
	
	if input.UserInputType == Enum.UserInputType.Keyboard then -- If the action was on a keyboard, continue.
		if input.KeyCode == Key then -- if the key matches then continue
			InputCorrect("Open Door")
		end
	elseif input.UserInputType == Enum.UserInputType.Gamepad1 then -- If you don't have Console Support, stop here, and don't write anything else in this section. | if the action was on a gamepad (Xbox controller), continue.
		if input.KeyCode == Extras.Xbox.Button then -- If the button pressed was the button assigned, continue.
			InputCorrect("Open Door")
		end
	end
end)

Now, make a Folder in ReplicatedStorage called “Doors”. Then, add a RemoteEvent to the Folder called “OpenDoor”.

Now, insert a Script into ServerScriptService. Call it “Doors”, and put this in:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Doors = ReplicatedStorage:WaitForChild("Doors")

local TweenService = game:GetService("TweenService")

local increment = 3 -- Change this to the time (in seconds) that you'd like the game to wait before closing the door.

Doors.OpenDoor.OnServerEvent:Connect(function(player, door)
	print("Got event")
	
	if typeof(door) == "string" then
		print("string")
		if door == "Closest" then
			local lowest = 1000000 -- A very high number
			local lowestDoor = nil
			for _, Door in pairs(workspace:GetDescendants()) do
				if Door.Name == "DoorPart" then
					if (player.Character.HumanoidRootPart.Position - Door.Position).magnitude < lowest then
						lowestDoor = Door
						lowest = (player.Character.HumanoidRootPart.Position - Door.Position).magnitude
					end
				end
			end
			
			local tween = TweenService:Create(lowestDoor, TweenInfo.new(2, Enum.EasingStyle.Quint, Enum.EasingDirection.InOut, 0, false, 0), {Position = lowestDoor.OpenedPos.Value, Orientation = lowestDoor.OpenedRot.Value})
			tween:Play()
			wait(increment)
			tween = TweenService:Create(lowestDoor, TweenInfo.new(2, Enum.EasingStyle.Quint, Enum.EasingDirection.InOut, 0, false, 0), {Position = lowestDoor.ClosedPos.Value, Orientation = lowestDoor.ClosedRot.Value})
			tween:Play()
		else
			warn("Unregistered type of string for variable 'door'.  Please review.")
			return
		end
	end
end)

If you test now, you’ll see an error along the lines of, “Attempt to index a nil value ‘OpenedPos’.” Let’s fix that. Go to your door with the BillboardGui, and create four Vector3 Values. Call one “OpenedPos,” another “OpenedRot”, one called “ClosedPos”, and the final one called “ClosedRot”.

Now, we must be VERY careful when doing this next step. It tells the game how we want our game to make our doors look.

Making the Door Placement Values

In EDIT mode, go to your door, and open it using the Rotation and Move Tools. Now, use a noting software, such as Microsoft Word, Google Docs, or Notepad.

Write the two names of the Opened___ values. Then, copy the door’s opened position. After you’ve copied, click beside OpenedPos and paste the 3 numbers into your noting software. Then, move on to the OpenedRot value. Copy the door’s opened orientation, and paste it beside “OpenedRot.”

Once you’ve done that, go back into studio, hold control, and press Z until your door is back closed. Now you can just copy and paste the orientation and position of the closed door into the Closed___ values accordingly.

Now, select all four values, and press CTRL/CMD + C. Now, select the two other doors, and press CTRL/CMD + V.

Do the values for each door. It’ll take a little while, but it’s totally worth it.

Coding the BillboardGui’s Adornee Changing

Now that we’re done that time-consuming, annoying step, let’s code our final aspect of the system: the BillboardGui.

“But Maximuss, we already coded the gui!” We did, but not completely. We need to make the BillboardGui’s Adornee the closest door. Let’s do this now.

Add a second LocalScript into the BillboardGui, called “AdorneeAdjustments”. Put this in that script:

local BillboardGui = script.Parent.Parent -- The BillboardGui

local player = game.Players.LocalPlayer -- The player

repeat wait(1) until game:IsLoaded() == true -- Wait until the game is fully loaded, so we don't get any errors

while true do
	wait(1)
	local lowest = 10000000 -- a very high number
	local lowestDoor = nil
	for _, door in pairs(game.Workspace:GetDescendants()) do
		if door.Name == "DoorPart" then
			if (player.Character.HumanoidRootPart.Position - door.Position).magnitude < lowest then
			lowest = (player.Character.HumanoidRootPart.Position - door.Position).magnitude
				lowestDoor = door
			end
		end
	end
	-- Notice the similarities in this code from the one in ServerScriptService?  It all adds up!
	BillboardGui.Adornee = lowestDoor
end

Alright, now test!

The Doors Work! (YouTube Video)

YES!

So, all you need to do now is make sure the door part of all your builds is called “DoorPart”, set these values inside all doors, and your UI should work!

Thanks for sticking with me, and I hope this helped! :slight_smile:

P.S. Let me know if you have any issues with your broken script’s code, and I’ll try to diagnose your issue!

35 Likes

Just general advice: try to not use local scripts as much as possible because lets say thats a VIP only door, exploiters can change the code so they can access it instead of just VIP.

3 Likes

Good idea, actually. I just used that because of Input, but yes, that is good advice.

In my code I usually use Modules, but I code core stuff more, including Data Saving, and custom Services. I just know a bit of this because I code everything with my development partner, @XGetRect9X

1 Like

I could be wrong but I think modules can be accessed aswell.

They can, however I do believe it’s a local change, not server.

That is correct. Its to do with the fact that you cant normally see script but you can see modules and local.

The best way to test is to use F3X to see what is accessible from the tool.

2 Likes

This should be done on local scripts only. Even without the local scripts, exploiters can just change the collisions to bypass the door.

1 Like

I would suggest that you revise this tutorial, in my opinion you have some unneeded code that could be done much simpler. In your local script, you should have a minimum magnitude that will be checked inside the loop. I also see some bad practices that should be fixed. And inside of your video it shows the door coming outside of the door hole and fitting to the rotated position, you should be using CFrame.Angles with math.rad on the y axis.

I would also recommend to fix up your scripts for everyone’s sake and for readability

Use this tutorial to see how to properly tween a door open

2 Likes

Let’s take the example of Ninjarmadillo1:

To prevent that, you need to use RemoteEvents, so you can send data to the server. For example, if the player press E, the script will fire the RemoteEvent, and in another script, you need to check when the event has been fired, and there we will do Sanity-Checks, used to prevent hackers from skipping checks from the Client, that’s why mostly everything must be done in the Servers. So there we will check if the player is in the distance where you can press E to open the door, check if the player is VIP, etc.

However, thanks for the Tutorial, i needed a Press E to X. Thanks! :+1:

1 Like

By ‘custom services’, do you mean you code with AeroGameFrameWork?

1 Like

To be honest, idk, lol. I just code my own services that help me, for example, not go over the datastore limits.

1 Like

No problem.

Also, I do use a RemoteEvent in there to make sure that the server does it all. In this example, though, I didn’t use a VIP door.

2 Likes

I just skimmed this post but I notice a few things that I really do need to comment on. While it’s welcome to teach material, make sure you also know what you’re discussing because you can be setting developers up for failure even if it is just an introduction on the topic.

Here are my bullet point notes, which are mostly on the code:
(I wrote topdown so the vocabulary may not make immediate sense.)

Client Input Code:

  • You’re introducing inconsistent service indexing. Use GetService for all cases of needing a service (except workspace, that’s optional). Readability is as important as functionality and optimisation. It looks messy when you use GetService sometimes and other times not. Remember that your code sample’s also implicitly teaching practice regarding these kinds of systems.

  • The remote code should be changed. Currently, the client does absolutely no work whatsoever and just opts to fire a remote any time E is pressed, leaving the work to the server. For the sake of reducing server strain and quicker feedback, the client should be the one finding the door and telling the server what door it wants to open. The server can verify the client is allowed first, then accordingly open the door.

Server-side Remote Code:

  • This remote has no checks aside from type-checking the parameter sent through. Not fully sure what the point of this is, but then again, I just skimmed, so I’m not tracking my notes well.

  • The server is doing unnecessary work, it has no business finding the closest door to the player. Have the client fire the remote with the exact door it wants to open instead and have the server sanity check if the client is allowed to open that door (e.g. are they close enough? are they allowed to open doors in their/the game’s current state?).

  • More of a tip than a criticism: if you want smooth tweens, tween on the client, not the server. The server can, if the client is allowed to toggle the door, inform other clients via remote to also animate a door open.

Adornee Changing Code:

  • Keep consistency with services. You use GetService on some and not on others. Players is still a service, canonically it should be gotten with GetService as well. This also avoids silly troubles such as naming (though you should never actually rename a service intentionally except to monkey patch skid exploits).

  • Do not use loops where not necessary. The repeat until loaded loop isn’t required: as your LocalScript is a decendant of StarterGui, it is protected by game.Loaded. That is, this script will not start executing until game.Loaded fires. Repeat runs at least one iteration of its body, so now you’ve wasted ~1 seconds yielding.

  • RunService may offer you better options than a while loop for checking what item is the closest to you. It doesn’t take a whole lot to do a quick check on the magnitude of the difference of positions. A while loop waiting 1 second may not provide near-instant client feedback which isn’t UX-friendly.

  • Don’t yield at the start of a loop. Please.

  • Opt to use CollectionService to create a list of door parts that you can iterate over rather than iterating over the descendants of the workspace every one second. That is a great way to create lag and a miserable experience.

6 Likes

Thanks! I’ve read through this, and, to be honest, I didn’t know that CollectionService did that, or that the client makes smoother tweens. Thanks for the feedback, again!

2 Likes

Note: For Dvorak users, it’s > and for Colemak users it’s F to press depsite saying “E” to open as all keys automatically remap to QWERTY. I suggest using GetStringForKeyCode to show what button to interact.

Keyboard Layout Button to press
QWERTY E
QWERTZ E
AZERTY E
Dvorak >
Colemak F
Workman R
Halmak R
Neo L
3 Likes