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/BasisLocalCharacterDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs index 227ace6eaf..7a856ccc80 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; @@ -103,6 +103,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. @@ -162,8 +179,10 @@ 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; if (!HasEvents) { HasEvents = true; @@ -175,11 +194,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) @@ -187,10 +205,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); } } @@ -263,7 +280,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; @@ -279,7 +296,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; @@ -387,16 +404,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; @@ -426,7 +447,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); @@ -482,7 +504,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/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); } } 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..20a9180b3f --- /dev/null +++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs @@ -0,0 +1,634 @@ +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). + /// + [RequireComponent(typeof(Rigidbody))] + [RequireComponent(typeof(CapsuleCollider))] + public class BasisKinematicCharacterController : MonoBehaviour + { + // Capsule Collider + public float _height = 2f; + public float _radius = 0.3f; + public Vector3 _center = new Vector3(0f, 1f, 0f); + + // CharacterController Parameters + public float _skinWidth = 0.01f; + public float _stepOffset = 0.3f; + public float _minMoveDistance = 0f; + public float _slopeLimit = 45f; + public bool _detectCollisions = true; + + // Gravity + public Vector3 _gravityDirection = Vector3.down; + + // Runtime Info + public Rigidbody _rigidbody; + public CapsuleCollider _capsule; + public bool isGrounded; + private Vector3 _groundNormal = Vector3.up; + 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; + 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; + + // 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 + { + 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; + _capsule.enabled = value; + } + } + + /// + /// 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 + + 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; + SyncCapsule(); + } + + private void SyncCapsule() + { + _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; + _collisionMaskDirty = true; // Refresh mask once per Move() + + if (!enabled || !_detectCollisions) + { + transform.position += motion; + isGrounded = false; + 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(pos); + 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; + } + } + + // ── 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); + } + + // snap to ground + if (isGrounded && verticalComponent <= 0f) + { + pos = GroundSnap(pos, up, cosSlope); + } + + // Depenetration if clipping with a collider + pos = Depenetrate(pos); + + // Single transform write per Move() call + transform.position = pos; + + // Ground probe + GroundProbe(pos); + + 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; + } + + 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; + } + + /// + /// 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; + } + + 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, _cachedRotation, + 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; + } + + private void GroundProbe(Vector3 pos) + { + if (!_detectCollisions || !enabled) + { + isGrounded = false; + _groundNormal = UpDirection; + return; + } + + Vector3 up = UpDirection; + + // Cast a small sphere downward from the bottom of the capsule + Vector3 worldCenter = pos + _cachedRotation * _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) + { + 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; + point2 = worldCenter - capsuleUp * halfHeight; + } + + private int GetCollisionMask() + { + if (_collisionMaskDirty) + { + 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 _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) + { + 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..51abb167e3 100644 --- a/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab +++ b/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab @@ -327,11 +327,13 @@ 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} 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 @@ -484,7 +488,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 +526,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 +549,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 +583,70 @@ 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 + upperLegToFootVertical: 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: 7825922350359905173} bottomPointLocalSpace: {x: 0, y: 0, z: 0} LastBottomPoint: {x: 0, y: 0, z: 0} groundedPlayer: 0 @@ -590,22 +660,29 @@ 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 pushPower: 1 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 CrouchBlendDelta: 0 CanPushRigidbodys: 0 - BasisLocalPlayerTransform: {fileID: 0} + BasisLocalPlayerTransform: {fileID: 303201899784742928} CurrentSpeed: 0 LocalSeatDriver: UseDefaultMasking: 1 @@ -617,6 +694,9 @@ MonoBehaviour: m_Bits: 456 maxDownProbe: 3 maxUpProbe: 1 + DebugDrawGizmos: 1 + DebugPointRadius: 0.03 + DebugAxisLength: 0.12 LocalAnimatorDriver: basisAnimatorVariableApply: Animator: {fileID: 0} @@ -680,7 +760,6 @@ MonoBehaviour: centerBias: 2.5 perEyeVarianceDeg: 0.4 occasionalCenterReturn: 1 - HasEyeSchedule: 0 LocalHandDriver: LeftHand: ThumbPercentage: {x: 0, y: 0} @@ -715,148 +794,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 +811,14 @@ MonoBehaviour: BlendShapeInfos: [] HasJob: 0 blendShapeCount: 0 + UseOpenLipSync: 0 + EligibleForOpenLipSync: 0 + TrackedAudioSource: {fileID: 0} phonemeBlendShapeTable: [] WasSuccessful: 0 HashInstanceID: -1 uLipSyncEnabledState: 1 + InVisemeRange: 1 FacialBlinkDriver: Override: 0 meshRenderer: {fileID: 0} @@ -894,8 +836,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 +878,35 @@ 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} + _rigidbody: {fileID: 8369693708823570861} + _capsule: {fileID: 3011869999757657} + isGrounded: 0