Skip to content

Commit d774c5a

Browse files
committed
feat(Presence): add step height support to body physics
The Body Physics script now supports a step offset height to determine if a collided object is small enough to be climbed upon even if the object is too high for the capsule collider to naturally resolve the collision to the new height. This is achieved with a sub capsule collider that sits below the main collider and acts as a foot collider to detect when any objects are collided with and then a box is cast down to see if the main body collider should be standing on a new surface. If the box cast is valid then the play area is either teleported to the new position if a teleporter is available or the play area is just snapped to the new position if no teleporter is available.
1 parent 7e76415 commit d774c5a

2 files changed

Lines changed: 115 additions & 28 deletions

File tree

Assets/VRTK/Scripts/Presence/VRTK_BodyPhysics.cs

Lines changed: 112 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ public enum FallingRestrictors
7171
[Tooltip("The `y` distance between the headset and the object being leaned over, if object being leaned over is taller than this threshold then the current standing position won't be updated.")]
7272
public float leanYThreshold = 0.5f;
7373

74+
[Header("Step Settings")]
75+
76+
[Tooltip("The maximum height to consider when checking if an object can be stepped upon to.")]
77+
public float stepUpYOffset = 0.15f;
78+
[Tooltip("The width/depth of the foot collider in relation to the radius of the body collider.")]
79+
[Range(0.1f, 0.9f)]
80+
public float stepThicknessMultiplier = 0.5f;
81+
[Tooltip("The distance between the current play area Y position and the new stepped up Y position to consider a valid step up. A higher number can help with juddering on slopes or small increases in collider heights.")]
82+
public float stepDropThreshold = 0.08f;
83+
7484
[Header("Snap To Floor Settings")]
7585

7686
[Tooltip("A custom raycaster to use when raycasting to find floors.")]
@@ -118,7 +128,9 @@ public enum FallingRestrictors
118128
protected Transform playArea;
119129
protected Transform headset;
120130
protected Rigidbody bodyRigidbody;
131+
protected GameObject bodyColliderContainer;
121132
protected CapsuleCollider bodyCollider;
133+
protected CapsuleCollider footCollider;
122134
protected VRTK_CollisionTracker collisionTracker;
123135
protected bool currentBodyCollisionsSetting;
124136
protected GameObject currentCollidingObject = null;
@@ -145,6 +157,8 @@ public enum FallingRestrictors
145157
protected bool generateCollider = false;
146158
protected bool generateRigidbody = false;
147159
protected Vector3 playAreaVelocity = Vector3.zero;
160+
protected const string BODY_COLLIDER_CONTAINER_NAME = "VRTK_BodyColliderContainer";
161+
protected const string FOOT_COLLIDER_CONTAINER_NAME = "VRTK_FootColliderContainer";
148162

149163
// Draws a sphere for current standing position and a sphere for current headset position.
150164
// Set to `true` to view the debug spheres.
@@ -294,16 +308,17 @@ protected virtual void FixedUpdate()
294308

295309
protected virtual void OnCollisionEnter(Collision collision)
296310
{
297-
if (!VRTK_PlayerObject.IsPlayerObject(collision.gameObject) && currentValidFloorObject && !currentValidFloorObject.Equals(collision.gameObject))
311+
if (!VRTK_PlayerObject.IsPlayerObject(collision.gameObject) && currentValidFloorObject != null && !currentValidFloorObject.Equals(collision.gameObject))
298312
{
313+
CheckStepUpCollision(collision);
299314
currentCollidingObject = collision.gameObject;
300315
OnStartColliding(SetBodyPhysicsEvent(currentCollidingObject));
301316
}
302317
}
303318

304319
protected virtual void OnTriggerEnter(Collider collider)
305320
{
306-
if (!VRTK_PlayerObject.IsPlayerObject(collider.gameObject) && currentValidFloorObject && !currentValidFloorObject.Equals(collider.gameObject))
321+
if (!VRTK_PlayerObject.IsPlayerObject(collider.gameObject) && currentValidFloorObject != null && !currentValidFloorObject.Equals(collider.gameObject))
307322
{
308323
currentCollidingObject = collider.gameObject;
309324
OnStartColliding(SetBodyPhysicsEvent(currentCollidingObject));
@@ -443,14 +458,18 @@ protected virtual void CalculateVelocity()
443458

444459
protected virtual void TogglePhysics(bool state)
445460
{
446-
if (bodyRigidbody)
461+
if (bodyRigidbody != null)
447462
{
448463
bodyRigidbody.isKinematic = !state;
449464
}
450-
if (bodyCollider)
465+
if (bodyCollider != null)
451466
{
452467
bodyCollider.isTrigger = !state;
453468
}
469+
if (footCollider != null)
470+
{
471+
footCollider.isTrigger = !state;
472+
}
454473

455474
currentBodyCollisionsSetting = state;
456475
}
@@ -646,6 +665,50 @@ protected virtual void DisableBodyPhysics()
646665
InitControllerListeners(VRTK_DeviceFinder.GetControllerRightHand(), false);
647666
}
648667

668+
protected virtual void CheckStepUpCollision(Collision collision)
669+
{
670+
if (footCollider != null && collision.contacts.Length > 0 && collision.contacts[0].thisCollider.transform.name == FOOT_COLLIDER_CONTAINER_NAME)
671+
{
672+
float stepYIncrement = 0.55f;
673+
float boxCastHeight = 0.01f;
674+
675+
Vector3 colliderWorldCenter = playArea.TransformPoint(footCollider.center);
676+
Vector3 castStart = new Vector3(colliderWorldCenter.x, colliderWorldCenter.y + (CalculateStepUpYOffset() * stepYIncrement), colliderWorldCenter.z);
677+
Vector3 castExtents = new Vector3(bodyCollider.radius, boxCastHeight, bodyCollider.radius);
678+
RaycastHit floorCheckHit;
679+
float castDistance = castStart.y - playArea.position.y;
680+
if (Physics.BoxCast(castStart, castExtents, Vector3.down, out floorCheckHit, Quaternion.identity, castDistance) && (floorCheckHit.point.y - playArea.position.y) > stepDropThreshold)
681+
{
682+
//If there is a teleporter attached then use that to move
683+
if (teleporter != null && enableTeleport)
684+
{
685+
hitFloorYDelta = playArea.position.y - floorCheckHit.point.y;
686+
TeleportFall(floorCheckHit.point.y, floorCheckHit);
687+
lastFrameFloorY = floorCheckHit.point.y;
688+
}
689+
//If there isn't a teleporter then just force the position
690+
else
691+
{
692+
playArea.position = new Vector3((floorCheckHit.point.x - (headset.position.x - playArea.position.x)), floorCheckHit.point.y, (floorCheckHit.point.z - (headset.position.z - playArea.position.z)));
693+
}
694+
}
695+
}
696+
}
697+
698+
protected virtual GameObject CreateColliderContainer(string name, Transform parent)
699+
{
700+
GameObject generatedContainer = new GameObject(name);
701+
generatedContainer.transform.SetParent(parent);
702+
generatedContainer.transform.localPosition = Vector3.zero;
703+
generatedContainer.transform.localRotation = Quaternion.identity;
704+
generatedContainer.transform.localScale = Vector3.one;
705+
706+
generatedContainer.layer = LayerMask.NameToLayer("Ignore Raycast");
707+
VRTK_PlayerObject.SetPlayerObject(generatedContainer, VRTK_PlayerObject.ObjectTypes.Collider);
708+
709+
return generatedContainer;
710+
}
711+
649712
protected virtual void CreateCollider()
650713
{
651714
generateCollider = false;
@@ -667,14 +730,22 @@ protected virtual void CreateCollider()
667730
bodyRigidbody.freezeRotation = true;
668731
}
669732

670-
bodyCollider = playArea.GetComponent<CapsuleCollider>();
671-
if (bodyCollider == null)
733+
if (bodyColliderContainer == null)
672734
{
673735
generateCollider = true;
674-
bodyCollider = playArea.gameObject.AddComponent<CapsuleCollider>();
675-
bodyCollider.center = new Vector3(0f, 1f, 0f);
676-
bodyCollider.height = 1f;
736+
bodyColliderContainer = CreateColliderContainer(BODY_COLLIDER_CONTAINER_NAME, playArea);
737+
738+
bodyCollider = bodyColliderContainer.AddComponent<CapsuleCollider>();
677739
bodyCollider.radius = 0.15f;
740+
741+
if (CalculateStepUpYOffset() > 0f)
742+
{
743+
GameObject footColliderContainer = CreateColliderContainer(FOOT_COLLIDER_CONTAINER_NAME, bodyColliderContainer.transform);
744+
footCollider = footColliderContainer.AddComponent<CapsuleCollider>();
745+
}
746+
747+
bodyColliderContainer.gameObject.layer = LayerMask.NameToLayer("Ignore Raycast");
748+
VRTK_PlayerObject.SetPlayerObject(bodyColliderContainer, VRTK_PlayerObject.ObjectTypes.Collider);
678749
}
679750

680751
if (playArea.gameObject.layer == 0)
@@ -693,25 +764,35 @@ protected virtual void DestroyCollider()
693764

694765
if (generateCollider)
695766
{
696-
Destroy(bodyCollider);
767+
Destroy(bodyColliderContainer);
697768
}
698769
}
699770

700771
protected virtual void UpdateCollider()
701772
{
702-
if (bodyCollider)
773+
if (bodyColliderContainer != null && headset != null)
703774
{
704-
float newpresenceColliderYSize = (headset ? headset.transform.localPosition.y - headsetYOffset : 0f);
705-
float newpresenceColliderYCenter = Mathf.Max((newpresenceColliderYSize / 2) + playAreaHeightAdjustment, bodyCollider.radius + playAreaHeightAdjustment);
775+
float newpresenceColliderYSize = (headset ? headset.transform.localPosition.y - (headsetYOffset + CalculateStepUpYOffset()) : 0f);
776+
float newpresenceColliderYCenter = Mathf.Max((newpresenceColliderYSize * 0.5f) + CalculateStepUpYOffset() + playAreaHeightAdjustment, bodyCollider.radius + playAreaHeightAdjustment);
706777

707-
if (headset && bodyCollider)
778+
bodyCollider.height = Mathf.Max(newpresenceColliderYSize, bodyCollider.radius);
779+
bodyCollider.center = new Vector3(headset.localPosition.x, newpresenceColliderYCenter, headset.localPosition.z);
780+
781+
if (footCollider != null)
708782
{
709-
bodyCollider.height = Mathf.Max(newpresenceColliderYSize, bodyCollider.radius);
710-
bodyCollider.center = new Vector3(headset.localPosition.x, newpresenceColliderYCenter, headset.localPosition.z);
783+
float footThickness = bodyCollider.radius * stepThicknessMultiplier;
784+
footCollider.radius = footThickness;
785+
footCollider.height = CalculateStepUpYOffset();
786+
footCollider.center = new Vector3(headset.localPosition.x, CalculateStepUpYOffset() * 0.5f, headset.localPosition.z);
711787
}
712788
}
713789
}
714790

791+
protected virtual float CalculateStepUpYOffset()
792+
{
793+
return stepUpYOffset * 2f;
794+
}
795+
715796
protected virtual void InitControllerListeners(GameObject mappedController, bool state)
716797
{
717798
if (mappedController)
@@ -750,16 +831,21 @@ protected virtual IEnumerator RestoreCollisions(GameObject obj)
750831

751832
protected virtual void IgnoreCollisions(Collider[] colliders, bool state)
752833
{
753-
if (playArea)
834+
if (bodyColliderContainer != null)
754835
{
755-
Collider collider = playArea.GetComponent<Collider>();
756-
if (collider.gameObject.activeInHierarchy)
836+
Collider[] playareaColliders = bodyColliderContainer.GetComponentsInChildren<Collider>();
837+
for (int i = 0; i < playareaColliders.Length; i++)
757838
{
758-
foreach (Collider controllerCollider in colliders)
839+
Collider collider = playareaColliders[i];
840+
if (collider.gameObject.activeInHierarchy)
759841
{
760-
if (controllerCollider.gameObject.activeInHierarchy)
842+
for (int j = 0; j < colliders.Length; j++)
761843
{
762-
Physics.IgnoreCollision(collider, controllerCollider, state);
844+
Collider controllerCollider = colliders[j];
845+
if (controllerCollider.gameObject.activeInHierarchy)
846+
{
847+
Physics.IgnoreCollision(collider, controllerCollider, state);
848+
}
763849
}
764850
}
765851
}
@@ -847,19 +933,18 @@ protected virtual void SnapToNearestFloor()
847933
Ray ray = new Ray(headset.transform.position, -playArea.up);
848934
RaycastHit rayCollidedWith;
849935
bool rayHit = VRTK_CustomRaycast.Raycast(customRaycast, ray, out rayCollidedWith, layersToIgnore, Mathf.Infinity);
850-
float hitFloorY = headset.transform.position.y - rayCollidedWith.distance;
851-
hitFloorYDelta = playArea.position.y - hitFloorY;
936+
hitFloorYDelta = playArea.position.y - rayCollidedWith.point.y;
852937

853-
if (initialFloorDrop && (ValidDrop(rayHit, rayCollidedWith, hitFloorY) || retogglePhysicsOnCanFall))
938+
if (initialFloorDrop && (ValidDrop(rayHit, rayCollidedWith, rayCollidedWith.point.y) || retogglePhysicsOnCanFall))
854939
{
855940
storedCurrentPhysics = ArePhysicsEnabled();
856941
resetPhysicsAfterTeleport = false;
857942
TogglePhysics(false);
858943

859-
HandleFall(hitFloorY, rayCollidedWith);
944+
HandleFall(rayCollidedWith.point.y, rayCollidedWith);
860945
}
861946
initialFloorDrop = true;
862-
lastFrameFloorY = hitFloorY;
947+
lastFrameFloorY = rayCollidedWith.point.y;
863948
}
864949
}
865950

@@ -926,7 +1011,6 @@ protected virtual void TeleportFall(float floorY, RaycastHit rayCollidedWith)
9261011
GameObject currentFloor = rayCollidedWith.transform.gameObject;
9271012
Vector3 newPosition = new Vector3(playArea.position.x, floorY, playArea.position.z);
9281013
float originalblinkTransitionSpeed = teleporter.blinkTransitionSpeed;
929-
9301014
teleporter.blinkTransitionSpeed = (Mathf.Abs(hitFloorYDelta) > blinkYThreshold ? originalblinkTransitionSpeed : 0f);
9311015
OnDestinationMarkerSet(SetDestinationMarkerEvent(rayCollidedWith.distance, currentFloor.transform, rayCollidedWith, newPosition, uint.MaxValue, true, null));
9321016
teleporter.blinkTransitionSpeed = originalblinkTransitionSpeed;

DOCUMENTATION.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4705,6 +4705,9 @@ To allow for peeking over a ledge and not falling, a fall restiction can happen
47054705
* **Movement Threshold:** The amount of movement of the headset between the headset's current position and the current standing position to determine if the user is walking in play space and to ignore the body physics collisions if the movement delta is above this threshold.
47064706
* **Standing History Samples:** The maximum number of samples to collect of headset position before determining if the current standing position within the play space has changed.
47074707
* **Lean Y Threshold:** The `y` distance between the headset and the object being leaned over, if object being leaned over is taller than this threshold then the current standing position won't be updated.
4708+
* **Step Up Y Offset:** The maximum height to consider when checking if an object can be stepped upon to.
4709+
* **Step Thickness Multiplier:** The width/depth of the foot collider in relation to the radius of the body collider.
4710+
* **Step Drop Threshold:** The distance between the current play area Y position and the new stepped up Y position to consider a valid step up. A higher number can help with juddering on slopes or small increases in collider heights.
47084711
* **Custom Raycast:** A custom raycaster to use when raycasting to find floors.
47094712
* **Fall Restriction:** A check to see if the drop to nearest floor should take place. If the selected restrictor is still over the current floor then the drop to nearest floor will not occur. Works well for being able to lean over ledges and look down. Only works for falling down not teleporting up.
47104713
* **Gravity Fall Y Threshold:** When the `y` distance between the floor and the headset exceeds this distance and `Enable Body Collisions` is true then the rigidbody gravity will be used instead of teleport to drop to nearest floor.

0 commit comments

Comments
 (0)