// Step Multiplier|Locomotion|20130 namespace VRTK { using UnityEngine; /// /// Multiplies each real world step within the play area to enable further distances to be travelled in the virtual world. /// /// /// **Optional Components:** /// * `VRTK_ControllerEvents` - The events component to listen for the button presses on. This must be applied on the same GameObject as this script if one is not provided via the `Controller Events` parameter. /// /// **Script Usage:** /// * Place the `VRTK_StepMultiplier` script on either: /// * Any GameObject in the scene if no activation button is required. /// * The GameObject with the Controller Events scripts if an activation button is required. /// * Any other scene GameObject and provide a valid `VRTK_ControllerEvents` component to the `Controller Events` parameter of this script if an activation button is required. /// /// /// `VRTK/Examples/028_CameraRig_RoomExtender` shows how the Step Multiplier can be used to move around the scene with multiplied steps. /// [AddComponentMenu("VRTK/Scripts/Locomotion/VRTK_StepMultiplier")] public class VRTK_StepMultiplier : MonoBehaviour { /// /// Movement methods. /// public enum MovementFunction { /// /// Moves the head with a non-linear drift movement. /// Nonlinear, /// /// Moves the headset in a direct linear movement. /// LinearDirect } [Header("Step Multiplier Settings")] [Tooltip("The controller button to activate the step multiplier effect. If it is `Undefined` then the step multiplier will always be active.")] public VRTK_ControllerEvents.ButtonAlias activationButton = VRTK_ControllerEvents.ButtonAlias.Undefined; [Tooltip("This determines the type of movement used by the extender.")] public MovementFunction movementFunction = MovementFunction.LinearDirect; [Tooltip("This is the factor by which movement at the edge of the circle is amplified. `0` is no movement of the play area. Higher values simulate a bigger play area but may be too uncomfortable.")] [Range(0, 10)] public float additionalMovementMultiplier = 1.0f; [Tooltip("This is the size of the circle in which the play area is not moved and everything is normal. If it is to low it becomes uncomfortable when crouching.")] [Range(0, 5)] public float headZoneRadius = 0.25f; [Header("Custom Settings")] [Tooltip("The Controller Events to listen for the events on. If the script is being applied onto a controller then this parameter can be left blank as it will be auto populated by the controller the script is on at runtime.")] public VRTK_ControllerEvents controllerEvents; protected Vector3 relativeMovementOfCameraRig = new Vector3(); protected Transform movementTransform; protected Transform playArea; protected Vector3 headCirclePosition; protected Vector3 lastPosition; protected Vector3 lastMovement; protected bool activationEnabled; protected VRTK_ControllerEvents.ButtonAlias subscribedActivationButton = VRTK_ControllerEvents.ButtonAlias.Undefined; protected bool buttonSubscribed; protected virtual void Awake() { VRTK_SDKManager.AttemptAddBehaviourToToggleOnLoadedSetupChange(this); } protected virtual void OnEnable() { activationEnabled = false; buttonSubscribed = false; movementTransform = VRTK_DeviceFinder.HeadsetTransform(); playArea = VRTK_DeviceFinder.PlayAreaTransform(); MoveHeadCircleNonLinearDrift(); if (movementTransform != null) { lastPosition = movementTransform.localPosition; } } protected virtual void OnDestroy() { VRTK_SDKManager.AttemptRemoveBehaviourToToggleOnLoadedSetupChange(this); } protected virtual void Update() { ManageButtonSubscription(); switch (movementFunction) { case MovementFunction.Nonlinear: MoveHeadCircleNonLinearDrift(); break; case MovementFunction.LinearDirect: MoveHeadCircle(); break; default: break; } } protected virtual void ManageButtonSubscription() { controllerEvents = (controllerEvents != null ? controllerEvents : GetComponentInParent()); if (controllerEvents != null && buttonSubscribed && subscribedActivationButton != VRTK_ControllerEvents.ButtonAlias.Undefined && activationButton != subscribedActivationButton) { buttonSubscribed = false; controllerEvents.UnsubscribeToButtonAliasEvent(subscribedActivationButton, true, ActivationButtonPressed); controllerEvents.UnsubscribeToButtonAliasEvent(subscribedActivationButton, false, ActivationButtonReleased); subscribedActivationButton = VRTK_ControllerEvents.ButtonAlias.Undefined; } if (controllerEvents != null && !buttonSubscribed && activationButton != VRTK_ControllerEvents.ButtonAlias.Undefined) { controllerEvents.SubscribeToButtonAliasEvent(activationButton, true, ActivationButtonPressed); controllerEvents.SubscribeToButtonAliasEvent(activationButton, false, ActivationButtonReleased); buttonSubscribed = true; subscribedActivationButton = activationButton; } } protected virtual void ActivationButtonPressed(object sender, ControllerInteractionEventArgs e) { activationEnabled = true; } protected virtual void ActivationButtonReleased(object sender, ControllerInteractionEventArgs e) { activationEnabled = false; } protected virtual void Move(Vector3 movement) { headCirclePosition += movement; if (playArea != null && (activationEnabled || activationButton == VRTK_ControllerEvents.ButtonAlias.Undefined)) { playArea.localPosition += movement * additionalMovementMultiplier; relativeMovementOfCameraRig += movement * additionalMovementMultiplier; } } protected virtual void MoveHeadCircle() { if (movementTransform != null) { //Get the movement of the head relative to the headCircle. Vector3 circleCenterToHead = new Vector3(movementTransform.localPosition.x - headCirclePosition.x, 0, movementTransform.localPosition.z - headCirclePosition.z); //Get the direction of the head movement. UpdateLastMovement(); //Checks if the head is outside of the head cirlce and moves the head circle and play area in the movementDirection. if (circleCenterToHead.sqrMagnitude > headZoneRadius * headZoneRadius && lastMovement != Vector3.zero) { //Just move like the headset moved Move(lastMovement); } } } protected virtual void MoveHeadCircleNonLinearDrift() { if (movementTransform != null) { Vector3 movement = new Vector3(movementTransform.localPosition.x - headCirclePosition.x, 0, movementTransform.localPosition.z - headCirclePosition.z); if (movement.sqrMagnitude > headZoneRadius * headZoneRadius) { Vector3 deltaMovement = movement.normalized * (movement.magnitude - headZoneRadius); Move(deltaMovement); } } } protected virtual void UpdateLastMovement() { if (movementTransform != null) { //Save the last movement lastMovement = movementTransform.localPosition - lastPosition; lastMovement.y = 0; lastPosition = movementTransform.localPosition; } } } }