Skip to content

Commit

Permalink
feat(CameraRig): add ability to move camera rig with touchpad
Browse files Browse the repository at this point in the history
The touchpad can now be used to move the play area in the direction
the headset is facing. It can also be used to strafe left and right
in the current facing direction.

The new `SteamVR_TouchpadWalking` script is added to the `[CameraRig]`
prefab and adds a box collider and a rigidbody to the camera rig to
handle collisions.

If the `SteamVR_HeadsetCollisionFade` script is also applied to the
`[CameraRig]->Camera` then on collision of the headset with a game
object, the user will have their position reset to their last good
known position. To achieve this a new `HeadsetCollisionDetect` event
has been added to the Headset Collision Fade script which the new
Touchpad Walking script listens for.

A big thanks to Reddit user `u/MisterDeum` for all the help with
testing this feature and coming up with ideas to make it a decent
feature to add to the toolkit.

A new example scene has been added to demonstrate this feature.
  • Loading branch information
thestonefox committed May 21, 2016
1 parent 70b4b75 commit 3582c87
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 3 deletions.
Binary file not shown.
8 changes: 8 additions & 0 deletions Assets/Examples/017_CameraRig_TouchpadWalking.unity.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
using UnityEngine;
using System.Collections;

public struct HeadsetCollisionEventArgs
{
public Collider collider;
public Transform currentTransform;
}

public delegate void HeadsetCollisionEventHandler(object sender, HeadsetCollisionEventArgs e);

public class SteamVR_HeadsetCollisionFade : MonoBehaviour {
public float blinkTransitionSpeed = 0.1f;
public Color fadeColor = Color.black;

void Start () {
public event HeadsetCollisionEventHandler HeadsetCollisionDetect;

public virtual void OnHeadsetCollisionDetect(HeadsetCollisionEventArgs e)
{
if (HeadsetCollisionDetect != null)
HeadsetCollisionDetect(this, e);
}

protected HeadsetCollisionEventArgs SetHeadsetCollisionEvent(Collider collider, Transform currentTransform)
{
HeadsetCollisionEventArgs e;
e.collider = collider;
e.currentTransform = currentTransform;
return e;
}

protected void Start () {
this.name = "PlayerObject_" + this.name;
BoxCollider collider = this.gameObject.AddComponent<BoxCollider>();
collider.isTrigger = true;
Expand All @@ -16,15 +40,16 @@ public class SteamVR_HeadsetCollisionFade : MonoBehaviour {
rb.useGravity = false;
}

void OnTriggerStay(Collider collider)
protected void OnTriggerStay(Collider collider)
{
if (!collider.name.Contains("PlayerObject_"))
{
OnHeadsetCollisionDetect(SetHeadsetCollisionEvent(collider, this.transform));
SteamVR_Fade.Start(fadeColor, blinkTransitionSpeed);
}
}

void OnTriggerExit(Collider collider)
protected void OnTriggerExit(Collider collider)
{
if (!collider.name.Contains("PlayerObject_"))
{
Expand Down
202 changes: 202 additions & 0 deletions Assets/SteamVR_Unity_Toolkit/Scripts/SteamVR_TouchpadWalking.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
using UnityEngine;
using System.Collections;

public class SteamVR_TouchpadWalking : MonoBehaviour {
public float maxWalkSpeed = 3f;
public float deceleration = 0.1f;
public float headsetYOffset = 0.2f;
public bool ignoreGrabbedCollisions = true;

private float movementSpeed = 0f;
private float strafeSpeed = 0f;
private int listenerInitTries = 5;

private Transform headset;
private Vector2 touchAxis;

private Rigidbody rb;
private BoxCollider bc;

private GameObject floorTouching;
private Vector3 lastGoodPosition;
private bool lastGoodPositionSet = false;
private float highestHeadsetY = 0f;
private float crouchMargin = 0.5f;
private float lastPlayAreaY = 0f;

private void Start () {
this.name = "PlayerObject_" + this.name;
lastGoodPositionSet = false;
headset = GetHeadset();
CreateCollider();
InitListeners();
}

private Transform GetHeadset()
{
#if (UNITY_5_4_OR_NEWER)
return GameObject.FindObjectOfType<SteamVR_Camera>().GetComponent<Transform>();
#endif
return GameObject.FindObjectOfType<SteamVR_GameView>().GetComponent<Transform>();
}

private void InitListeners()
{
SteamVR_ControllerEvents[] controllers = GameObject.FindObjectsOfType<SteamVR_ControllerEvents>();
if (controllers.Length == 0)
{
if (listenerInitTries > 0)
{
listenerInitTries--;
Invoke("InitListeners", 0.25f);
}
else
{
Debug.LogError("A GameObject must exist with a SteamVR_ControllerEvents script attached to it");
return;
}
}

foreach (SteamVR_ControllerEvents controller in controllers)
{
controller.TouchpadAxisChanged += new ControllerClickedEventHandler(DoTouchpadAxisChanged);
controller.TouchpadUntouched += new ControllerClickedEventHandler(DoTouchpadUntouched);

if (ignoreGrabbedCollisions && controller.GetComponent<SteamVR_InteractGrab>())
{
SteamVR_InteractGrab grabbingController = controller.GetComponent<SteamVR_InteractGrab>();
grabbingController.ControllerGrabInteractableObject += new ObjectInteractEventHandler(OnGrabObject);
grabbingController.ControllerUngrabInteractableObject += new ObjectInteractEventHandler(OnUngrabObject);
}
}

if (headset.GetComponent<SteamVR_HeadsetCollisionFade>())
{
headset.GetComponent<SteamVR_HeadsetCollisionFade>().HeadsetCollisionDetect += new HeadsetCollisionEventHandler(OnHeadsetCollision);
}
}

private void OnGrabObject(object sender, ObjectInteractEventArgs e)
{
Physics.IgnoreCollision(this.GetComponent<Collider>(), e.target.GetComponent<Collider>(), true);
}

private void OnUngrabObject(object sender, ObjectInteractEventArgs e)
{
Physics.IgnoreCollision(this.GetComponent<Collider>(), e.target.GetComponent<Collider>(), false);
}

private void OnHeadsetCollision(object sender, HeadsetCollisionEventArgs e)
{
if (lastGoodPositionSet) {
SteamVR_Fade.Start(Color.black, 0f);
this.transform.position = lastGoodPosition;
}
}

private void CreateCollider()
{
rb = this.gameObject.AddComponent<Rigidbody>();
rb.mass = 100;
rb.freezeRotation = true;

bc = this.gameObject.AddComponent<BoxCollider>();
bc.center = new Vector3(0f, 1f, 0f);
bc.size = new Vector3(0.25f, 1f, 0.25f);

this.gameObject.layer = 2;
}

private void DoTouchpadAxisChanged(object sender, ControllerClickedEventArgs e)
{
touchAxis = e.touchpadAxis;
}

private void DoTouchpadUntouched(object sender, ControllerClickedEventArgs e)
{
touchAxis = Vector2.zero;
}

private void CalculateSpeed(ref float speed, float inputValue)
{
if (inputValue != 0f)
{
speed = (maxWalkSpeed * inputValue);
}
else
{
Decelerate(ref speed);
}
}

private void Decelerate(ref float speed)
{
if (speed > 0)
{
speed -= Mathf.Lerp(deceleration, maxWalkSpeed, 0f);
}
else if (speed < 0)
{
speed += Mathf.Lerp(deceleration, -maxWalkSpeed, 0f);
}
else
{
speed = 0;
}
}

private void Move()
{
Vector3 movement = headset.transform.forward * movementSpeed * Time.deltaTime;
Vector3 strafe = headset.transform.right * strafeSpeed * Time.deltaTime;
float fixY = this.transform.position.y;
this.transform.position += (movement + strafe);
this.transform.position = new Vector3(this.transform.position.x, fixY, this.transform.position.z);
}

private void UpdateCollider()
{
float playAreaHeightAdjustment = 0.01f;
float newBCYSize = (headset.transform.position.y - headsetYOffset) - this.transform.position.y;
float newBCYCenter = (newBCYSize != 0 ? (newBCYSize / 2) + playAreaHeightAdjustment: 0);

bc.size = new Vector3(bc.size.x, newBCYSize, bc.size.z);
bc.center = new Vector3(headset.localPosition.x, newBCYCenter, headset.localPosition.z);
}

private void SetHeadsetY()
{
//if the play area height has changed then always recalc headset height
float floorVariant = 0.005f;
if (this.transform.position.y > lastPlayAreaY + floorVariant || this.transform.position.y < lastPlayAreaY - floorVariant)
{
highestHeadsetY = 0f;
}

if (headset.transform.position.y > highestHeadsetY)
{
highestHeadsetY = headset.transform.position.y;
}

if (headset.transform.position.y > highestHeadsetY - crouchMargin)
{
lastGoodPositionSet = true;
lastGoodPosition = this.transform.position;
}

lastPlayAreaY = this.transform.position.y;
}

private void Update()
{
SetHeadsetY();
UpdateCollider();
}

private void FixedUpdate()
{
CalculateSpeed(ref movementSpeed, touchAxis.y);
CalculateSpeed(ref strafeSpeed, touchAxis.x);
Move();
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -507,12 +507,69 @@ The following script parameters are available:
* **Blink Transition Speed:** The fade blink speed on collision.
* **Fade Color:** The colour to fade the headset to on collision.

The following events are emitted:

* **HeadsetCollisionDetect:** Emitted when the user's headset
collides with another game object.

The event payload that is emitted contains:

* **collider:** The Collider of the game object the headset has
collided with.
* **currentTransform:** The current Transform of the object that
the Headset Collision Fade script is attatched to (Camera).

An example of the `SteamVR_HeadsetCollisionFade` script can be
viewed in the scene `Examples/011_Camera_HeadSetCollisionFading`.
The scene has collidable walls around the play area and if the player
puts their head into any of the walls then the headset will fade to
black.

#### Touchpad Movement (SteamVR_TouchpadWalking)

The ability to move the play area around the game world by sliding a
finger over the touchpad is achieved using this script. The
Touchpad Walking script is applied to the `[CameraRig]` prefab and
adds a rigidbody and a box collider to the user's position to
prevent them from walking through other collidable game objects.

If the Headset Collision Fade script has been applied to the Camera
prefab, then if a user attempts to collide with an object then their
position is reset to the last good known position. This can happen
if the user is moving through a section where they need to crouch
and then they stand up and collide with the ceiling. Rather than
allow a user to do this and cause collision resolution issues it is
better to just move them back to a valid location. This does break
immersion but the user is doing something that isn't natural.

The following script parameters are available:

* **Max Walk Speed:** The maximum speed the play area will be moved
when the touchpad is being touched at the extremes of the axis. If
a lower part of the touchpad axis is touched (nearer the centre)
then the walk speed is slower.
* **Deceleration:** The speed in which the play area slows down to
a complete stop when the user is no longer touching the touchpad.
This deceleration effect can ease any motion sickness that may be
suffered.
* **Headset Y Offset:** The box collider which is created for the
user is set at a height from the user's headset position. If the
collider is required to be lower to allow for room between the
play area collider and the headset then this offset value will
shorten the height of the generated box collider.
* **Ignore Grabbed Collisions:** If this is checked then any items
that are grabbed with the controller will not collide with the
box collider and rigid body on the play area. This is very useful
if the user is required to grab and wield objects because if the
collider was active they would bounce off the play area collider.

An example of the `SteamVR_TouchpadWalking` script can be viewed in
the scene `Examples/017_CameraRig_TouchpadWalking`. The scene has
a collection of walls and slopes that can be traversed by the user
with the touchpad. There is also an area that can only be traversed
if the user is crouching. Standing up in this crouched area will
cause the user to appear back at their last good known position.

#### Interactable Object (SteamVR_InteractableObject)

The Interactable Object script is attached to any game object that is
Expand Down Expand Up @@ -979,6 +1036,18 @@ The current examples are:
apart if it is hit hard enough by the sword.
* [View Example Tour on Youtube](https://www.youtube.com/watch?v=ErSxZlZh6fc)

* **017_CameraRig_TouchpadWalking:** A scene which demonstrates how
to move around the game world using the touchpad by sliding a finger
forward and backwards to move in that direction. Sliding a finger
left and right across the touchpad strafes in that direction. The
rotation is done via the player in game physically rotating their
body in the place space and whichever way the headset is looking
will be the way the player walks forward. Crouching is also possible
as demonstrated in this scene and in conjunction with the
Headset Collision Fade script it can detect unwanted collisions
(e.g. if the player stands up whilst walking as crouched) and reset
their position to the last good known position.

* **018_CameraRig_FramesPerSecondCounter:** A scene which displays
the frames per second in the centre of the headset view. Pressing
the trigger generates a new sphere and pressing the touchpad
Expand Down

0 comments on commit 3582c87

Please sign in to comment.