How to create a Smooth locomotion system for your VR project using UE4.
This tutorial was made possible by the very talented Corysia over in the GDXR Discord channel, thank you for allowing me to turn your work into a tutorial. I recommend heading over to Corysia's Youtube channel to keep up to date with their current projects.
In this tutorial, we will be setting up a smooth locomotion system similar to games like Half-Life Alyx, Pavlov VR and many, many more. This tutorial will allow your player to freely move around the scene, climb stairs, crouch beneath objects, fall of ledges, snap rotate in place and even grab objects without flying away.
One of the most important things in VR is a good movement system and this can often be extremely difficult to achieve. So in this blog post, I'm going to show how you can create your own smooth locomotion system using Unreal Engines default template. Giving us the ability to toggle between teleportation and the ability to smoothly move around the scene. While also including some extra ability such as Jumping, crouching, sprinting and snap turning. I will also show you how to set up custom collisions to stop the player from flying away when they interact with objects within the scene. If you would like access to download the project files from this tutorial you can get them from my Patreon: https://www.patreon.com/GameDevXR
Before we get started you will need to open up the default VR Template (or your project) as were going to use the existing MotionControllerPawn (your VR pawn) and I will be working on the Motion Controller map for ease of use.
1. Setting up our Custom Input's
The default template within unreal already has three of our inputs already created you can find these by going to Edit > Project settings > Input.
- MotionControllerThumbLeft_Y = Move Left Right - MotionControllerThumbLeft_X = Move Forward Back
- MotionControllerThumbRight_X = Snap Rotate Left Right
Our Left controls will be used to make us "Walk around" ThumbLeft_X will be used to move us left and right while ThumbLeft_Y will be used to move us forward and back. ThumbRight_X will be used to snap our rotation left and right.
These inputs are located under the Axis Mapping tab because they require inputs going from -1 to 1 defaulting at 0. but in our case, we're going to set up two new inputs so we can jump and sprint. And we're going to do this by adding them to our Action Mappings section as these inputs will be acting like bool's On or Off.
To do this hit the plus to the right of action mappings twice to create two new inputs. Name the first Jump and the Second Sprint. Now we can add our controller inputs.
For this tutorial ill be using the Oculus Touch inputs but you can always add more for different platforms or only add the inputs for the device your using.
For Jumping I'm going to use the B key (Oculus Touch (R) B Pressed) on my right controller and for sprinting I'm going to click the left stick (Oculus Touch (L) Thumbstick). Your Inputs should now look something like this.
That's it for our inputs and we can now close our project settings for now.
2. Setting up our Motion Controller Pawn.
Now we have our inputs created we can open up our MotionControllerPawn.
By default, our MotionControllerPawn has the Parent class Pawn. We need to change this to give our player the Parent Class of Character. as this will give our player a Capsule component and the ability to have gravity affect us.
We can do this by going to Class Settings located at the top of our window and on the right changing our Parent Class to Character.
You should now see our Components panel change to include a capsule component, arrow and mesh.
With this complete, we now need to select our MotionControllerPawn(Self) from the components list and make sure Use Controller Rotation Yaw is Disabled. As seen in the image below. (yellow arrow on the right).
With our class changed we can switch over to the Viewport tab so we can move our VROrigin's position. You will need to select the VROrigin component and move it down on the Z-Axis to -90 (The bottom of the Capsule).
If you're not using a game mode and the player starts to spawn your player you will need to reposition the MotionControllerPawn in the scene so the camera is on the floor.
3. Setting up or can teleport Bool.
In our MotionControllerPawn Event graph, we can start setting up our ability to toggle between teleportation and our smooth locomotion. To do this Start by creating a Bool Variable called bCanTelleport and making sure it's set to false. We then need to find our InputAction TeleportLeft and Teleport Right. This is where we are going to add some branches. We will need one branch after each pressed and released inputs. you can add a branch by holding B on the keyboard as you left click in the Event Graph. It should look like this.
You can now connect the bCanTelleport bool we created earlier to each of the branch nodes we created.
Now since our bool is set to false we won't be able to teleport. We can now work on setting up our Thumbstick movement.
4. Setting Up Our Thumbstick Movement.
To set up our thumbstick movement inputs we need to find some open space in our motion event graph. I like doing this to the right of our GrabLeft and Grab Right Events. We then need to right-click and search for our MotionControllerThumbLeft_X Input Event and our MotionControllerThumbLeft_Y event we have in our project settings.
Make sure to have both events in the graph. But for now, we're going to work from the ThumbLeft_X event as we can copy what we right for the forward movement with the exception of a single node.
for this section, we need to create two float variables. The first called "Movement Deadzone" With a value of "0.2" and the second "Movement Scaler" With a value of 0.6 If you want to keep your variables organised you can add a new Category by typing in SmoothLocomotion where it says Default on the right. This will let us place all our smooth locomotion variable's here as we will be creating quite a few.
With our Variables organised we now need to drag our Axis Value pin on our Input Axis and create an ABS (Absolute) node. This saves us having to do a greater than and less than branch to see if were going left or right. We can just do a greater than and check if the axis value is greater than our Movement Deadzone "0.2" and finish it by creating a branch and connecting it to the branches condition.
Now from the True output of our Branch we can drag off and create an Add Movement Input node, this is what will let us move in the direction of our Axis Value. We will now need to add in our Movement Scaler Variable and a float Multiplier. to connect to our Movement Input Scaler Value.
The next thing we need to do is bring in a reference to our Camera Component so we can "Get Right Vector" and use this to set our World Direction as our camera will control the reference to the direction we're moving in.
Your code so far should look like this. When you move the thumbstick in the X-axis we check to see if the value is < or > our 0.2 deadzone if it is we can move. we then multiply the Axis Value by our movement scaler to determine the speed we move at. You can change this value to move slower or quicker depending on what you're after. We then get the camera to determine our right and left to move in those directions.
Now to move forward and back we just need to copy everything except our even and connect it to our MotionControllerThumbLeft_Y input.
We will also need to replace our "Get Right Vector" with the "Get Forward Vector" node.
I also recommend commenting your code here and calling it Thumbstick movement
Now if we jump into the level and test in VR we can move around using the left thumbstick.
5. fixing/Setting Up Room Scale movement
One of our issues so far is that our current setup doesn't support the room-scale movement, You can see this by selecting Your capsule component and setting Hidden in-game to false so you can see our player capsule. You'll see that if you move in the real world our camera moves away from the capsule. This itself isn't very good for moving around. as we could make sure our capsule is on the edge of a ledge and if we move over it in the real world then we wouldn't fall off it. But that's what we want.
To fix this we first need to start by creating a "Custom Event" which will be named "Update Roomscale Movement" Which we will be updating with our event tick.
Finding our event tick we can use the already created sequence to "Update our Room Scale Movement"
Moving back to our custom event we can drag from our Exec and create two nodes, the first being "Add actor World Offset" and the second "Add World Offset (VROrigin)."
Now we need to bring in a reference to our "Camera" and our "Capsule Component" we then need to get the world location for both and subtract them from each other.
You will also need to split the output vector pin. And we also need to split the Delta Location pin on our AddActorWorldOffset. With those pins split you can connect the X and Y values from the minus node to the X and Y values of the Delta Location.
We now need to Negate our X and Y vectors for our VROrigin's add world offset. The Negate Vector node essentially acts as a multiply -1 so it inverts our vectors. essentially setting our camera to our capsule and our capsule to our camera.
You will need to connect our X and Y values to our negate node leaving Z as zero. You can then connect the negate vector to our Add World Offset Delta Location node
It's difficult to see in the gif but if you jump in and move around in the real world the capsule will now update its position to match.
6. Setting up Jump and Sprint.
Setting up jump and sprint is probably the easiest part of this process this is because UNreal already has a node we can use to control our jumping. The first thing we need to do is find some free space. In our MotionControllerPawn. Here we are going to add both the custom input actions we created earlier, Jump and Sprint.
To set up our jump all we need to do is drag from our InputAction Jump Exec pin and search Jump. We can now comment on our Jump code as that's it.
Now we can set up our sprinting. if you remember earlier in the tutorial we created a Float Variable called Movement Scaler. We will be changing this value to increase our player's speed. When working with this it's best to think of it as a speed multiplier. Our default speed is 0.6 so we're going to increase this on InputAction Sprint pressed to a value of 1 and on release, we will set it back to our default of 0.6.
We can do this by dragging in our Movement Scaler Variable and setting it. We will need two.
Now we have our Jump and Sprint set up we can jump into the headset and try it out.
7. Adding The Ability to crouch
In this section were going to start by creating two custom events, the first will be called "Initialize Capsule Height" and the second will be called "Set Capsule Height"
For the first part, we're going to concentrate on our Initialise Capsule Height as we will need this so we can reference it in our Set Capsule Height.
Now we need to create a new float Variable named "Previous Capsule Half Height". This will be set on Begin Play and referenced in our Set Capsule Height code later on. (Don't forget to set the category for organisation)
you will need to connect our Initialise capsule height Exec pin to our Set Previous Capsule Half Height. And then drag in a reference to our Capsule Component. We will use this to get the capsules Half heigh and use that to set our Previous Capsule Half Height variable.
We can now move on to changing our Capsule Height which will allow us to physically crouch to move under objects.
we need to bring in a reference to our Capsule Component so we can use it to Get the Scaled Capsule Radius and Set the Capsule Size node. Make sure to drag from the capsule Component reference to get them.
Before adding our math were going to get a reference to our VROrigin and use it to AddRelativeLocation you will want to connect it to the Exec pin of our Set Capsule Size
From our AddRelativeLocation node, we can drag in a reference to our Prevoisue Capsul;e Half Height and we will need a set as we're going to add some math to it. So far your code should look like this.
What were going to do here is start by using the Get Orientation and Position node which lets us access the HMD (Head Mounted Displays) information both rotation and location. We will then divide it by 2 so we get the halfway position and we are going to add 10 to compensate for the top of our head. With the Get Orientation and Position node, you will need to split the Position pin as we only need the Z position.
We also need to connect our + 10 node to our Set Previous Capsule Half Height. and we need to get our Previous Capsule Half Height as a reference so we can subtract our Z position from it. Your code should now look like this.
The last thing we need to do for our crouch is set our Custom events we created to fire. Our Initialise Capsule Height must be updated from Event Begin Play and our Set Capsule Height needs to be updated with On Tick.
Now if we jump into our headset and crouch with the capsule visible we should now see it update letting us crouch.
8. adding Snap rotation.
Snap rotation is by far the most complex part of the system. The first thing we need to do is create five variables one boolean and four floats. These will be.
Boolean named "bSmooth Rotation" = Flase
Float named "Turn Deadzone" = 0
Float named "Smooth Rotation Speed" = 1
Float named "Snap Rotation Degrees" = 15
Float named "Rotation Angle" = 0
With our variables created we can start by creating our input axis. if you remember back to our input setup we already have our thumbstick controls created. For our snap rotation were going to use our InputAxis MotioncontrollerThumbRight_X followed by a delay node = 0.5 and a branch.
Our delay node will stop us from spinning around and give us a small duration after moving the thumbstick before we move. For our branch condition, we will be adding our bSmoothRotation boolean we created. followed by two branch nodes, one connected to both the true and false channel.
From here we're going to first work from the False Exec which will control our snap rotation. For our branch connected to it to work were going to check if our Turn Deadzone is greater than our events Axis value. We will be converting the Axis value using an ABS Absolute node.
What we're doing here is checking that our Axis Value is greater than our Turn Deadzon which is set to 0. Our code so far basically say if smooth rotation is disabled (we will be using snap rotation) and if the thumbstick axis is greater than 0 fire our true Exec. We now need to add another branch to our True Exec and we will use our Greater than check again to see if were turning. we also need to bring in our rotation Angle variable twice and set them from the true and false Exec Pins.
We now need to set our Rotation Angle, we actually have a float variable which we will use for this, our Snap Rotation Angle however we will need to invert this by multiplying by -1 for our False channel Rotation Angle. we will now connect these both to a single DoOnce.
We will have two areas to reset our Do Once node the first is from our False Exec pin from our Branch and the second will be from a sequence node after our do once which will go through a delay node of 0.2 seconds.
From our Sequence Then 0 Exec pin we can add our Set Actor Rotation node (split New Rotation Pin), this is what will rotate us in place from our chosen Rotation Angle (Remember this is zero because it's being set from our Snap Rotation Degrees). we will be working with the New Rotation Z (Yaw) pin. We need to GetActorRotation (Split the Return Value Pin), This node gets our current rotation and then we will add our Rotation angle to it.
Now we can move onto adding our Smooth Rotation. we can do part of this code by copying our get Actor Rotation the + plus node and our Set Actor Rotation. however, we don't need the Rotation angle.
Now we can use our Smooth Rotation Speed Variable we created and multiply our Axis Value by it. and then connecting it to our + Node.
8.1 Fixing an error
In the image above you might have noticed, I've removed the greater than to the second branch (the one with the reroute node) I made a mistake by connecting our > Greater than node to our Branch. We need to delete this connection and create a new > Greater than node from our InputAxis Axis Value. And connect that instead. It should now look like this instead.
With that fixed we can now jump into the project and test out our snap rotation we've set up.
9. creating custom collisions so we can pick up actors.
So if you've tried moving at this point while using the grip button to make fists or even pick up the cubes you will notice that you move away from the direction your facing. as fun as this is, it's pretty useless for your game and our movement system. but we can fix this by setting up a custom collision for our player and making some changes to our motion controller pawn and our grabbable objects.
The reason this is happening is that we're now using a Capsule collision to control our movement, when we overlap with something let's say a wall, it stops us from moving through it. However, when we grab an object it also comes with us so we continue to move away from it.
To fix this we need to open our project settings > Collision and under Object Channels, we will need to select New object Channel. We will then give this the name of PlayerCollision with the default response set to Block.
With our custom channel created we can now go back to our MotionControllerPawn and select our Capsule Component so we can change our Collision Preset to custom and change our Object Type to our new PlayerCollision.
In here we also need to set the player collision channel to Overlap. if you don't do this and bump into an AI Pawn, they will fire you across the map or into the sky.
Now that's it for our MotionControllerPawn we now need to open up our BP_MotionController where we will modify our Grabsphere Collision.
Select the GrabSphere Component and change its collision Preset to Custom, we do this so we can modify our Collision Responses. Now we just need to set Player Collision to Ignore.
We also need to make a change to our HandMesh you might be wondering why since it is already set to NoCollision, If you have a look through the BP_MotionController Event graph you will find a section commented Only let hand collide with the environment while gripping.
Essentially when you press the grip button it applies collision to the hands. So we need to do the same thing as our grab sphere. Change the Collision preset to Custom and set PlayerCollision to Ignore.
Now if we test our project and grip our hands we should be able to move around while gripping. Like so.
Now we can do the exact same thing to our Interactable object. If we Open up our BP_PickupCube we can select our Static Mesh Component and change the collision Preset to custom so we set player Collision to Ignore.
And that's it, you can now run, jump, crouch grabs and snaps turn. I'd love to see how you use this system for your own projects. if you want to show it off head over to the GDXR Discord Server.
If this helped and you'd like to support tutorials like this why not head over to my Patreon where I have a variety of VR content available for download as well as one to one tutoring if you need help.
If you enjoyed this or found it useful please consider Subscribing to my Youtube channel where I post regular VR Tutorials for Unreal Engine 4 and Blender.
If you're looking for somewhere to hand out or need help with something VR, Game dev or 3D related maybe consider joining our growing discord community of over 300+ Vr developers.
Until next time, stay safe and I'll see you then.