From 6f78152aa049567ccbbee209ad9186056ce1655b Mon Sep 17 00:00:00 2001
From: Sketch <109103755+SketchFoxsky@users.noreply.github.com>
Date: Mon, 6 Apr 2026 00:47:45 -0400
Subject: [PATCH 1/5] Intial Push
Replaced Unity Character Controller with a Kinematic Rigidbody Character Controller
---
.../Local/BasisLocalCharacterDriver.cs | 97 ++-
.../BasisFlyMovementMode.cs | 16 +-
.../BasisKinematicCharacterController.cs | 642 ++++++++++++++++++
.../BasisKinematicCharacterController.cs.meta | 2 +
.../BasisNoClipMovementMode.cs | 46 +-
.../BasisWalkMovementMode.cs | 20 +-
.../Character Controller/IMovementMode.cs | 2 +-
.../Prefabs/Players/LocalPlayer.prefab | 299 ++++----
8 files changed, 911 insertions(+), 213 deletions(-)
create mode 100644 Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs
create mode 100644 Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs.meta
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs
index a3cfade514..c92153f96f 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs
@@ -15,7 +15,7 @@ public class BasisLocalCharacterDriver
{
public BasisLocalPlayer LocalPlayer;
[System.NonSerialized] public BasisLocalAnimatorDriver LocalAnimatorDriver;
- public CharacterController characterController;
+ public BasisKinematicCharacterController characterController;
public Vector3 bottomPointLocalSpace;
public Vector3 LastBottomPoint;
public bool groundedPlayer;
@@ -104,6 +104,23 @@ public void SetMode(Mode mode)
public Quaternion CurrentRotation;
public CollisionFlags Flags;
public float radius;
+ ///
+ /// The direction gravity pulls the character. Defaults to Vector3.down.
+ /// Changing this allows the character to walk on walls/ceilings.
+ ///
+ public Vector3 GravityDirection
+ {
+ get => characterController != null ? characterController.GravityDirection : Vector3.down;
+ set
+ {
+ if (characterController != null)
+ characterController.GravityDirection = value;
+ }
+ }
+ ///
+ /// The up direction for the character, opposite of GravityDirection.
+ ///
+ public Vector3 UpDirection => characterController != null ? characterController.UpDirection : Vector3.up;
public Vector2 MovementVector { get; private set; }
///
/// A value between 0 and 1 representing the relative speed of player movement.
@@ -158,13 +175,56 @@ public void DeInitalize()
HasEvents = false;
}
}
+ ///
+ /// Ensures the KCC component exists on the player GameObject.
+ /// Migrates from Unity's CharacterController if one is still present on the prefab.
+ ///
+ private void EnsureKCC()
+ {
+ if (characterController != null) return;
+
+ GameObject go = BasisLocalPlayerTransform.gameObject;
+
+ // Check if already has KCC
+ characterController = go.GetComponent();
+ if (characterController != null) return;
+
+ // Migrate from legacy CharacterController if present
+ CharacterController legacy = go.GetComponent();
+ float oldHeight = 2f;
+ float oldRadius = 0.3f;
+ Vector3 oldCenter = new Vector3(0f, 1f, 0f);
+ float oldSkinWidth = 0.01f;
+ float oldStepOffset = 0.3f;
+ float oldSlopeLimit = 45f;
+ if (legacy != null)
+ {
+ oldHeight = legacy.height;
+ oldRadius = legacy.radius;
+ oldCenter = legacy.center;
+ oldSkinWidth = legacy.skinWidth;
+ oldStepOffset = legacy.stepOffset;
+ oldSlopeLimit = legacy.slopeLimit;
+ UnityEngine.Object.DestroyImmediate(legacy);
+ }
+
+ characterController = go.AddComponent();
+ characterController.height = oldHeight;
+ characterController.radius = oldRadius;
+ characterController.center = oldCenter;
+ characterController.skinWidth = oldSkinWidth;
+ characterController.stepOffset = oldStepOffset;
+ characterController.slopeLimit = oldSlopeLimit;
+ }
public void Initialize(BasisLocalPlayer localPlayer)
{
LocalPlayer = localPlayer;
BasisLocalPlayerTransform = localPlayer.transform;
LocalAnimatorDriver = localPlayer.LocalAnimatorDriver;
+ EnsureKCC();
characterController.minMoveDistance = 0;
characterController.skinWidth = 0.01f;
+ characterController.OnKCCColliderHit = OnKCCHit;
if (!HasEvents)
{
HasEvents = true;
@@ -176,11 +236,10 @@ public void Initialize(BasisLocalPlayer localPlayer)
SetMode(Mode.Walk);
}
- public void OnControllerColliderHit(ControllerColliderHit hit)
+ private void OnKCCHit(KCCHitInfo hit)
{
if (CanPushRigidbodys)
{
- // Check if the hit object has a Rigidbody and if it is not kinematic
Rigidbody body = hit.collider.attachedRigidbody;
if (body == null || body.isKinematic)
@@ -188,10 +247,9 @@ public void OnControllerColliderHit(ControllerColliderHit hit)
return;
}
- // Ensure we're only pushing objects in the horizontal plane
- Vector3 pushDir = new Vector3(hit.moveDirection.x, 0, hit.moveDirection.z);
+ // Project push direction onto the plane perpendicular to gravity (horizontal)
+ Vector3 pushDir = hit.moveDirection - UpDirection * Vector3.Dot(hit.moveDirection, UpDirection);
- // Apply the force to the object
body.AddForce(pushDir * pushPower, ForceMode.Impulse);
}
}
@@ -264,7 +322,7 @@ public void SimulateMovement(float DeltaTime)
// Get the current rotation and position of the player
Vector3 pivot = BasisLocalBoneDriver.EyeControl.OutgoingWorldData.position;
- Vector3 upAxis = Vector3.up;
+ Vector3 upAxis = UpDirection;
// Calculate direction from the pivot to the current position
Vector3 directionToPivot = CurrentPosition - pivot;
@@ -280,7 +338,7 @@ public void SimulateMovement(float DeltaTime)
BasisLocalPlayerTransform.SetPositionAndRotation(FinalRotation, rotation * CurrentRotation);
float HeightOffset = (characterController.height / 2) - characterController.radius;
- bottomPointLocalSpace = FinalRotation + (characterController.center - new Vector3(0, HeightOffset, 0));
+ bottomPointLocalSpace = FinalRotation + (characterController.center - upAxis * HeightOffset);
Quaternion newRot = rotation * CurrentRotation;
Vector3 newPos = FinalRotation;
@@ -388,16 +446,20 @@ public void SetMovementVector(Vector2 movement)
}
public void HandleMovement(float DeltaTime)
{
- // Cache current rotation and zero out x and z components
+ // Cache current rotation and flatten to the plane perpendicular to gravity
currentRotation = BasisLocalBoneDriver.HeadControl.OutgoingWorldData.rotation;
- Vector3 rotationEulerAngles = currentRotation.eulerAngles;
- rotationEulerAngles.x = 0;
- rotationEulerAngles.z = 0;
-
- Quaternion flattenedRotation = Quaternion.Euler(rotationEulerAngles);
+ Vector3 up = UpDirection;
+ Vector3 flatForward = currentRotation * Vector3.forward;
+ flatForward -= up * Vector3.Dot(flatForward, up);
+ if (flatForward.sqrMagnitude < 0.0001f)
+ {
+ flatForward = -(currentRotation * Vector3.up);
+ flatForward -= up * Vector3.Dot(flatForward, up);
+ }
+ Quaternion flattenedRotation = Quaternion.LookRotation(flatForward.normalized, up);
if (CrouchBlendDelta != 0) UpdateCrouchBlend(CrouchBlendDelta);
- // Calculate horizontal movement direction
+ // Calculate horizontal movement direction (in the character's gravity-relative plane)
Vector3 horizontalMoveDirection = new Vector3(MovementVector.x, 0, MovementVector.y).normalized;
CurrentSpeed = math.lerp(MinimumMovementSpeed, MaximumMovementSpeed, MovementSpeedScale) + MinimumMovementSpeed * MovementSpeedBoost;
@@ -427,7 +489,8 @@ public void HandleMovement(float DeltaTime)
HasJumpAction = false;
- totalMoveDirection.y = currentVerticalSpeed * DeltaTime;
+ // Apply vertical speed along the gravity up axis instead of world Y
+ totalMoveDirection += up * (currentVerticalSpeed * DeltaTime);
// Move character
Flags = characterController.Move(totalMoveDirection);
@@ -479,7 +542,7 @@ public void CalculateCharacterSize()
}
// Clamp stepOffset to something sane relative to height
- float maxStep = (finalHeight + 2f * characterController.radius) - 0.001f;
+ float maxStep = (finalHeight + 2f * radius) - 0.001f;
maxStep = Mathf.Max(0f, maxStep);
maxStep = Mathf.Min(maxStep, finalHeight * 0.25f);
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisFlyMovementMode.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisFlyMovementMode.cs
index f760ac0989..1ea7798ccb 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisFlyMovementMode.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisFlyMovementMode.cs
@@ -14,7 +14,7 @@ public void Enter(BasisLocalCharacterDriver ctx)
{
if (ctx.characterController != null)
{
- ctx.characterController.detectCollisions = true; // solid, but no gravity
+ ctx.characterController.detectCollisions = true;
ctx.characterController.enabled = true;
}
ctx.currentVerticalSpeed = 0f;
@@ -24,16 +24,18 @@ public void Exit(BasisLocalCharacterDriver ctx) { }
public void Tick(BasisLocalCharacterDriver ctx, float dt)
{
- // Project head forward onto horizontal plane (avoids gimbal lock near ±90° pitch)
+ Vector3 up = ctx.UpDirection;
+
+ // Project head forward onto the plane perpendicular to gravity
Quaternion headRot = BasisLocalBoneDriver.HeadControl.OutgoingWorldData.rotation;
Vector3 flatForward = headRot * Vector3.forward;
- flatForward.y = 0f;
+ flatForward -= up * Vector3.Dot(flatForward, up);
if (flatForward.sqrMagnitude < 0.0001f)
{
flatForward = -(headRot * Vector3.up);
- flatForward.y = 0f;
+ flatForward -= up * Vector3.Dot(flatForward, up);
}
- Quaternion facing = Quaternion.LookRotation(flatForward.normalized, Vector3.up);
+ Quaternion facing = Quaternion.LookRotation(flatForward.normalized, up);
// Planar
Vector3 planar = new Vector3(ctx.MovementVector.x, 0, ctx.MovementVector.y).normalized;
@@ -44,8 +46,8 @@ public void Tick(BasisLocalCharacterDriver ctx, float dt)
Vector3 move = facing * planar * ctx.CurrentSpeed * dt;
- // ===== Vertical input (held) =====
- move.y = ctx.GetVerticalMovement() * ctx.CurrentSpeed * dt;
+ // Vertical input along gravity-relative up axis
+ move += up * (ctx.GetVerticalMovement() * ctx.CurrentSpeed * dt);
// Clear tap
ctx.HasJumpAction = false;
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs
new file mode 100644
index 0000000000..7b93998063
--- /dev/null
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs
@@ -0,0 +1,642 @@
+using UnityEngine;
+
+namespace Basis.Scripts.BasisCharacterController
+{
+ ///
+ /// A kinematic character controller that replaces Unity's built-in CharacterController.
+ /// Uses a CapsuleCollider + kinematic Rigidbody so that the player can be rotated
+ /// freely, enabling custom gravity directions (not Y-axis locked).
+ ///
+ /// TODO
+ /// Add Rotate to Gravity
+ /// Add Flight/Noclip exposure for worlds
+ /// MAYBE add native swimming!
+ ///
+ ///
+ [RequireComponent(typeof(Rigidbody))]
+ [RequireComponent(typeof(CapsuleCollider))]
+ public class BasisKinematicCharacterController : MonoBehaviour
+ {
+ // Capsule Collider
+ [SerializeField] private float _height = 2f;
+ [SerializeField] private float _radius = 0.3f;
+ [SerializeField] private Vector3 _center = new Vector3(0f, 1f, 0f);
+
+ // CharacterController Parameters
+ [SerializeField] private float _skinWidth = 0.01f;
+ [SerializeField] private float _stepOffset = 0.3f;
+ [SerializeField] private float _minMoveDistance = 0f;
+ [SerializeField] private float _slopeLimit = 45f;
+ [SerializeField] private bool _detectCollisions = true;
+
+ //Gravity
+ [SerializeField] private Vector3 _gravityDirection = Vector3.down;
+
+ // Runtime Info
+ private Rigidbody _rigidbody;
+ private CapsuleCollider _capsule;
+ private bool _isGrounded;
+ private Vector3 _groundNormal = Vector3.up;
+ private CollisionFlags _lastFlags;
+
+ // Collision
+ private const int MaxHits = 16;
+ private const int MaxOverlaps = 16;
+ private const int MaxDepenetrationIterations = 4;
+ private const int MaxMoveIterations = 4;
+ private readonly RaycastHit[] _hitBuffer = new RaycastHit[MaxHits];
+ private readonly Collider[] _overlapBuffer = new Collider[MaxOverlaps];
+ private const float GroundProbeExtra = 0.04f;
+ public delegate void KCCColliderHit(KCCHitInfo hit);
+ public KCCColliderHit OnKCCColliderHit;
+
+ //Public Get Set
+
+ public float height
+ {
+ get => _height;
+ set
+ {
+ _height = Mathf.Max(value, 0.001f);
+ SyncCapsule();
+ }
+ }
+
+ public float radius
+ {
+ get => _radius;
+ set
+ {
+ _radius = Mathf.Max(value, 0.001f);
+ SyncCapsule();
+ }
+ }
+
+ public Vector3 center
+ {
+ get => _center;
+ set
+ {
+ _center = value;
+ SyncCapsule();
+ }
+ }
+
+ public float skinWidth
+ {
+ get => _skinWidth;
+ set => _skinWidth = Mathf.Max(value, 0.001f);
+ }
+
+ public float stepOffset
+ {
+ get => _stepOffset;
+ set => _stepOffset = Mathf.Max(value, 0f);
+ }
+
+ public float minMoveDistance
+ {
+ get => _minMoveDistance;
+ set => _minMoveDistance = Mathf.Max(value, 0f);
+ }
+
+ public float slopeLimit
+ {
+ get => _slopeLimit;
+ set => _slopeLimit = Mathf.Clamp(value, 0f, 90f);
+ }
+
+ public bool detectCollisions
+ {
+ get => _detectCollisions;
+ set
+ {
+ _detectCollisions = value;
+ if (_capsule != null)
+ _capsule.enabled = value;
+ }
+ }
+
+ public bool isGrounded => _isGrounded;
+
+ ///
+ /// The ground surface normal from the last Move() call. Only valid when isGrounded is true.
+ ///
+ public Vector3 groundNormal => _groundNormal;
+
+ ///
+ /// The direction gravity pulls the character. Defaults to Vector3.down.
+ /// Setting this allows the character to walk on walls/ceilings.
+ /// Must be normalized.
+ ///
+ public Vector3 GravityDirection
+ {
+ get => _gravityDirection;
+ set => _gravityDirection = value.normalized;
+ }
+
+ ///
+ /// The "up" direction for this character, opposite of gravity.
+ /// Guaranteed to be normalized; falls back to Vector3.up if gravity direction is zero.
+ ///
+ public Vector3 UpDirection
+ {
+ get
+ {
+ Vector3 up = -_gravityDirection;
+ if (up.sqrMagnitude < 0.0001f) up = Vector3.up;
+ return up.normalized;
+ }
+ }
+
+ #region UNITY LIFECYCLE
+
+ private void Awake()
+ {
+ // Ensure gravity direction is valid (may be zero if deserialized from a fresh component)
+ if (_gravityDirection.sqrMagnitude < 0.0001f)
+ _gravityDirection = Vector3.down;
+
+ _rigidbody = GetComponent();
+ _rigidbody.isKinematic = true;
+ _rigidbody.useGravity = false;
+ _rigidbody.interpolation = RigidbodyInterpolation.None;
+ _rigidbody.collisionDetectionMode = CollisionDetectionMode.Discrete;
+ _rigidbody.constraints = RigidbodyConstraints.FreezeAll;
+
+ _capsule = GetComponent();
+ SyncCapsule();
+ }
+
+ private void SyncCapsule()
+ {
+ if (_capsule == null) return;
+ _capsule.direction = 1; // Y-axis
+ _capsule.center = _center;
+ _capsule.radius = _radius;
+ _capsule.height = _height;
+ _capsule.isTrigger = false;
+ }
+
+ // ── Core Move method ────────────────────────────────────────────
+
+ ///
+ /// Moves the character by with full collision
+ /// resolution. Returns CollisionFlags indicating which sides were hit.
+ /// Gravity/jump velocity should already be included in motion.
+ ///
+ public CollisionFlags Move(Vector3 motion)
+ {
+ _lastFlags = CollisionFlags.None;
+
+ if (!enabled || !_detectCollisions)
+ {
+ transform.position += motion;
+ _isGrounded = false;
+ return _lastFlags;
+ }
+
+ if (motion.sqrMagnitude < _minMoveDistance * _minMoveDistance)
+ {
+ GroundProbe();
+ return _lastFlags;
+ }
+
+ Vector3 up = UpDirection;
+ float cosSlope = Mathf.Cos(_slopeLimit * Mathf.Deg2Rad);
+ float verticalComponent = Vector3.Dot(motion, up);
+ Vector3 verticalMotion = up * verticalComponent;
+ Vector3 horizontalMotion = motion - verticalMotion;
+ bool movingDown = verticalComponent < 0f;
+
+ // ── Grounded behaviour: slope projection + ground snap ──────
+ if (_isGrounded && movingDown)
+ {
+ float groundDot = Vector3.Dot(_groundNormal, up);
+ bool walkableSlope = groundDot >= cosSlope;
+
+ if (walkableSlope)
+ {
+ if (horizontalMotion.sqrMagnitude > 0.00001f)
+ {
+ horizontalMotion = Vector3.ProjectOnPlane(horizontalMotion, _groundNormal);
+ float origLen = (motion - verticalMotion).magnitude;
+ float projLen = horizontalMotion.magnitude;
+ if (projLen > 0.00001f)
+ horizontalMotion = horizontalMotion * (origLen / projLen);
+ }
+ verticalMotion = Vector3.zero;
+ }
+ }
+
+ Vector3 pos = transform.position;
+
+ // ── Horizontal movement with step-up fallback ───────────────
+ if (horizontalMotion.sqrMagnitude > 0.00001f)
+ {
+ Vector3 beforeSlide = pos;
+ pos = SimpleMove(pos, horizontalMotion, ref _lastFlags, up, cosSlope, isHorizontal: true);
+
+ // If grounded and slide made little horizontal progress, try stepping up
+ if (_isGrounded && _stepOffset > 0f && movingDown)
+ {
+ Vector3 traveled = pos - beforeSlide;
+ Vector3 horizontalTraveled = traveled - up * Vector3.Dot(traveled, up);
+ Vector3 horizontalWanted = horizontalMotion - up * Vector3.Dot(horizontalMotion, up);
+ float wantedLen = horizontalWanted.magnitude;
+
+ if (wantedLen > 0.001f && horizontalTraveled.magnitude < wantedLen * 0.5f)
+ {
+ // Blocked — try step-up from the pre-slide position
+ Vector3 stepPos = beforeSlide;
+ if (TryStepUp(ref stepPos, horizontalMotion, up, cosSlope))
+ {
+ pos = stepPos;
+ }
+ }
+ }
+ }
+
+ // vertical movement
+ if (verticalMotion.sqrMagnitude > 0.00001f)
+ {
+ pos = SimpleMove(pos, verticalMotion, ref _lastFlags, up, cosSlope, isHorizontal: false);
+ }
+
+ transform.position = pos;
+
+ // snap to ground
+ if (_isGrounded && verticalComponent <= 0f)
+ {
+ pos = GroundSnap(pos, up, cosSlope);
+ transform.position = pos;
+ }
+
+ // Depenetration if clipping with a collider
+ pos = Depenetrate(pos);
+ transform.position = pos;
+
+ // Ground probe
+ GroundProbe();
+
+ return _lastFlags;
+ }
+
+ private Vector3 SimpleMove(Vector3 position, Vector3 motion, ref CollisionFlags flags, Vector3 up, float cosSlope, bool isHorizontal)
+ {
+ if (motion.sqrMagnitude < 0.00001f) return position;
+
+ Vector3 remaining = motion;
+ for (int i = 0; i < MaxMoveIterations && remaining.sqrMagnitude > 0.00001f; i++)
+ {
+ float dist = remaining.magnitude;
+ Vector3 dir = remaining / dist;
+
+ GetCapsuleEnds(position, out Vector3 p1, out Vector3 p2);
+ float castRadius = _radius - _skinWidth;
+ if (castRadius < 0.001f) castRadius = 0.001f;
+
+ int hitCount = Physics.CapsuleCastNonAlloc(
+ p1, p2, castRadius,
+ dir, _hitBuffer,
+ dist + _skinWidth,
+ GetCollisionMask(),
+ QueryTriggerInteraction.Ignore
+ );
+
+ if (!FindClosestHit(hitCount, out RaycastHit closestHit))
+ {
+ position += remaining;
+ break;
+ }
+
+ // Move up to the hit point (minus skin)
+ float safeDistance = Mathf.Max(closestHit.distance - _skinWidth, 0f);
+ position += dir * safeDistance;
+
+ // Classify collision
+ Vector3 hitNormal = closestHit.normal;
+ float dotUp = Vector3.Dot(hitNormal, up);
+
+ if (dotUp > 0.7f)
+ flags |= CollisionFlags.Below;
+ else if (dotUp < -0.7f)
+ flags |= CollisionFlags.Above;
+ else
+ flags |= CollisionFlags.Sides;
+
+ FireHitCallback(closestHit, dir, dist);
+
+ // move along the surface
+ remaining -= dir * safeDistance;
+ remaining = Vector3.ProjectOnPlane(remaining, hitNormal);
+
+ // For horizontal movement on steep slopes, prevent climbing
+ if (isHorizontal && dotUp > 0f && dotUp < cosSlope)
+ {
+ float upComponent = Vector3.Dot(remaining, up);
+ if (upComponent > 0f)
+ remaining -= up * upComponent;
+ }
+ }
+
+ return position;
+ }
+
+ // Step Offset
+
+ // kill me please this took a while to debug, had to look at old braxy tutorials :)
+
+ private bool TryStepUp(ref Vector3 pos, Vector3 horizontalMotion, Vector3 up, float cosSlope)
+ {
+ float castRadius = _radius - _skinWidth;
+ if (castRadius < 0.001f) castRadius = 0.001f;
+
+ Vector3 hDir = horizontalMotion.normalized;
+ // Use a generous forward distance so we clear the step edge.
+ // At minimum cast the frame motion, but ensure we cast at least
+ // radius + skin so the capsule actually clears the step lip.
+ float hDist = Mathf.Max(horizontalMotion.magnitude, _radius + _skinWidth);
+
+ // Phase 1: Cast UP to find ceiling clearance
+ float maxUpDist = _stepOffset;
+ GetCapsuleEnds(pos, out Vector3 up1, out Vector3 up2);
+ int hitCount = Physics.CapsuleCastNonAlloc(
+ up1, up2, castRadius,
+ up, _hitBuffer,
+ maxUpDist + _skinWidth,
+ GetCollisionMask(),
+ QueryTriggerInteraction.Ignore
+ );
+ if (FindClosestHit(hitCount, out RaycastHit ceilingHit))
+ {
+ maxUpDist = Mathf.Max(ceilingHit.distance - _skinWidth, 0f);
+ }
+ if (maxUpDist < 0.01f)
+ return false; // No room to step up
+
+ Vector3 elevated = pos + up * maxUpDist;
+
+ // Phase 2: Cast FORWARD at elevated height
+ GetCapsuleEnds(elevated, out Vector3 ep1, out Vector3 ep2);
+ hitCount = Physics.CapsuleCastNonAlloc(
+ ep1, ep2, castRadius,
+ hDir, _hitBuffer,
+ hDist + _skinWidth,
+ GetCollisionMask(),
+ QueryTriggerInteraction.Ignore
+ );
+
+ float forwardDist = hDist;
+ if (FindClosestHit(hitCount, out RaycastHit forwardHit))
+ {
+ forwardDist = Mathf.Max(forwardHit.distance - _skinWidth, 0f);
+ }
+ // Need at least a tiny forward movement to land on top of the step
+ if (forwardDist < _skinWidth)
+ return false;
+
+ Vector3 forwarded = elevated + hDir * Mathf.Min(forwardDist, hDist);
+
+ // Phase 3: Cast DOWN to find the step surface
+ GetCapsuleEnds(forwarded, out Vector3 dp1, out Vector3 dp2);
+ float downDist = maxUpDist + GroundProbeExtra;
+ hitCount = Physics.CapsuleCastNonAlloc(
+ dp1, dp2, castRadius,
+ -up, _hitBuffer,
+ downDist,
+ GetCollisionMask(),
+ QueryTriggerInteraction.Ignore
+ );
+
+ if (!FindClosestHit(hitCount, out RaycastHit stepHit))
+ return false; // No ground found after stepping
+
+ // Verify the surface is walkable
+ float dotUp = Vector3.Dot(stepHit.normal, up);
+ if (dotUp < cosSlope)
+ return false;
+
+ // Snap down to the step surface
+ float snapDown = Mathf.Max(stepHit.distance - _skinWidth, 0f);
+ Vector3 finalPos = forwarded - up * snapDown;
+
+ // Must have actually gained height
+ float heightGain = Vector3.Dot(finalPos - pos, up);
+ if (heightGain < 0.001f)
+ return false;
+
+ pos = finalPos;
+ _lastFlags |= CollisionFlags.Below;
+ return true;
+ }
+
+ //Ground Snapping
+
+ // Add ground Friction, its almost 1am im not doing that tonight
+
+ ///
+ /// When grounded and not jumping, cast downward to anchor the character
+ /// to the ground surface. Prevents floating over small bumps and slopes.
+ ///
+ private Vector3 GroundSnap(Vector3 position, Vector3 up, float cosSlope)
+ {
+ GetCapsuleEnds(position, out Vector3 p1, out Vector3 p2);
+ float castRadius = _radius - _skinWidth;
+ if (castRadius < 0.001f) castRadius = 0.001f;
+
+ // Snap distance: enough to cover step offset + skin + small gap
+ float snapDist = _stepOffset + _skinWidth + GroundProbeExtra;
+ int hitCount = Physics.CapsuleCastNonAlloc(
+ p1, p2, castRadius,
+ -up, _hitBuffer,
+ snapDist,
+ GetCollisionMask(),
+ QueryTriggerInteraction.Ignore
+ );
+
+ if (FindClosestHit(hitCount, out RaycastHit snapHit))
+ {
+ float dotUp = Vector3.Dot(snapHit.normal, up);
+ if (dotUp >= cosSlope)
+ {
+ float drop = Mathf.Max(snapHit.distance - _skinWidth, 0f);
+ if (drop > 0.0001f)
+ {
+ position -= up * drop;
+ _lastFlags |= CollisionFlags.Below;
+ }
+ }
+ }
+
+ return position;
+ }
+
+ // Depenetration Helper
+
+ private Vector3 Depenetrate(Vector3 position)
+ {
+ for (int iter = 0; iter < MaxDepenetrationIterations; iter++)
+ {
+ GetCapsuleEnds(position, out Vector3 p1, out Vector3 p2);
+
+ int overlapCount = Physics.OverlapCapsuleNonAlloc(
+ p1, p2, _radius,
+ _overlapBuffer,
+ GetCollisionMask(),
+ QueryTriggerInteraction.Ignore
+ );
+
+ bool resolved = true;
+ for (int i = 0; i < overlapCount; i++)
+ {
+ Collider other = _overlapBuffer[i];
+ if (other == _capsule) continue;
+
+ if (Physics.ComputePenetration(
+ _capsule, position, transform.rotation,
+ other, other.transform.position, other.transform.rotation,
+ out Vector3 dir, out float dist))
+ {
+ position += dir * (dist + 0.001f);
+ resolved = false;
+ }
+ }
+
+ if (resolved) break;
+ }
+
+ return position;
+ }
+
+ // Ground Detection
+
+ private void GroundProbe()
+ {
+ if (!_detectCollisions || !enabled)
+ {
+ _isGrounded = false;
+ _groundNormal = UpDirection;
+ return;
+ }
+
+ Vector3 up = UpDirection;
+ Vector3 pos = transform.position;
+
+ // Cast a small sphere downward from the bottom of the capsule
+ Vector3 worldCenter = pos + transform.rotation * _center;
+ float halfHeight = (_height * 0.5f) - _radius;
+ Vector3 bottom = worldCenter - up * halfHeight;
+
+ float castRadius = _radius - _skinWidth;
+ if (castRadius < 0.001f) castRadius = 0.001f;
+
+ float probeOffset = _skinWidth + 0.01f;
+ int hitCount = Physics.SphereCastNonAlloc(
+ bottom + up * probeOffset,
+ castRadius,
+ -up,
+ _hitBuffer,
+ probeOffset + GroundProbeExtra,
+ GetCollisionMask(),
+ QueryTriggerInteraction.Ignore
+ );
+
+ _isGrounded = false;
+ _groundNormal = up;
+ float cosSlope = Mathf.Cos(_slopeLimit * Mathf.Deg2Rad);
+ float closestDist = float.MaxValue;
+
+ for (int i = 0; i < hitCount; i++)
+ {
+ if (_hitBuffer[i].collider == _capsule) continue;
+ float dotUp = Vector3.Dot(_hitBuffer[i].normal, up);
+ if (dotUp >= cosSlope && _hitBuffer[i].distance < closestDist)
+ {
+ closestDist = _hitBuffer[i].distance;
+ _isGrounded = true;
+ _groundNormal = _hitBuffer[i].normal;
+ }
+ }
+ }
+ #endregion
+
+ #region HELPERS
+
+ private void GetCapsuleEnds(Vector3 position, out Vector3 point1, out Vector3 point2)
+ {
+ Quaternion rot = transform.rotation;
+ Vector3 worldCenter = position + rot * _center;
+ Vector3 capsuleUp = rot * Vector3.up;
+ float halfHeight = (_height * 0.5f) - _radius;
+ if (halfHeight < 0f) halfHeight = 0f;
+ point1 = worldCenter + capsuleUp * halfHeight;
+ point2 = worldCenter - capsuleUp * halfHeight;
+ }
+
+ private int GetCollisionMask()
+ {
+ int layer = gameObject.layer;
+ int mask = 0;
+ for (int i = 0; i < 32; i++)
+ {
+ if (!Physics.GetIgnoreLayerCollision(layer, i))
+ mask |= (1 << i);
+ }
+ return mask;
+ }
+
+ private bool FindClosestHit(int hitCount, out RaycastHit closest)
+ {
+ float closestDist = float.MaxValue;
+ closest = default;
+ bool found = false;
+ for (int i = 0; i < hitCount; i++)
+ {
+ if (_hitBuffer[i].collider == _capsule) continue;
+ if (_hitBuffer[i].distance < closestDist)
+ {
+ closestDist = _hitBuffer[i].distance;
+ closest = _hitBuffer[i];
+ found = true;
+ }
+ }
+ return found;
+ }
+
+ private bool HasValidHit(int hitCount)
+ {
+ for (int i = 0; i < hitCount; i++)
+ {
+ if (_hitBuffer[i].collider != _capsule) return true;
+ }
+ return false;
+ }
+
+ private void FireHitCallback(RaycastHit hit, Vector3 moveDir, float moveDist)
+ {
+ if (OnKCCColliderHit == null) return;
+ KCCHitInfo info;
+ info.collider = hit.collider;
+ info.point = hit.point;
+ info.normal = hit.normal;
+ info.moveDirection = moveDir;
+ info.moveLength = moveDist;
+ OnKCCColliderHit(info);
+ }
+ }
+
+ #endregion
+
+ ///
+ /// Hit information passed to the KCC collision callback, mirroring ControllerColliderHit.
+ ///
+ public struct KCCHitInfo
+ {
+ public Collider collider;
+ public Vector3 point;
+ public Vector3 normal;
+ public Vector3 moveDirection;
+ public float moveLength;
+ }
+}
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs.meta b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs.meta
new file mode 100644
index 0000000000..feb57957d0
--- /dev/null
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3f50f4d9a7b7b5c488ef048f8e073021
\ No newline at end of file
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisNoClipMovementMode.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisNoClipMovementMode.cs
index 5b42d041a3..0385c9cb25 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisNoClipMovementMode.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisNoClipMovementMode.cs
@@ -17,14 +17,14 @@ public class BasisNoClipMovementMode : IMovementMode
public void Enter(BasisLocalCharacterDriver ctx)
{
- // Fully ghost the CharacterController so it won’t push/depenetrate
+ // Disable the KCC's collision detection so it won't push/depenetrate
if (ctx.characterController != null)
{
ctx.characterController.detectCollisions = false;
ctx.characterController.enabled = false;
}
- // Ensure a trigger-only probe exists and matches the CC size
+ // Ensure a trigger-only probe exists and matches the KCC size
EnsureTriggerProbe(ctx);
// enable the trigger probe
@@ -38,7 +38,7 @@ public void Enter(BasisLocalCharacterDriver ctx)
public void Exit(BasisLocalCharacterDriver ctx)
{
- // Re-enable CC for other modes
+ // Re-enable KCC for other modes
if (ctx.characterController != null)
{
ctx.characterController.detectCollisions = true;
@@ -52,16 +52,18 @@ public void Exit(BasisLocalCharacterDriver ctx)
public void Tick(BasisLocalCharacterDriver ctx, float dt)
{
- // Project head forward onto horizontal plane (avoids gimbal lock near ±90° pitch)
+ Vector3 up = ctx.UpDirection;
+
+ // Project head forward onto the plane perpendicular to gravity
Quaternion headRot = BasisLocalBoneDriver.HeadControl.OutgoingWorldData.rotation;
Vector3 flatForward = headRot * Vector3.forward;
- flatForward.y = 0f;
+ flatForward -= up * Vector3.Dot(flatForward, up);
if (flatForward.sqrMagnitude < 0.0001f)
{
flatForward = -(headRot * Vector3.up);
- flatForward.y = 0f;
+ flatForward -= up * Vector3.Dot(flatForward, up);
}
- Quaternion facing = Quaternion.LookRotation(flatForward.normalized, Vector3.up);
+ Quaternion facing = Quaternion.LookRotation(flatForward.normalized, up);
// Same speed model you already use
ctx.CurrentSpeed =
@@ -72,8 +74,8 @@ public void Tick(BasisLocalCharacterDriver ctx, float dt)
Vector3 planar = new Vector3(ctx.MovementVector.x, 0f, ctx.MovementVector.y).normalized;
Vector3 move = facing * planar * ctx.CurrentSpeed * dt;
- // Vertical input
- move.y = ctx.GetVerticalMovement() * ctx.CurrentSpeed * dt;
+ // Vertical input along gravity-relative up axis
+ move += up * (ctx.GetVerticalMovement() * ctx.CurrentSpeed * dt);
ctx.HasJumpAction = false;
if (ctx.MovementLock) move = Vector3.zero;
@@ -87,13 +89,13 @@ public void Tick(BasisLocalCharacterDriver ctx, float dt)
_triggerBody.position = ctx.BasisLocalPlayerTransform.position;
_triggerBody.rotation = ctx.BasisLocalPlayerTransform.rotation;
}
- var cc = ctx.characterController;
- if (cc != null && _triggerCapsule != null)
+ var kcc = ctx.characterController;
+ if (kcc != null && _triggerCapsule != null)
{
- _triggerCapsule.center = cc.center;
- _triggerCapsule.radius = cc.radius;
- _triggerCapsule.height = cc.height;
- _triggerCapsule.direction = 1; // Y axis like CharacterController
+ _triggerCapsule.center = kcc.center;
+ _triggerCapsule.radius = kcc.radius;
+ _triggerCapsule.height = kcc.height;
+ _triggerCapsule.direction = 1; // Y axis
}
// Sync state
ctx.BasisLocalPlayerTransform.GetPositionAndRotation(out ctx.CurrentPosition, out ctx.CurrentRotation);
@@ -114,14 +116,14 @@ private void EnsureTriggerProbe(BasisLocalCharacterDriver ctx)
_triggerCapsule = BasisHelpers.GetOrAddComponent(go);
_triggerCapsule.isTrigger = true;
- // Match CC dimensions so overlaps are accurate
- var cc = ctx.characterController;
- if (cc != null)
+ // Match KCC dimensions so overlaps are accurate
+ var kcc = ctx.characterController;
+ if (kcc != null)
{
- _triggerCapsule.center = cc.center;
- _triggerCapsule.radius = cc.radius;
- _triggerCapsule.height = cc.height;
- _triggerCapsule.direction = 1; // Y axis like CharacterController
+ _triggerCapsule.center = kcc.center;
+ _triggerCapsule.radius = kcc.radius;
+ _triggerCapsule.height = kcc.height;
+ _triggerCapsule.direction = 1; // Y axis
}
// Make sure physics queries will consider triggers (usually true by default)
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisWalkMovementMode.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisWalkMovementMode.cs
index 23a1bffa37..f5cccb644f 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisWalkMovementMode.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisWalkMovementMode.cs
@@ -27,16 +27,18 @@ public void Tick(BasisLocalCharacterDriver ctx, float dt)
ctx.UpdateCrouchBlend(ctx.CrouchBlendDelta);
}
- // Project head forward onto horizontal plane (avoids gimbal lock near ±90° pitch)
+ Vector3 up = ctx.UpDirection;
+
+ // Project head forward onto the plane perpendicular to gravity (avoids gimbal lock near ±90° pitch)
Quaternion headRot = BasisLocalBoneDriver.HeadControl.OutgoingWorldData.rotation;
Vector3 flatForward = headRot * Vector3.forward;
- flatForward.y = 0f;
+ flatForward -= up * Vector3.Dot(flatForward, up);
if (flatForward.sqrMagnitude < 0.0001f)
{
flatForward = -(headRot * Vector3.up);
- flatForward.y = 0f;
+ flatForward -= up * Vector3.Dot(flatForward, up);
}
- Quaternion facing = Quaternion.LookRotation(flatForward.normalized, Vector3.up);
+ Quaternion facing = Quaternion.LookRotation(flatForward.normalized, up);
Vector3 inputDir = new Vector3(ctx.MovementVector.x, 0, ctx.MovementVector.y).normalized;
@@ -48,7 +50,6 @@ public void Tick(BasisLocalCharacterDriver ctx, float dt)
// Ground & gravity
ctx.GroundCheck(dt);
-
if (ctx.CanJump && ctx.HasJumpAction && !ctx.MovementLock)
{
ctx.currentVerticalSpeed = Mathf.Sqrt(ctx.jumpHeight * -2f * ctx.gravityValue);
@@ -61,14 +62,17 @@ public void Tick(BasisLocalCharacterDriver ctx, float dt)
}
ctx.currentVerticalSpeed = Mathf.Max(ctx.currentVerticalSpeed, -Mathf.Abs(ctx.gravityValue));
+
ctx.HasJumpAction = false;
- move.y = ctx.currentVerticalSpeed * dt;
+ // Apply vertical speed along gravity-relative up axis instead of world Y
+ move += up * (ctx.currentVerticalSpeed * dt);
if (ctx.MovementLock)
{
- move.x = 0;
- move.z = 0;
+ // Zero out horizontal component but keep vertical (gravity)
+ Vector3 verticalPart = up * Vector3.Dot(move, up);
+ move = verticalPart;
}
ctx.Flags = ctx.characterController.Move(move);
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/IMovementMode.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/IMovementMode.cs
index 1f8e3b2f00..f320c2d4ed 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/IMovementMode.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/IMovementMode.cs
@@ -18,7 +18,7 @@ public interface IMovementMode
void Exit(BasisLocalCharacterDriver ctx);
// Per-frame simulation of displacement and vertical speed
- // Should call CharacterController.Move when Collision==Solid, or set transform directly when Ghost
+ // Should call BasisKinematicCharacterController.Move when Collision==Solid, or set transform directly when Ghost
void Tick(BasisLocalCharacterDriver ctx, float deltaTime);
}
}
diff --git a/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab b/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab
index 1966def44c..019825774e 100644
--- a/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab
+++ b/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab
@@ -327,7 +327,7 @@ MonoBehaviour:
SpriteMicrophoneOff: {fileID: 21300000, guid: f184b79ab72cc224e94720390fb038c2, type: 3}
VRdesiredNormXY: {x: -0.42, y: -0.52}
VRextraViewportPad: 0.022
- iconHalfRU: {x: 0, y: 0}
+ IconPositionOffset: {x: 0, y: 0}
LocalIsTransmitting: 0
UnMutedMutedIconColorActive: {r: 1, g: 1, b: 1, a: 0.67058825}
UnMutedMutedIconColorInactive: {r: 0.5, g: 0.5, b: 0.5, a: 0.67058825}
@@ -484,7 +484,9 @@ GameObject:
m_Component:
- component: {fileID: 303201899784742928}
- component: {fileID: 4587526044252889482}
- - component: {fileID: 5548795088377030369}
+ - component: {fileID: 8369693708823570861}
+ - component: {fileID: 3011869999757657}
+ - component: {fileID: 7825922350359905173}
m_Layer: 3
m_Name: LocalPlayer
m_TagString: Untagged
@@ -520,11 +522,13 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 19f412872e0df964494751723e9f3838, type: 3}
m_Name:
m_EditorClassIdentifier:
+ PlayerPlatform:
DisplayName:
UUID:
SafeDisplayName:
BasisAvatar: {fileID: 0}
AvatarTransform: {fileID: 0}
+ AvatarAnimatorTransform: {fileID: 0}
PlayerSelf: {fileID: 0}
FaceIsVisible: 0
FaceRenderer: {fileID: 0}
@@ -541,9 +545,18 @@ MonoBehaviour:
BasisBundleDescription:
AssetBundleName:
AssetBundleDescription:
+ AssetBundleIcon: {fileID: 0}
BasisBundleGenerated: []
ImageBase64:
DateOfCreation:
+ Bounds:
+ m_Center: {x: 0, y: 0, z: 0}
+ m_Extents: {x: 0, y: 0, z: 0}
+ MetaData:
+ TrianglesCount: 0
+ MaterialCount: 0
+ BonesCount: 0
+ ComponentNames: []
LocalCameraDriver: {fileID: 22857711927985140}
LocalBoneDriver:
ControlsLength: 0
@@ -566,17 +579,69 @@ MonoBehaviour:
m_Active: 0
BasisFullIKConstraint: {fileID: 0}
timeAccumulator: 0
- OutputkneeBendPrefLeftWeights: {x: 0, y: 0, z: 0}
- OutputkneeBendPrefRightWeights: {x: 0, y: 0, z: 0}
- kneeBendPrefLeftWeights: {x: 0, y: 1, z: -0.2}
- kneeBendPrefRightWeights: {x: 0, y: 1, z: 0.2}
elbowBendPrefLeftWeights: {x: 0, y: 1, z: 0}
elbowBendPrefRightWeights: {x: 0, y: 1, z: 0}
spineBendNormalWeights: {x: 0, y: 1, z: 0}
- ScrossLeggedModifier: {x: 1.25, y: 0.25}
+ BasisLocalFootDriver:
+ predictionFactor: 0.9
+ velocityBiasFactor: 0.18
+ leadOffsetFactor: 0.6
+ maxVelocityOffsetFraction: 0.35
+ maxPredictionFraction: 0.35
+ plantedLerpSpeed: 40
+ rotationLerpSpeed: 16
+ velocitySmoothAccel: 10
+ velocitySmoothDecel: 50
+ bodyFwdRateMoving: 6
+ bodyFwdRateStationary: 2.5
+ kneeHintLerpSpeed: 10
+ maxFootTiltDegrees: 35
+ maxFootYawDegrees: 18
+ raySphereRadiusMul: 0.3
+ footHeightOffsetMul: 0.2
+ stepTriggerMul: 0.08
+ strideScaleMul: 0.06
+ stepHeightMul: 0.18
+ stepDurSlowMul: 0.3
+ stepDurFastMul: 0.18
+ fastSpeedMul: 1.2
+ stepArcLiftExp: 0.6
+ stepArcDropExp: 1.4
+ stepHeightMinFraction: 0.4
+ idleSpeedThreshold: 0.05
+ idleBoostFraction: 0.5
+ maxPlantedYawDegrees: 35
+ idealSideEnforceFraction: 0.3
+ stepTargetSideFraction: 0.15
+ footSideEnforceFraction: 0.2
+ maxVerticalDriftFraction: 0.25
+ kneeForwardPushFraction: 0.4
+ kneeMinSideFraction: 0.05
+ bodyFwdHipsWeight: 3
+ bodyFwdChestWeight: 2
+ bodyFwdHeadWeight: 1
+ hipBobFraction: 0.02
+ stanceWidth: 0
+ hipToFoot: 0
+ leftLegLen: 0
+ rightLegLen: 0
+ leftThighLen: 0
+ leftShinLen: 0
+ rightThighLen: 0
+ rightShinLen: 0
+ footLength: 0
+ ankleHeight: 0
+ stepTriggerDist: 0
+ strideScale: 0
+ stepHeightCalc: 0
+ stepDurSlow: 0
+ stepDurFast: 0
+ raySphereRadius: 0
+ footHeightOffset: 0
+ fastSpeedRef: 0
LocalCharacterDriver:
LocalPlayer: {fileID: 4587526044252889482}
- characterController: {fileID: 5548795088377030369}
+ characterController: {fileID: 0}
bottomPointLocalSpace: {x: 0, y: 0, z: 0}
LastBottomPoint: {x: 0, y: 0, z: 0}
groundedPlayer: 0
@@ -590,9 +655,16 @@ MonoBehaviour:
LastWasGrounded: 1
IsFalling: 0
IsJumpHeld: 0
+ IsDescendHeld: 0
HasJumpAction: 0
jumpHeight: 1
currentVerticalSpeed: 0
+ landingDescentSpeed: 15
+ landingRecoverySpeed: 6
+ landingImpactScale: 0.06
+ maxLandingCrouchEffect: 0.35
+ coyoteTimeDuration: 0.15
+ fallingGracePeriod: 0.1
Rotation: {x: 0, y: 0}
RotationSpeed: 200
HasEvents: 0
@@ -600,6 +672,7 @@ MonoBehaviour:
CurrentPosition: {x: 0, y: 0, z: 0}
CurrentRotation: {x: 0, y: 0, z: 0, w: 0}
Flags: 0
+ radius: 0
k__BackingField: 0
k__BackingField: 0
CrouchBlend: 1
@@ -617,6 +690,9 @@ MonoBehaviour:
m_Bits: 456
maxDownProbe: 3
maxUpProbe: 1
+ DebugDrawGizmos: 1
+ DebugPointRadius: 0.03
+ DebugAxisLength: 0.12
LocalAnimatorDriver:
basisAnimatorVariableApply:
Animator: {fileID: 0}
@@ -680,7 +756,6 @@ MonoBehaviour:
centerBias: 2.5
perEyeVarianceDeg: 0.4
occasionalCenterReturn: 1
- HasEyeSchedule: 0
LocalHandDriver:
LeftHand:
ThumbPercentage: {x: 0, y: 0}
@@ -715,148 +790,7 @@ MonoBehaviour:
RightMiddle: []
RightRing: []
RightLittle: []
- LastLeftThumbPercentage: {x: -1.1, y: -1.1}
- LastLeftIndexPercentage: {x: -1.1, y: -1.1}
- LastLeftMiddlePercentage: {x: -1.1, y: -1.1}
- LastLeftRingPercentage: {x: -1.1, y: -1.1}
- LastLeftLittlePercentage: {x: -1.1, y: -1.1}
- LastRightThumbPercentage: {x: -1.1, y: -1.1}
- LastRightIndexPercentage: {x: -1.1, y: -1.1}
- LastRightMiddlePercentage: {x: -1.1, y: -1.1}
- LastRightRingPercentage: {x: -1.1, y: -1.1}
- LastRightLittlePercentage: {x: -1.1, y: -1.1}
- LeftThumbAdditional:
- PoseData:
- LeftThumb: []
- LeftIndex: []
- LeftMiddle: []
- LeftRing: []
- LeftLittle: []
- RightThumb: []
- RightIndex: []
- RightMiddle: []
- RightRing: []
- RightLittle: []
- Coord: {x: 0, y: 0}
- LeftIndexAdditional:
- PoseData:
- LeftThumb: []
- LeftIndex: []
- LeftMiddle: []
- LeftRing: []
- LeftLittle: []
- RightThumb: []
- RightIndex: []
- RightMiddle: []
- RightRing: []
- RightLittle: []
- Coord: {x: 0, y: 0}
- LeftMiddleAdditional:
- PoseData:
- LeftThumb: []
- LeftIndex: []
- LeftMiddle: []
- LeftRing: []
- LeftLittle: []
- RightThumb: []
- RightIndex: []
- RightMiddle: []
- RightRing: []
- RightLittle: []
- Coord: {x: 0, y: 0}
- LeftRingAdditional:
- PoseData:
- LeftThumb: []
- LeftIndex: []
- LeftMiddle: []
- LeftRing: []
- LeftLittle: []
- RightThumb: []
- RightIndex: []
- RightMiddle: []
- RightRing: []
- RightLittle: []
- Coord: {x: 0, y: 0}
- LeftLittleAdditional:
- PoseData:
- LeftThumb: []
- LeftIndex: []
- LeftMiddle: []
- LeftRing: []
- LeftLittle: []
- RightThumb: []
- RightIndex: []
- RightMiddle: []
- RightRing: []
- RightLittle: []
- Coord: {x: 0, y: 0}
- RightThumbAdditional:
- PoseData:
- LeftThumb: []
- LeftIndex: []
- LeftMiddle: []
- LeftRing: []
- LeftLittle: []
- RightThumb: []
- RightIndex: []
- RightMiddle: []
- RightRing: []
- RightLittle: []
- Coord: {x: 0, y: 0}
- RightIndexAdditional:
- PoseData:
- LeftThumb: []
- LeftIndex: []
- LeftMiddle: []
- LeftRing: []
- LeftLittle: []
- RightThumb: []
- RightIndex: []
- RightMiddle: []
- RightRing: []
- RightLittle: []
- Coord: {x: 0, y: 0}
- RightMiddleAdditional:
- PoseData:
- LeftThumb: []
- LeftIndex: []
- LeftMiddle: []
- LeftRing: []
- LeftLittle: []
- RightThumb: []
- RightIndex: []
- RightMiddle: []
- RightRing: []
- RightLittle: []
- Coord: {x: 0, y: 0}
- RightRingAdditional:
- PoseData:
- LeftThumb: []
- LeftIndex: []
- LeftMiddle: []
- LeftRing: []
- LeftLittle: []
- RightThumb: []
- RightIndex: []
- RightMiddle: []
- RightRing: []
- RightLittle: []
- Coord: {x: 0, y: 0}
- RightLittleAdditional:
- PoseData:
- LeftThumb: []
- LeftIndex: []
- LeftMiddle: []
- LeftRing: []
- LeftLittle: []
- RightThumb: []
- RightIndex: []
- RightMiddle: []
- RightRing: []
- RightLittle: []
- Coord: {x: 0, y: 0}
LerpSpeed: 22
- Poses: []
LocalVisemeDriver:
smoothAmount: 70
HasViseme:
@@ -873,10 +807,13 @@ MonoBehaviour:
BlendShapeInfos: []
HasJob: 0
blendShapeCount: 0
+ UseOpenLipSync: 0
+ EligibleForOpenLipSync: 0
phonemeBlendShapeTable: []
WasSuccessful: 0
HashInstanceID: -1
uLipSyncEnabledState: 1
+ InVisemeRange: 1
FacialBlinkDriver:
Override: 0
meshRenderer: {fileID: 0}
@@ -894,8 +831,35 @@ MonoBehaviour:
NextBlinkTime: 0
BlinkStartTime: 0
OpenStartTime: 0
---- !u!143 &5548795088377030369
-CharacterController:
+--- !u!54 &8369693708823570861
+Rigidbody:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8538648612911191307}
+ serializedVersion: 5
+ m_Mass: 1
+ m_LinearDamping: 0
+ m_AngularDamping: 0.05
+ m_CenterOfMass: {x: 0, y: 0, z: 0}
+ m_InertiaTensor: {x: 1, y: 1, z: 1}
+ m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ImplicitCom: 1
+ m_ImplicitTensor: 1
+ m_UseGravity: 0
+ m_IsKinematic: 1
+ m_Interpolate: 0
+ m_Constraints: 0
+ m_CollisionDetection: 0
+--- !u!136 &3011869999757657
+CapsuleCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
@@ -909,13 +873,32 @@ CharacterController:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
+ m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
- serializedVersion: 3
+ serializedVersion: 2
+ m_Radius: 0.3
m_Height: 2
- m_Radius: 0.15
- m_SlopeLimit: 65
- m_StepOffset: 0.5
- m_SkinWidth: 0.01
- m_MinMoveDistance: 0.001
+ m_Direction: 1
m_Center: {x: 0, y: 0, z: 0}
+--- !u!114 &7825922350359905173
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8538648612911191307}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 3f50f4d9a7b7b5c488ef048f8e073021, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Basis Framework::Basis.Scripts.BasisCharacterController.BasisKinematicCharacterController
+ _height: 2
+ _radius: 0.3
+ _center: {x: 0, y: 1, z: 0}
+ _skinWidth: 0.01
+ _stepOffset: 0.3
+ _minMoveDistance: 0
+ _slopeLimit: 45
+ _detectCollisions: 1
+ _gravityDirection: {x: 0, y: -1, z: 0}
From 2bbfe5a6bd95b9931dea31b425431c6522ce4c5f Mon Sep 17 00:00:00 2001
From: Sketch <109103755+SketchFoxsky@users.noreply.github.com>
Date: Wed, 8 Apr 2026 16:29:14 -0400
Subject: [PATCH 2/5] Update BasisLocalCharacterDriver.cs
Removed Legacy Character Controller check as it is not needed.
---
.../Local/BasisLocalCharacterDriver.cs | 42 -------------------
1 file changed, 42 deletions(-)
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs
index c92153f96f..55e9052074 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs
@@ -175,53 +175,11 @@ public void DeInitalize()
HasEvents = false;
}
}
- ///
- /// Ensures the KCC component exists on the player GameObject.
- /// Migrates from Unity's CharacterController if one is still present on the prefab.
- ///
- private void EnsureKCC()
- {
- if (characterController != null) return;
-
- GameObject go = BasisLocalPlayerTransform.gameObject;
-
- // Check if already has KCC
- characterController = go.GetComponent();
- if (characterController != null) return;
-
- // Migrate from legacy CharacterController if present
- CharacterController legacy = go.GetComponent();
- float oldHeight = 2f;
- float oldRadius = 0.3f;
- Vector3 oldCenter = new Vector3(0f, 1f, 0f);
- float oldSkinWidth = 0.01f;
- float oldStepOffset = 0.3f;
- float oldSlopeLimit = 45f;
- if (legacy != null)
- {
- oldHeight = legacy.height;
- oldRadius = legacy.radius;
- oldCenter = legacy.center;
- oldSkinWidth = legacy.skinWidth;
- oldStepOffset = legacy.stepOffset;
- oldSlopeLimit = legacy.slopeLimit;
- UnityEngine.Object.DestroyImmediate(legacy);
- }
-
- characterController = go.AddComponent();
- characterController.height = oldHeight;
- characterController.radius = oldRadius;
- characterController.center = oldCenter;
- characterController.skinWidth = oldSkinWidth;
- characterController.stepOffset = oldStepOffset;
- characterController.slopeLimit = oldSlopeLimit;
- }
public void Initialize(BasisLocalPlayer localPlayer)
{
LocalPlayer = localPlayer;
BasisLocalPlayerTransform = localPlayer.transform;
LocalAnimatorDriver = localPlayer.LocalAnimatorDriver;
- EnsureKCC();
characterController.minMoveDistance = 0;
characterController.skinWidth = 0.01f;
characterController.OnKCCColliderHit = OnKCCHit;
From 98b62f8783b90efa916074d2891cfe448424ca63 Mon Sep 17 00:00:00 2001
From: Sketch <109103755+SketchFoxsky@users.noreply.github.com>
Date: Wed, 8 Apr 2026 17:03:37 -0400
Subject: [PATCH 3/5] Changed KCC Initialization, Exposed some fields
---
.../Local/BasisLocalCharacterDriver.cs | 1 +
.../BasisKinematicCharacterController.cs | 33 +++++++------------
2 files changed, 12 insertions(+), 22 deletions(-)
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs
index f4f18084db..f7eec0675b 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs
@@ -179,6 +179,7 @@ public void Initialize(BasisLocalPlayer localPlayer)
LocalPlayer = localPlayer;
BasisLocalPlayerTransform = localPlayer.transform;
LocalAnimatorDriver = localPlayer.LocalAnimatorDriver;
+ characterController.PlayerInitialize();
characterController.minMoveDistance = 0;
characterController.skinWidth = 0.01f;
characterController.OnKCCColliderHit = OnKCCHit;
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs
index 7b93998063..99fbeb20da 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs
@@ -33,9 +33,9 @@ public class BasisKinematicCharacterController : MonoBehaviour
[SerializeField] private Vector3 _gravityDirection = Vector3.down;
// Runtime Info
- private Rigidbody _rigidbody;
- private CapsuleCollider _capsule;
- private bool _isGrounded;
+ public Rigidbody _rigidbody;
+ public CapsuleCollider _capsule;
+ public bool isGrounded;
private Vector3 _groundNormal = Vector3.up;
private CollisionFlags _lastFlags;
@@ -117,8 +117,6 @@ public bool detectCollisions
}
}
- public bool isGrounded => _isGrounded;
-
///
/// The ground surface normal from the last Move() call. Only valid when isGrounded is true.
///
@@ -151,20 +149,11 @@ public Vector3 UpDirection
#region UNITY LIFECYCLE
- private void Awake()
+ public void PlayerInitialize()
{
// Ensure gravity direction is valid (may be zero if deserialized from a fresh component)
if (_gravityDirection.sqrMagnitude < 0.0001f)
_gravityDirection = Vector3.down;
-
- _rigidbody = GetComponent();
- _rigidbody.isKinematic = true;
- _rigidbody.useGravity = false;
- _rigidbody.interpolation = RigidbodyInterpolation.None;
- _rigidbody.collisionDetectionMode = CollisionDetectionMode.Discrete;
- _rigidbody.constraints = RigidbodyConstraints.FreezeAll;
-
- _capsule = GetComponent();
SyncCapsule();
}
@@ -192,7 +181,7 @@ public CollisionFlags Move(Vector3 motion)
if (!enabled || !_detectCollisions)
{
transform.position += motion;
- _isGrounded = false;
+ isGrounded = false;
return _lastFlags;
}
@@ -210,7 +199,7 @@ public CollisionFlags Move(Vector3 motion)
bool movingDown = verticalComponent < 0f;
// ── Grounded behaviour: slope projection + ground snap ──────
- if (_isGrounded && movingDown)
+ if (isGrounded && movingDown)
{
float groundDot = Vector3.Dot(_groundNormal, up);
bool walkableSlope = groundDot >= cosSlope;
@@ -238,7 +227,7 @@ public CollisionFlags Move(Vector3 motion)
pos = SimpleMove(pos, horizontalMotion, ref _lastFlags, up, cosSlope, isHorizontal: true);
// If grounded and slide made little horizontal progress, try stepping up
- if (_isGrounded && _stepOffset > 0f && movingDown)
+ if (isGrounded && _stepOffset > 0f && movingDown)
{
Vector3 traveled = pos - beforeSlide;
Vector3 horizontalTraveled = traveled - up * Vector3.Dot(traveled, up);
@@ -266,7 +255,7 @@ public CollisionFlags Move(Vector3 motion)
transform.position = pos;
// snap to ground
- if (_isGrounded && verticalComponent <= 0f)
+ if (isGrounded && verticalComponent <= 0f)
{
pos = GroundSnap(pos, up, cosSlope);
transform.position = pos;
@@ -515,7 +504,7 @@ private void GroundProbe()
{
if (!_detectCollisions || !enabled)
{
- _isGrounded = false;
+ isGrounded = false;
_groundNormal = UpDirection;
return;
}
@@ -542,7 +531,7 @@ private void GroundProbe()
QueryTriggerInteraction.Ignore
);
- _isGrounded = false;
+ isGrounded = false;
_groundNormal = up;
float cosSlope = Mathf.Cos(_slopeLimit * Mathf.Deg2Rad);
float closestDist = float.MaxValue;
@@ -554,7 +543,7 @@ private void GroundProbe()
if (dotUp >= cosSlope && _hitBuffer[i].distance < closestDist)
{
closestDist = _hitBuffer[i].distance;
- _isGrounded = true;
+ isGrounded = true;
_groundNormal = _hitBuffer[i].normal;
}
}
From 01aca13c4f1fd625e4ff62a0cd136969f22d0ec1 Mon Sep 17 00:00:00 2001
From: Sketch <109103755+SketchFoxsky@users.noreply.github.com>
Date: Thu, 9 Apr 2026 17:33:00 -0400
Subject: [PATCH 4/5] github please what is actually going on
Changes made previously were not pushed or git stroked out
IK changes from Developer branch broke need to rewire it!
- Exposed more fields
- Properly Cache Collision Masks
- Cache transform data so its not being called a shit ton
---
.../BasisKinematicCharacterController.cs | 109 +++++++++---------
.../Prefabs/Players/LocalPlayer.prefab | 14 ++-
2 files changed, 67 insertions(+), 56 deletions(-)
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs
index 99fbeb20da..20a9180b3f 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs
@@ -6,31 +6,25 @@ namespace Basis.Scripts.BasisCharacterController
/// A kinematic character controller that replaces Unity's built-in CharacterController.
/// Uses a CapsuleCollider + kinematic Rigidbody so that the player can be rotated
/// freely, enabling custom gravity directions (not Y-axis locked).
- ///
- /// TODO
- /// Add Rotate to Gravity
- /// Add Flight/Noclip exposure for worlds
- /// MAYBE add native swimming!
- ///
///
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(CapsuleCollider))]
public class BasisKinematicCharacterController : MonoBehaviour
{
// Capsule Collider
- [SerializeField] private float _height = 2f;
- [SerializeField] private float _radius = 0.3f;
- [SerializeField] private Vector3 _center = new Vector3(0f, 1f, 0f);
+ public float _height = 2f;
+ public float _radius = 0.3f;
+ public Vector3 _center = new Vector3(0f, 1f, 0f);
// CharacterController Parameters
- [SerializeField] private float _skinWidth = 0.01f;
- [SerializeField] private float _stepOffset = 0.3f;
- [SerializeField] private float _minMoveDistance = 0f;
- [SerializeField] private float _slopeLimit = 45f;
- [SerializeField] private bool _detectCollisions = true;
+ public float _skinWidth = 0.01f;
+ public float _stepOffset = 0.3f;
+ public float _minMoveDistance = 0f;
+ public float _slopeLimit = 45f;
+ public bool _detectCollisions = true;
- //Gravity
- [SerializeField] private Vector3 _gravityDirection = Vector3.down;
+ // Gravity
+ public Vector3 _gravityDirection = Vector3.down;
// Runtime Info
public Rigidbody _rigidbody;
@@ -40,6 +34,9 @@ public class BasisKinematicCharacterController : MonoBehaviour
private CollisionFlags _lastFlags;
// Collision
+ // 16 hits/overlaps covers worst-case scenarios for capsule casts in typical
+ // game environments. NonAlloc silently drops excess results but 16 is generous —
+ // most casts hit 1-3 colliders. Increase only if levels have extreme collider density.
private const int MaxHits = 16;
private const int MaxOverlaps = 16;
private const int MaxDepenetrationIterations = 4;
@@ -50,6 +47,13 @@ public class BasisKinematicCharacterController : MonoBehaviour
public delegate void KCCColliderHit(KCCHitInfo hit);
public KCCColliderHit OnKCCColliderHit;
+ // Cached collision mask — rebuilt once per Move() instead of per-cast.
+ private int _collisionMask;
+ private bool _collisionMaskDirty = true;
+
+ // Cached per-Move() to avoid repeated Transform property access.
+ private Quaternion _cachedRotation;
+
//Public Get Set
public float height
@@ -112,8 +116,7 @@ public bool detectCollisions
set
{
_detectCollisions = value;
- if (_capsule != null)
- _capsule.enabled = value;
+ _capsule.enabled = value;
}
}
@@ -159,7 +162,6 @@ public void PlayerInitialize()
private void SyncCapsule()
{
- if (_capsule == null) return;
_capsule.direction = 1; // Y-axis
_capsule.center = _center;
_capsule.radius = _radius;
@@ -177,6 +179,7 @@ private void SyncCapsule()
public CollisionFlags Move(Vector3 motion)
{
_lastFlags = CollisionFlags.None;
+ _collisionMaskDirty = true; // Refresh mask once per Move()
if (!enabled || !_detectCollisions)
{
@@ -185,9 +188,12 @@ public CollisionFlags Move(Vector3 motion)
return _lastFlags;
}
+ // Cache rotation once per Move() — used by GetCapsuleEnds, GroundProbe, Depenetrate.
+ transform.GetPositionAndRotation(out Vector3 pos, out _cachedRotation);
+
if (motion.sqrMagnitude < _minMoveDistance * _minMoveDistance)
{
- GroundProbe();
+ GroundProbe(pos);
return _lastFlags;
}
@@ -218,8 +224,6 @@ public CollisionFlags Move(Vector3 motion)
}
}
- Vector3 pos = transform.position;
-
// ── Horizontal movement with step-up fallback ───────────────
if (horizontalMotion.sqrMagnitude > 0.00001f)
{
@@ -252,21 +256,20 @@ public CollisionFlags Move(Vector3 motion)
pos = SimpleMove(pos, verticalMotion, ref _lastFlags, up, cosSlope, isHorizontal: false);
}
- transform.position = pos;
-
// snap to ground
if (isGrounded && verticalComponent <= 0f)
{
pos = GroundSnap(pos, up, cosSlope);
- transform.position = pos;
}
// Depenetration if clipping with a collider
pos = Depenetrate(pos);
+
+ // Single transform write per Move() call
transform.position = pos;
// Ground probe
- GroundProbe();
+ GroundProbe(pos);
return _lastFlags;
}
@@ -332,10 +335,6 @@ private Vector3 SimpleMove(Vector3 position, Vector3 motion, ref CollisionFlags
return position;
}
- // Step Offset
-
- // kill me please this took a while to debug, had to look at old braxy tutorials :)
-
private bool TryStepUp(ref Vector3 pos, Vector3 horizontalMotion, Vector3 up, float cosSlope)
{
float castRadius = _radius - _skinWidth;
@@ -420,10 +419,6 @@ private bool TryStepUp(ref Vector3 pos, Vector3 horizontalMotion, Vector3 up, fl
return true;
}
- //Ground Snapping
-
- // Add ground Friction, its almost 1am im not doing that tonight
-
///
/// When grounded and not jumping, cast downward to anchor the character
/// to the ground surface. Prevents floating over small bumps and slopes.
@@ -461,8 +456,6 @@ private Vector3 GroundSnap(Vector3 position, Vector3 up, float cosSlope)
return position;
}
- // Depenetration Helper
-
private Vector3 Depenetrate(Vector3 position)
{
for (int iter = 0; iter < MaxDepenetrationIterations; iter++)
@@ -483,7 +476,7 @@ private Vector3 Depenetrate(Vector3 position)
if (other == _capsule) continue;
if (Physics.ComputePenetration(
- _capsule, position, transform.rotation,
+ _capsule, position, _cachedRotation,
other, other.transform.position, other.transform.rotation,
out Vector3 dir, out float dist))
{
@@ -498,9 +491,7 @@ private Vector3 Depenetrate(Vector3 position)
return position;
}
- // Ground Detection
-
- private void GroundProbe()
+ private void GroundProbe(Vector3 pos)
{
if (!_detectCollisions || !enabled)
{
@@ -510,10 +501,9 @@ private void GroundProbe()
}
Vector3 up = UpDirection;
- Vector3 pos = transform.position;
// Cast a small sphere downward from the bottom of the capsule
- Vector3 worldCenter = pos + transform.rotation * _center;
+ Vector3 worldCenter = pos + _cachedRotation * _center;
float halfHeight = (_height * 0.5f) - _radius;
Vector3 bottom = worldCenter - up * halfHeight;
@@ -554,9 +544,8 @@ private void GroundProbe()
private void GetCapsuleEnds(Vector3 position, out Vector3 point1, out Vector3 point2)
{
- Quaternion rot = transform.rotation;
- Vector3 worldCenter = position + rot * _center;
- Vector3 capsuleUp = rot * Vector3.up;
+ Vector3 worldCenter = position + _cachedRotation * _center;
+ Vector3 capsuleUp = _cachedRotation * Vector3.up;
float halfHeight = (_height * 0.5f) - _radius;
if (halfHeight < 0f) halfHeight = 0f;
point1 = worldCenter + capsuleUp * halfHeight;
@@ -565,14 +554,28 @@ private void GetCapsuleEnds(Vector3 position, out Vector3 point1, out Vector3 po
private int GetCollisionMask()
{
- int layer = gameObject.layer;
- int mask = 0;
- for (int i = 0; i < 32; i++)
+ if (_collisionMaskDirty)
{
- if (!Physics.GetIgnoreLayerCollision(layer, i))
- mask |= (1 << i);
+ int layer = gameObject.layer;
+ int mask = 0;
+ for (int i = 0; i < 32; i++)
+ {
+ if (!Physics.GetIgnoreLayerCollision(layer, i))
+ mask |= (1 << i);
+ }
+ _collisionMask = mask;
+ _collisionMaskDirty = false;
}
- return mask;
+ return _collisionMask;
+ }
+
+ ///
+ /// Call if the object's layer or physics layer collision matrix changes at runtime.
+ /// The mask is automatically refreshed at the start of each Move() call.
+ ///
+ public void InvalidateCollisionMask()
+ {
+ _collisionMaskDirty = true;
}
private bool FindClosestHit(int hitCount, out RaycastHit closest)
@@ -613,9 +616,9 @@ private void FireHitCallback(RaycastHit hit, Vector3 moveDir, float moveDist)
info.moveLength = moveDist;
OnKCCColliderHit(info);
}
- }
- #endregion
+ #endregion
+ }
///
/// Hit information passed to the KCC collision callback, mirroring ControllerColliderHit.
diff --git a/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab b/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab
index 019825774e..51abb167e3 100644
--- a/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab
+++ b/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab
@@ -332,6 +332,8 @@ MonoBehaviour:
UnMutedMutedIconColorActive: {r: 1, g: 1, b: 1, a: 0.67058825}
UnMutedMutedIconColorInactive: {r: 0.5, g: 0.5, b: 0.5, a: 0.67058825}
MutedColor: {r: 1, g: 0, b: 0, a: 0.67058825}
+ ShoutColorActive: {r: 1, g: 0.92156863, b: 0.015686275, a: 1}
+ ShoutColorInactive: {r: 0.6, g: 0.6, b: 0, a: 1}
StartingScale: {x: 0, y: 0, z: 0}
largerScale: {x: 0, y: 0, z: 0}
MuteSound: {fileID: 8300000, guid: 5b9ed5013ea1941499840ac97ad2b8e9, type: 3}
@@ -340,6 +342,8 @@ MonoBehaviour:
duration: 0.35
halfDuration: 0
CameraDriver: {fileID: 22857711927985140}
+ avatarPreviewDriver:
+ CameraFieldOfView: 40
--- !u!114 &3806033592985501336
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -631,6 +635,7 @@ MonoBehaviour:
rightShinLen: 0
footLength: 0
ankleHeight: 0
+ upperLegToFootVertical: 0
stepTriggerDist: 0
strideScale: 0
stepHeightCalc: 0
@@ -641,7 +646,7 @@ MonoBehaviour:
fastSpeedRef: 0
LocalCharacterDriver:
LocalPlayer: {fileID: 4587526044252889482}
- characterController: {fileID: 0}
+ characterController: {fileID: 7825922350359905173}
bottomPointLocalSpace: {x: 0, y: 0, z: 0}
LastBottomPoint: {x: 0, y: 0, z: 0}
groundedPlayer: 0
@@ -666,7 +671,6 @@ MonoBehaviour:
coyoteTimeDuration: 0.15
fallingGracePeriod: 0.1
Rotation: {x: 0, y: 0}
- RotationSpeed: 200
HasEvents: 0
pushPower: 1
CurrentPosition: {x: 0, y: 0, z: 0}
@@ -678,7 +682,7 @@ MonoBehaviour:
CrouchBlend: 1
CrouchBlendDelta: 0
CanPushRigidbodys: 0
- BasisLocalPlayerTransform: {fileID: 0}
+ BasisLocalPlayerTransform: {fileID: 303201899784742928}
CurrentSpeed: 0
LocalSeatDriver:
UseDefaultMasking: 1
@@ -809,6 +813,7 @@ MonoBehaviour:
blendShapeCount: 0
UseOpenLipSync: 0
EligibleForOpenLipSync: 0
+ TrackedAudioSource: {fileID: 0}
phonemeBlendShapeTable: []
WasSuccessful: 0
HashInstanceID: -1
@@ -902,3 +907,6 @@ MonoBehaviour:
_slopeLimit: 45
_detectCollisions: 1
_gravityDirection: {x: 0, y: -1, z: 0}
+ _rigidbody: {fileID: 8369693708823570861}
+ _capsule: {fileID: 3011869999757657}
+ isGrounded: 0
From 1d19ce86ba3e3ac7102969ae51d478167206275a Mon Sep 17 00:00:00 2001
From: Sketch <109103755+SketchFoxsky@users.noreply.github.com>
Date: Thu, 9 Apr 2026 19:10:31 -0400
Subject: [PATCH 5/5] I hate Githubbbbbbb
IK changes were regressed during a previous merge, These will also be cherry picked later for an update upstream.
---
.../Drivers/Local/BasisLocalAvatarDriver.cs | 3 +-
.../Drivers/Local/BasisLocalRigDriver.cs | 16 ++---
.../Local/BasisLocalVirtualSpineDriver.cs | 67 ++++++++++---------
3 files changed, 45 insertions(+), 41 deletions(-)
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalAvatarDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalAvatarDriver.cs
index 4ad01356d7..679b5b70e8 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalAvatarDriver.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalAvatarDriver.cs
@@ -509,8 +509,9 @@ public void GetBoneRotAndPos(quaternion RootRotation, Animator anim, HumanBodyBo
/// Offset vector applied to the base position.
public float3 CalculateFallbackOffset(HumanBodyBones bone, float fallbackHeight, float3 heightPercentage)
{
+ Vector3 playerUp = BasisLocalPlayer.localToWorldMatrix.MultiplyVector(Vector3.up).normalized;
Vector3 height = fallbackHeight * heightPercentage;
- return bone == HumanBodyBones.Hips ? math.mul(height, -Vector3.up) : math.mul(height, Vector3.up);
+ return bone == HumanBodyBones.Hips ? math.mul(height, -playerUp) : math.mul(height, playerUp);
}
///
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalRigDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalRigDriver.cs
index 10787bacf7..3797b8171d 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalRigDriver.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalRigDriver.cs
@@ -309,6 +309,7 @@ public void SimulateIKDestinations(float deltaTime)
}
timeAccumulator += Mathf.Max(deltaTime, 1e-6f);
+ Vector3 cachedPlayerUp = localPlayer.LocalCharacterDriver.UpDirection;
BasisFullBodyData data = BasisFullIKConstraint.data;
@@ -375,7 +376,7 @@ public void SimulateIKDestinations(float deltaTime)
hipsRot = EuroRot[S_Hips] ? fRotHips.Filter(hipsRot, timeAccumulator) : FallbackRot(ref sRotHips, hipsRot, deltaTime);
}
- hipsPos.y -= localPlayer.LocalCharacterDriver.landingCrouchEffect;
+ hipsPos -= cachedPlayerUp * localPlayer.LocalCharacterDriver.landingCrouchEffect;
data.PositionHips = hipsPos;
data.RotationHips = hipsRot;
@@ -525,9 +526,7 @@ public void SimulateIKDestinations(float deltaTime)
bool hipsHaveTracker = BasisLocalBoneDriver.HipsControl.HasTracked == BasisHasTracked.HasTracker;
if (!hipsHaveTracker)
{
- data.PositionHips = new Vector3(data.PositionHips.x,
- data.PositionHips.y + footDriver.ComputeHipBob() * footIKBlendWeight,
- data.PositionHips.z);
+ data.PositionHips += cachedPlayerUp * (footDriver.ComputeHipBob() * footIKBlendWeight);
}
}
@@ -568,7 +567,7 @@ public void SimulateIKDestinations(float deltaTime)
}
else if (footIKBlendWeightLeft > 0.001f && footDriverReady)
{
- Quaternion targetRotL = ComputeKneeHintRotation(data.PositionHips, data.LeftFootPosition, footDriver.LeftKneeHint);
+ Quaternion targetRotL = ComputeKneeHintRotation(data.PositionHips, data.LeftFootPosition, footDriver.LeftKneeHint, cachedPlayerUp);
float kneeRotAlpha = 1f - Mathf.Exp(-8f * deltaTime);
smoothedLeftKneeRot = Quaternion.Slerp(smoothedLeftKneeRot, targetRotL, kneeRotAlpha);
data.PositionLeftLowerLeg = footDriver.LeftKneeHint;
@@ -602,7 +601,7 @@ public void SimulateIKDestinations(float deltaTime)
}
else if (footIKBlendWeightRight > 0.001f && footDriverReady)
{
- Quaternion targetRotR = ComputeKneeHintRotation(data.PositionHips, data.RightFootPosition, footDriver.RightKneeHint);
+ Quaternion targetRotR = ComputeKneeHintRotation(data.PositionHips, data.RightFootPosition, footDriver.RightKneeHint, cachedPlayerUp);
float kneeRotAlpha = 1f - Mathf.Exp(-8f * deltaTime);
smoothedRightKneeRot = Quaternion.Slerp(smoothedRightKneeRot, targetRotR, kneeRotAlpha);
@@ -743,6 +742,7 @@ public void SimulateIKDestinations(float deltaTime)
data.KneeBendPrefRight = (hipsRot * Vector3.right);
data.SpineBendNormal = (fwd * spineBendNormalWeights.x + outR * spineBendNormalWeights.y + up * spineBendNormalWeights.z).normalized;
+ data.PlayerUp = cachedPlayerUp;
// Commit & evaluate
BasisFullIKConstraint.data = data;
@@ -1094,7 +1094,7 @@ private static Vector3 ComputeKneeBendNormal(Vector3 hip, Vector3 foot, Vector3
/// Forward = knee→foot direction, Up = derived from the bend plane.
/// This prevents snapping that occurs with Quaternion.identity.
///
- private static Quaternion ComputeKneeHintRotation(Vector3 hip, Vector3 foot, Vector3 kneeHint)
+ private static Quaternion ComputeKneeHintRotation(Vector3 hip, Vector3 foot, Vector3 kneeHint, Vector3 playerUp)
{
Vector3 kneeToFoot = foot - kneeHint;
Vector3 kneeToHip = hip - kneeHint;
@@ -1110,7 +1110,7 @@ private static Quaternion ComputeKneeHintRotation(Vector3 hip, Vector3 foot, Vec
Vector3 up = Vector3.Cross(fwd, bendNormal);
if (up.sqrMagnitude < 1e-8f)
- up = Vector3.up;
+ up = playerUp;
else
up.Normalize();
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalVirtualSpineDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalVirtualSpineDriver.cs
index 3a8a202e79..0cb3888ec9 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalVirtualSpineDriver.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalVirtualSpineDriver.cs
@@ -137,6 +137,11 @@ public void OnSimulate()
float dt = Time.deltaTime;
Matrix4x4 parentMatrix = BasisLocalPlayer.localToWorldMatrix;
+ // OutGoingData is in local space where Y is always the character's up axis,
+ // regardless of world orientation. Use Vector3.up for all local-space operations.
+ // (World-space player up is handled by the rig driver / FBIK animation job.)
+ Vector3 worldUp = Vector3.up;
+
// =========================
// 1) HEAD & NECK (top cues)
// =========================
@@ -146,27 +151,19 @@ public void OnSimulate()
neck.OutGoingData.rotation = SmoothSlerp(neck.OutGoingData.rotation, head.OutGoingData.rotation, NeckRotationSpeed, dt);
// Positions for head/neck come from their tracker-driven targets + offsets
- ApplyPositionControl(head, parentMatrix, torsoLock: false);
- ApplyPositionControl(neck, parentMatrix, torsoLock: false);
+ ApplyPositionControl(head, parentMatrix, torsoLock: false, worldUp);
+ ApplyPositionControl(neck, parentMatrix, torsoLock: false, worldUp);
Vector3 neckPosWorld = neck.OutGoingData.position;
- // ===========================================
- // 2) HIPS: build from neck and preserved span
- // ===========================================
- // Determine a stable world up
- Vector3 worldUp = parentMatrix.MultiplyVector(Vector3.up).normalized;
- if (worldUp.sqrMagnitude < 1e-6f) worldUp = Vector3.up;
-
// Preserve total length neck→hips, except when overridden.
Vector3 idealHips = HipsFreezeToTpose ? hips.TposeLocalScaled.position : neckPosWorld - worldUp * _lenTotal;
// Add small forward bias using head yaw, which also applies to the hips, except when overridden.
- Quaternion headYaw = HipsFreezeToTpose ? Quaternion.identity : ExtractYawRotation(head.OutGoingData.rotation);
+ Quaternion headYaw = HipsFreezeToTpose ? Quaternion.identity : ExtractYawRotation(head.OutGoingData.rotation, worldUp);
idealHips += (headYaw * Vector3.forward) * (HipsForwardBias * BasisHeightDriver.AvatarToDefaultRatioScaledWithAvatarScale);
-
- // Blend XZ with tracked hips for authority retention
+ // Blend horizontal position with tracked hips for authority retention
Vector3 trackedHips = hips.Target.OutGoingData.position;
Vector3 blendedHips = idealHips;
if (HipsXZFollowBlend > 0f)
@@ -177,14 +174,14 @@ public void OnSimulate()
// Hips rotation follows head yaw, damped.
Quaternion hipsYawTarget = headYaw;
- hips.OutGoingData.rotation = ExtractYawRotation(SmoothSlerp(hips.OutGoingData.rotation, hipsYawTarget, HipsRotationSpeed, dt));
+ hips.OutGoingData.rotation = ExtractYawRotation(SmoothSlerp(hips.OutGoingData.rotation, hipsYawTarget, HipsRotationSpeed, dt), worldUp);
hips.OutGoingData.position = blendedHips;
hips.ApplyWorldAndLast(parentMatrix);
// =======================================================
// 3) Fill the middle: chest & spine positions and yaws
// =======================================================
- Quaternion neckYaw = ExtractYawRotation(neck.OutGoingData.rotation);
+ Quaternion neckYaw = ExtractYawRotation(neck.OutGoingData.rotation, worldUp);
Quaternion hipsYaw = hips.OutGoingData.rotation; // already yaw-only
Vector3 neckToHips = hips.OutGoingData.position - neck.OutGoingData.position;
@@ -193,8 +190,8 @@ public void OnSimulate()
if (distNeckToHips < 1e-5f)
{
// Guard: fall back to tracker-driven positions
- ApplyPositionControl(chest, parentMatrix, torsoLock: true);
- ApplyPositionControl(spine, parentMatrix, torsoLock: true);
+ ApplyPositionControl(chest, parentMatrix, torsoLock: true, worldUp);
+ ApplyPositionControl(spine, parentMatrix, torsoLock: true, worldUp);
}
else
{
@@ -211,15 +208,15 @@ public void OnSimulate()
// Smooth rotations
chest.OutGoingData.rotation = ExtractYawRotation(
- SmoothSlerp(chest.OutGoingData.rotation, chestYawTarget, ChestRotationSpeed, dt)
+ SmoothSlerp(chest.OutGoingData.rotation, chestYawTarget, ChestRotationSpeed, dt), worldUp
);
spine.OutGoingData.rotation = ExtractYawRotation(
- SmoothSlerp(spine.OutGoingData.rotation, spineYawTarget, SpineRotationSpeed, dt)
+ SmoothSlerp(spine.OutGoingData.rotation, spineYawTarget, SpineRotationSpeed, dt), worldUp
);
- // Apply positions with offsets (torsoLock removes vertical offset)
- ApplyPositionWithGivenBase(chest, parentMatrix, chestPos, torsoLock: true);
- ApplyPositionWithGivenBase(spine, parentMatrix, spinePos, torsoLock: true);
+ // Apply positions with offsets (torsoLock removes up-axis offset)
+ ApplyPositionWithGivenBase(chest, parentMatrix, chestPos, torsoLock: true, worldUp);
+ ApplyPositionWithGivenBase(spine, parentMatrix, spinePos, torsoLock: true, worldUp);
}
// Finalize head/neck
@@ -229,18 +226,21 @@ public void OnSimulate()
///
/// Applies tracker-driven position plus offset for a bone control,
- /// optionally locking vertical to TPose baseline and yaw-only rotation.
+ /// optionally locking the up-axis component to TPose baseline and yaw-only rotation.
///
- private void ApplyPositionControl(BasisLocalBoneControl boneControl, Matrix4x4 parentMatrix, bool torsoLock)
+ private void ApplyPositionControl(BasisLocalBoneControl boneControl, Matrix4x4 parentMatrix, bool torsoLock, Vector3 up)
{
Quaternion rot = boneControl.Target.OutGoingData.rotation;
- if (torsoLock) rot = ExtractYawRotation(rot);
+ if (torsoLock) rot = ExtractYawRotation(rot, up);
Vector3 localOffset = boneControl.ScaledOffset;
if (torsoLock) localOffset.y = 0f;
Vector3 desired = boneControl.Target.OutGoingData.position + (rot * localOffset);
- if (torsoLock) desired.y = boneControl.TposeLocalScaled.position.y;
+ if (torsoLock)
+ {
+ desired.y = boneControl.TposeLocalScaled.position.y;
+ }
boneControl.OutGoingData.position = desired;
boneControl.ApplyWorldAndLast(parentMatrix);
@@ -249,16 +249,19 @@ private void ApplyPositionControl(BasisLocalBoneControl boneControl, Matrix4x4 p
///
/// Applies position using a provided world base position and the control's yaw/offset rules.
///
- private void ApplyPositionWithGivenBase(BasisLocalBoneControl boneControl, Matrix4x4 parentMatrix, Vector3 basePositionWorld, bool torsoLock)
+ private void ApplyPositionWithGivenBase(BasisLocalBoneControl boneControl, Matrix4x4 parentMatrix, Vector3 basePositionWorld, bool torsoLock, Vector3 up)
{
Quaternion rot = boneControl.OutGoingData.rotation;
- if (torsoLock) rot = ExtractYawRotation(rot);
+ if (torsoLock) rot = ExtractYawRotation(rot, up);
Vector3 localOffset = boneControl.ScaledOffset;
if (torsoLock) localOffset.y = 0f;
Vector3 desired = basePositionWorld + (rot * localOffset);
- if (torsoLock) desired.y = boneControl.TposeLocalScaled.position.y;
+ if (torsoLock)
+ {
+ desired.y = boneControl.TposeLocalScaled.position.y;
+ }
boneControl.OutGoingData.position = desired;
boneControl.ApplyWorldAndLast(parentMatrix);
@@ -274,14 +277,14 @@ private static Quaternion SmoothSlerp(Quaternion current, Quaternion target, flo
}
///
- /// Extracts yaw-only rotation (around global up) from a full quaternion.
+ /// Extracts yaw-only rotation (around the given up axis) from a full quaternion.
///
- private static Quaternion ExtractYawRotation(Quaternion rotation)
+ private static Quaternion ExtractYawRotation(Quaternion rotation, Vector3 up)
{
Vector3 f = rotation * Vector3.forward;
- f.y = 0f;
+ f -= up * Vector3.Dot(f, up); // project onto plane perpendicular to up
if (f.sqrMagnitude < 1e-6f) f = Vector3.forward;
f.Normalize();
- return Quaternion.LookRotation(f, Vector3.up);
+ return Quaternion.LookRotation(f, up);
}
}