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.
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.
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()