To use the custom VR binding I made a function that keeps track of what buttons are being pressed. I want to keep track of both triggers seperatly, so I can later use that to determine which leg is moving. I also want to keep track of when the player releases the triggers, so I know that I need to switch to the other leg at that moment.
private void ButtonState()
{
if (SteamVR_Input.GetStateDown("walkhold", SteamVR_Input_Sources.LeftHand))
{
walkHoldLeft = true;
}
if (SteamVR_Input.GetStateUp("walkhold", SteamVR_Input_Sources.LeftHand))
{
walkHoldLeft = false;
resetTriggerReleaseLeft = true;
if (useLeftLeg)
{
switchLegs = true;
}
}
if (SteamVR_Input.GetStateDown("walkhold", SteamVR_Input_Sources.RightHand))
{
walkHoldRight = true;
}
if (SteamVR_Input.GetStateUp("walkhold", SteamVR_Input_Sources.RightHand))
{
walkHoldRight = false;
resetTriggerReleaseRight = true;
if (!useLeftLeg)
{
switchLegs = true;
}
}
}
Move legs
Here's the function where I move the legs. First I take the direction that the player is looking at, so we can use that later to move relative to that direction. Then we check if the player can use its left leg and if it's holding the left trigger, if that's the case then we set some variables if this is the first time that it happens. After that we move the player towards its new position. If the player can't move left we check all the things for the right leg instead.
private void MoveLegs()
{
Vector3 camPosition = new Vector3(vrCamera.forward.x, 0f, vrCamera.forward.z);
if (walkHoldLeft && useLeftLeg) // If the user holds the left trigger and the left leg is supposed to move.
{
if (!isMovingLeg) // Only do this when the user is starting with moving their leg.
{
resetTriggerReleaseLeft = false;
startPositionLeg = leftLegPivot.position;
startPositionPlayer = transform.position;
isMovingLeg = true;
}
transform.position = startPositionPlayer + camPosition * GetWalkSpeed(leftLegPivot);
}
else if (walkHoldRight && !useLeftLeg)
{
if (!isMovingLeg) // Only do this when the user is starting with moving their leg.
{
resetTriggerReleaseRight = false;
startPositionLeg = rightLegPivot.position;
startPositionPlayer = transform.position;
isMovingLeg = true;
}
transform.position = startPositionPlayer + camPosition * GetWalkSpeed(rightLegPivot);
}
ChangeCurrentLeg();
}
Rotate legs
If we don't rotate the legs relative to the player, we'll get some very strange results when you move your head around. With the following code we prevent this.
private void RotateLegs()
{
// Rotate the legs around the player relative to the direction that the player's looking to.
Vector3 camRot = new Vector3(legs.eulerAngles.x, vrCamera.eulerAngles.y, legs.eulerAngles.z);
legs.eulerAngles = camRot;
// Set the rotation of the legs that are children of the player equal to that of the leg pivot objects.
leftLeg.rotation = new Quaternion(leftLegPivot.rotation.x, leftLegPivot.rotation.y, leftLegPivot.rotation.z, leftLegPivot.rotation.w);
rightLeg.rotation = new Quaternion(rightLegPivot.rotation.x, rightLegPivot.rotation.y, rightLegPivot.rotation.z, rightLegPivot.rotation.w);
}
Transform legs
We use separate leg pivot objects to prevent the parent from transforming the leg values. Here we set the pivot object local position and transform equal to the left and right controller positions.
public class LegMovement : MonoBehaviour
{
public SteamVR_Action_Pose poseAction = SteamVR_Input.GetAction<SteamVR_Action_Pose>("Pose");
public SteamVR_Input_Sources inputSourceLeftController = SteamVR_Input_Sources.LeftHand;
public SteamVR_Input_Sources inputSourceRightController = SteamVR_Input_Sources.RightHand;
// Walking Action Inputs
private bool walkHoldLeft, walkHoldRight;
private bool resetTriggerReleaseLeft = true;
private bool resetTriggerReleaseRight = true;
[SerializeField]
private Transform vrCamera, leftLegPivot, rightLegPivot, legs, leftLeg, rightLeg;
[SerializeField]
private float walkSpeedModifier = 3f;
private bool useLeftLeg;
private bool isMovingLeg;
private bool switchLegs;
private Vector3 startPositionPlayer, startPositionLeg;
protected virtual void Start()
{
if (poseAction == null)
{
Debug.LogError("<b>[SteamVR]</b> No pose action set for this component", this);
return;
}
Input.GetJoystickNames();
}
private void Update()
{
UpdateTransform();
ButtonState();
MoveLegs();
RotateLegs();
}
private void RotateLegs()
{
// Rotate the legs around the player relative to the direction that the player's looking to.
Vector3 camRot = new Vector3(legs.eulerAngles.x, vrCamera.eulerAngles.y, legs.eulerAngles.z);
legs.eulerAngles = camRot;
// Set the rotation of the legs that are children of the player equal to that of the leg pivot objects.
leftLeg.rotation = new Quaternion(leftLegPivot.rotation.x, leftLegPivot.rotation.y, leftLegPivot.rotation.z, leftLegPivot.rotation.w);
rightLeg.rotation = new Quaternion(rightLegPivot.rotation.x, rightLegPivot.rotation.y, rightLegPivot.rotation.z, rightLegPivot.rotation.w);
}
private void MoveLegs()
{
Vector3 camPosition = new Vector3(vrCamera.forward.x, 0f, vrCamera.forward.z);
if (walkHoldLeft && useLeftLeg) // If the user holds the left trigger and the left leg is supposed to move.
{
if (!isMovingLeg) // Only do this when the user is starting with moving their leg.
{
resetTriggerReleaseLeft = false;
startPositionLeg = leftLegPivot.position;
startPositionPlayer = transform.position;
isMovingLeg = true;
}
transform.position = startPositionPlayer + camPosition * GetWalkSpeed(leftLegPivot);
}
else if (walkHoldRight && !useLeftLeg)
{
if (!isMovingLeg) // Only do this when the user is starting with moving their leg.
{
resetTriggerReleaseRight = false;
startPositionLeg = rightLegPivot.position;
startPositionPlayer = transform.position;
isMovingLeg = true;
}
transform.position = startPositionPlayer + camPosition * GetWalkSpeed(rightLegPivot);
}
ChangeCurrentLeg();
}
private void ChangeCurrentLeg()
{
if (resetTriggerReleaseLeft && useLeftLeg)
{
isMovingLeg = false;
if (switchLegs)
{
useLeftLeg = false;
switchLegs = false;
}
}
else if (resetTriggerReleaseRight && !useLeftLeg)
{
isMovingLeg = false;
if (switchLegs)
{
useLeftLeg = true;
switchLegs = false;
}
}
}
private float GetWalkSpeed(Transform leg)
{
float legMoveDistance = startPositionLeg.y - leg.position.y;
float walkSpeed = legMoveDistance * walkSpeedModifier;
return walkSpeed;
}
private void ButtonState()
{
if (SteamVR_Input.GetStateDown("walkhold", SteamVR_Input_Sources.LeftHand))
{
walkHoldLeft = true;
}
if (SteamVR_Input.GetStateUp("walkhold", SteamVR_Input_Sources.LeftHand))
{
walkHoldLeft = false;
resetTriggerReleaseLeft = true;
if (useLeftLeg)
{
switchLegs = true;
}
}
if (SteamVR_Input.GetStateDown("walkhold", SteamVR_Input_Sources.RightHand))
{
walkHoldRight = true;
}
if (SteamVR_Input.GetStateUp("walkhold", SteamVR_Input_Sources.RightHand))
{
walkHoldRight = false;
resetTriggerReleaseRight = true;
if (!useLeftLeg)
{
switchLegs = true;
}
}
}
protected virtual void UpdateTransform()
{
leftLegPivot.localPosition = poseAction[inputSourceLeftController].localPosition;
leftLegPivot.localRotation = poseAction[inputSourceLeftController].localRotation;
rightLegPivot.localPosition = poseAction[inputSourceRightController].localPosition;
rightLegPivot.localRotation = poseAction[inputSourceRightController].localRotation;
}
}
Converting to OpenXR
We decided to switch the project from SteamVR to OpenXR, because of that I had to convert the leg movement code to still be able to use controller inputs.
It was fairly easy to adapt to OpenXR. First we make a script that extends the ActionBasedController script, to allow us to use custom input options without changing the ActionBasedController script.
// Some codepublic class CustomActionBasedController : ActionBasedController
{
[SerializeField]
InputActionProperty m_WalkHoldLeftAction;
public InputActionProperty walkHoldLeftAction
{
get => m_WalkHoldLeftAction;
set => SetInputActionProperty(ref m_WalkHoldLeftAction, value);
}
[SerializeField]
InputActionProperty m_WalkHoldRightAction;
public InputActionProperty walkHoldRightAction
{
get => m_WalkHoldRightAction;
set => SetInputActionProperty(ref m_WalkHoldRightAction, value);
}
//Copied directly from action based controller because changing the function to protected in the base class is not tracked by git.
private void SetInputActionProperty(ref InputActionProperty property, InputActionProperty value)
{
if (Application.isPlaying)
property.DisableDirectAction();
property = value;
if (Application.isPlaying && isActiveAndEnabled)
property.EnableDirectAction();
}
}
After we've done that we make custom actions in XRI Default Input Actions, we bind them to the left and right trigger.
The next step is to add the CustomActionBasedController script to both controller objects. And afterward set the Use reference of the respective direction on true and add the Input we've made in the previous step.
Now that the setup is complete we're ready to add the inputs in the walking script.
In the start we want to reference each function that will happen on a button press or release.
The other parts of the scripts are generally the same, except that we set the walkHold boolean and the reset trigger booleans in the following functions:
Adding networking to the legs was quite easy with the OpenXR and Photon system that was mainly set up by my teammates. I just had to give the lefthand and righthand position to an already existing function, and that would allow the network to make copies that would only be visible by other players and position those copies correctly.
Sprint 4 iterations
Final iteration
I use the IsButtonPressed function to register a button press, in this new version of the script it doesn't matter if the left or right trigger is pressed, we decide the starting leg in a later portion of the script depending on the starting height of both controllers.
public bool IsButtonPressed
{
get
{
return leftController.walkHoldLeftAction.action.ReadValue<float>() == 1 || rightController.walkHoldRightAction.action.ReadValue<float>() == 1;
}
}
MoveLegs is the main function that enables the user to walk. The user can walk by taking step after step. In this function I keep track of if there's a step and when it ends. Based on that we move the player.
private void MoveLegs()
{
if (IsButtonPressed)
{
if (!isMovingLeg) // Starts a new step.
{
if (!rightStep && !leftStep) // If there is no current moving leg.
DetermineStartingLeg(); // Decide with which leg to start walking depending on the height.
if (rightStep)
currentLegPivot = rightLegPivot;
else if (leftStep)
currentLegPivot = leftLegPivot;
maxStepHeight = float.MaxValue; // MaxStepHeight will be used to determine if the user needs to move forward or not.
StartLegMovement(currentLegPivot.position);
}
else
{
legHeight = currentLegPivot.position.y;
currentSpeed = GetWalkSpeed(currentLegPivot);
transform.position = stepStartPosition + camPosition * currentSpeed; // Update the position of the player.
if (legHeight > maxStepHeight) // If the leg moves against the direction it was going it ends the current "step"
ResetWalkValues();
}
}
}
The StartLegMovement function is mainly used for reseting values between a step. It also stops the rotateToNeutral coroutine.
Earlier we used the GetWalkSpeed function in the MoveLegs function, here's the code of the function. We calculate a speed based on the controller height but don't assign a negative speed when the controller moves backward. We do this by accidentally moving slightly backward at the end of a step.
As mentioned on the , I'm working on a way to walk for the players. The first step to figuring out how to walk is getting input from the trigger buttons. To do that I had to make a custom SteamVR binding.
During sprint 4 I made a bunch of iterations on the walking mechanic, you can read more about the design aspects in .