VR Teleportation Movement using Hands

This is my second tutorial in my series of VR tutorials. If you have not yet setup your VR hands, please follow my first tutorial of the series here.

In this tutorial we will add teleporation movement to our hands. Why teleportation? Teleportation is by far the most comfortable form of travel in VR. A lot of people get very sick of normal movement and teleportation is a lot more comfortable for them.

To start off, let’s take a look at the VR controllers. There are 3 very common VR headsets, and I’m pretty sure it works on all 3 of them but I can only confirm it working on 2 of them. The 3 headsets being the Oculus Rift, HTC Vive and the 5 Windows Mixed Reality Headsets. I know for a fact that it works on the Vive and the WMR headsets, but if there is someone with an Oculus Rift that can confirm that this is working for them that would be great!

The controls for VR are a bit sloppy. You can’t really get joystick input so we have to use the trackpad buttons for movement. The concept is simple, the user presses the trackpad and they get teleported to wherever their hand is pointing!

I’ve included some images below of all the buttons for the headsets. Find your headset and what button you would like to use and then we can move on!

Windows Mixed Reality controller:

HTC Vive controller:

The button I’m going to be using is ButtonL3. Let’s get to coding it!

The first thing we’re going to do is create a new function which we will run whenever the button is pressed. In this function we’re going to shoot a raycast from the left hand. (Or right hand if you want to use that for movement). How does a raycast work? Take our 2 hands and let’s say we were holding a laser beam shooting forward from our hand position. Wherever that beam hits the floor is where we will teleport.

A raycast requires 3 properties. Origin, Direction and RaycastParameters. The origin is the starting point of the raycast. (The left hand in our case) The Direction is the direction the ray will be shooting towards. (In our case wherever the left hand is looking) and RaycastParameters are optional parameters where you can for example choose a list of parts you want to ignore. We will use RaycastParams.new() since we’re not going to ignore any parts. Let’s set up the script!

local function teleportToHand()
	local origin = leftHand.Position
	local direction = leftHand.CFrame.LookVector * 100
	local raycastResult = game.Workspace:Raycast(origin, direction, RaycastParams.new())

end

You can replace the 100 by any number in order to change the maximum teleport distance. When you don’t multiply at all, the maximum distance is 1 stud. The LookVector is just the direction the hand is looking at.

Now we can check if the raycastResult is empty or not, and if it is not empty we can teleport to that position!

RaycastResult has a couple of properties we can make good use of! The first one is Instance. The Instance is the part or object that the raycast has hit. So if we hit the Baseplate, then raycastResult.Instance would hold the Baseplate and we could modify it or do whatever we want with it. We can also check things with it. For example if we hit our own hand we should not teleport to it. So we can check if the parent of the raycastResult.Instance is the player’s character and then just not teleport.

We can also use RaycastResult.Normal. This will return a Vector3 of the angle of wherever we hit. How does that work? Well say we hit a wall, then the normal would point away from the wall and the Vector3’s Y would be 0.

afbeelding

Can you see what we’re doing here? If the Y of the normal is lower then 0.6 the object is quite heavily angled and we should not teleport either. So with these new checks in place, the new code looks like this:

local function teleportToHand()
	local origin = leftHand.Position
	local direction = leftHand.CFrame.LookVector * 100
	local raycastResult = game.Workspace:Raycast(origin, direction, RaycastParams.new())

	if raycastResult then
		if raycastResult.Instance.Parent ~= character and raycastResult.Normal.Y > 0.4 then
			...
		end
	end
end

Now let’s actually teleport the player! The teleportation is actually pretty simple. We take the position where the raycast hit, and then add the player’s height to it. Then we set the camera’s CFrame to that position and we’re done. Right? Well, technically yes but if we set the camera’s CFrame to this:

camera.CFrame = CFrame.new(raycastResult.Position + Vector3.new(0, 4, 0))

The camera’s angles will completely reset! We should first store the camera’s angles, and then multiply by that once we teleported so that we don’t change camera angles when teleporting. Storing the angles is pretty simple. We take the CFrame of the camera, and then remove the position of the camera from it. That is because a CFrame holds positional and rotational information. So if we remove the positional information, the rotational information remains! So, store a reference of the camera angles and then multiply by them later on and we’ve got this:

local cameraAngles = camera.CFrame - camera.CFrame.Position
camera.CFrame = CFrame.new(raycastResult.Position + Vector3.new(0, 5, 0)) * cameraAngles

Now there is just one more issue. The player’s head moves around. That means that the head is never at the same position as the camera. So if the camera is at a certain position, the head might be a bit in front of it, or a little bit more to the left of it. So if we move the camera’s CFrame to the position, the head will actually not go there because it is a bit off. You can see it in the image below.
afbeelding
So let’s say you are the little headset with the hands and the camera is ofcourse the camera. Your little hand shoots the raycast and would you guess it? You hit the green dot. The camera now moves there, but your headset doesn’t move there at all because it is relative to the camera.

How do we fix this? Simple. We remove the head’s position from the solution and we’re done! This can be easily done because we can request the position of the head at any time using the VRService! It would look as follows:

local vrService = game:GetService("VRService") --Add vr service at the top!
...
local headCFrame = vrService:GetUserCFrame(Enum.UserCFrame.Head)

So now let’s add the pieces together to create this:

local function teleportToHand()
	local origin = leftHand.Position
	local direction = leftHand.CFrame.LookVector * 100
	local raycastResult = game.Workspace:Raycast(origin, direction, RaycastParams.new())

	if raycastResult then
		if raycastResult.Instance.Parent ~= character and raycastResult.Normal.Y > 0.4 then
            local cameraAngles = camera.CFrame - camera.CFrame.Position
			local headCFrame = vrService:GetUserCFrame(Enum.UserCFrame.Head)

            camera.CFrame = CFrame.new(raycastResult.Position + Vector3.new(0, 5 + headCFrame.Position.Y, 0) - headCFrame.Position) * cameraAngles
		end
	end
end

We add the Y position of the head to our player height so that we can also teleport while ducking.
Now we need to bring this puppy alive! Let’s create a simple button release event and in it, check for your button. Then trigger the teleport function and you’re done! Here is my button release event:

inputService.InputEnded:Connect(function(key, processed)
	if key.KeyCode == Enum.KeyCode.ButtonL3 then
		teleportToHand()
	end
end)

Let’s fire up the game and try it out!
It works great! But we are very tall and we can’t really see where we are going. So let’s give the user some visual feedback and reset our height on startup.

To reset the height, just recenter the player’s headset on startup. Do this using the vrService like so:

vrService:RecenterUserHeadCFrame()

This will put our height to 0 and makes us a lot less tall. Now for the visual feedback. It would be nice of we kept track of whenever the trackpad is held down or not. We could do this easily using a boolean. Just add a now boolean at the top of your script and then in your input events, change the boolean to true or false depending on wether the trackpad (or your chosen button) is pressed or not. Here are my final events:

local teleportPressed = false --This goes at the top of the script.
...
inputService.InputBegan:Connect(function(key, processed)
	if key.KeyCode == Enum.KeyCode.ButtonL3 then
		teleportPressed = true
	end
end)

inputService.InputEnded:Connect(function(key, processed)
	if key.KeyCode == Enum.KeyCode.ButtonL3 then
		teleportPressed = false
		teleportToHand()
	end
end)

Now let’s create a little part at the end of our raycast showing where we are going to teleport. We’re going to create a RenderStepped event which is ran before everything is displayed to the screen to prevent flickering. First add your runService reference at the top. The RenderStepped event is using it. Then we can copy most of our raycasting code and fire it whenever teleportPressed equals true. Like so:

local runService = game:GetService("RunService") --This goes at the top of the script.
...
runService.RenderStepped:Connect(function()
	if teleportPressed then
		local origin = leftHand.Position
		local direction = leftHand.CFrame.LookVector * 100
		local raycastResult = game.Workspace:Raycast(origin, direction, RaycastParams.new())

		if raycastResult then
			if raycastResult.Instance.Parent ~= character then
				...
			end
		end
	end
end)

Now we create a simple part that will move to the result of our raycast and we’re done! We will also remove the ball after a heartbeat (Which is basically the same as wait() but more efficient and good practice) and then we destroy it. The final RenderStepped event will look something like this:

runService.RenderStepped:Connect(function()
	if teleportPressed then
		local origin = leftHand.Position
		local direction = leftHand.CFrame.LookVector * 100
		local raycastResult = game.Workspace:Raycast(origin, direction, RaycastParams.new())

		if raycastResult then
			if raycastResult.Instance.Parent ~= character then
				local hit = Instance.new("Part")
				hit.Parent = character
				hit.Material = Enum.Material.SmoothPlastic
				hit.Anchored = true 
				hit.Position = raycastResult.Position
				hit.Size = Vector3.new(0.5, 0.5, 0.5)

				runService.Heartbeat:Wait()
				hit:Destroy()
			end
		end
	end
end)

Let’s try our visual feedback now!

Hooray! And that is all you need for comfortable VR movement! If you have questions about parts of the code, or about anything else, feel free to reply to this thread. The next tutorial will be about interacting with objects in the world. Hope to see you there!

Final code so far:

local player = game:GetService("Players").LocalPlayer
local character = player.Character
local camera = game.Workspace.CurrentCamera
local starterGui = game:GetService("StarterGui")
local inputService = game:GetService("UserInputService")
local vrService = game:GetService("VRService")
local runService = game:GetService("RunService")
local teleportPressed = false

camera.CameraType = "Scriptable"
camera.HeadScale = 1

starterGui:SetCore("VRLaserPointerMode", 0)
starterGui:SetCore("VREnableControllerModels", false)

character.HumanoidRootPart.Anchored = true
workspace.CurrentCamera.CFrame = CFrame.new(workspace.CurrentCamera.CFrame.Position)

local function createHand(handType)
	local hand = Instance.new("Part")
	hand.Parent = character
	hand.CFrame = character.HumanoidRootPart.CFrame
	hand.Size = Vector3.new(0.4, 0.4, 1)
	hand.Transparency = 0
	hand.Color = Color3.new(1, 0.72, 0.6)
	hand.Material = Enum.Material.SmoothPlastic
	hand.CanCollide = false
	hand.Anchored = true
	hand.Name = handType
	return hand
end

local leftHand = createHand("rightHand")
local rightHand = createHand("leftHand")

local function teleportToHand()
	local origin = leftHand.Position
	local direction = leftHand.CFrame.LookVector * 100
	local raycastResult = game.Workspace:Raycast(origin, direction, RaycastParams.new())

	if raycastResult then
		if raycastResult.Instance.Parent ~= character and raycastResult.Normal.Y > 0.4 then
			local cameraAngles = camera.CFrame - camera.CFrame.Position
			local headCFrame = vrService:GetUserCFrame(Enum.UserCFrame.Head)

			camera.CFrame = CFrame.new(raycastResult.Position + Vector3.new(0, 5 + headCFrame.Position.Y, 0) - headCFrame.Position) * cameraAngles
		end
	end
end

inputService.UserCFrameChanged:Connect(function(part, move)
	if part == Enum.UserCFrame.LeftHand then
		leftHand.CFrame = camera.CFrame * move
	elseif part == Enum.UserCFrame.RightHand then
		rightHand.CFrame = camera.CFrame * move
	end
end)

inputService.InputBegan:Connect(function(key, processed)
	if key.KeyCode == Enum.KeyCode.ButtonL3 then
		teleportPressed = true
	end
end)

inputService.InputEnded:Connect(function(key, processed)
	if key.KeyCode == Enum.KeyCode.ButtonL3 then
		teleportPressed = false
		teleportToHand()
	end
end)

runService.RenderStepped:Connect(function()
	if teleportPressed then
		local origin = leftHand.Position
		local direction = leftHand.CFrame.LookVector * 100
		local raycastResult = game.Workspace:Raycast(origin, direction, RaycastParams.new())

		if raycastResult then
			if raycastResult.Instance.Parent ~= character then
				local hit = Instance.new("Part")
				hit.Parent = character
				hit.Material = Enum.Material.SmoothPlastic
				hit.Anchored = true 
				hit.Position = raycastResult.Position
				hit.Size = Vector3.new(0.5, 0.5, 0.5)

				runService.Heartbeat:Wait()
				hit:Destroy()
			end
		end
	end
end)

vrService:RecenterUserHeadCFrame()
23 Likes

Can’t wait for part 3!

6 Likes

Hi Nathan again so does this script go inside the same script that we used in the last tutorial because I am a bit confused on where to put the scripts again. Also sorry in advance again I am still getting used to scripting and its a bit confusing on where to put it but yeah.

Still everything in the same script! I will make it clear when moving to another script.

1 Like

Ok thanks again as I am not experinced in scripting so not wasn’t sure. Onto the next tutorial!

Wow, this is cool!
Let’s say I wanted to weld a object to the right arm, how could I go about doing that?

Hello!
I just tested this with my Oculus Quest using Link and I can confirm it works! You can teleport by pressing the joystick on your left controller!
I’m really glad this works because I now finally understand something about VR in Roblox and I can finally make cool VR projects! Thanks man!

2 Likes

Will cover in the next tutorial. Not a lot of time though, so it’s going to take some time. We will also fix a lot of issues and I’m also planning on some other cool things like bow and arrow physics, etc.

Woah that’s awesome! I can’t wait!!

this is what i have been needing since december, thanks for the great guide!