From 6720e057500416ea543c124aafcb93e2fd6ed0a5 Mon Sep 17 00:00:00 2001 From: nothke Date: Sat, 17 Aug 2024 00:50:47 +0200 Subject: [PATCH] Added KCC --- Assets/Plugins.meta | 8 + .../Plugins/KinematicCharacterController.meta | 8 + .../KinematicCharacterController/Core.meta | 9 + .../Core/Editor.meta | 10 + .../Core/Editor/KCC.Editor.asmdef | 17 + .../Core/Editor/KCC.Editor.asmdef.meta | 7 + .../Editor/KinematicCharacterMotorEditor.cs | 28 + .../KinematicCharacterMotorEditor.cs.meta | 13 + .../Core/Editor/ReadOnlyPropertyDrawer.cs | 21 + .../Editor/ReadOnlyPropertyDrawer.cs.meta | 13 + .../Core/ICharacterController.cs | 50 + .../Core/ICharacterController.cs.meta | 11 + .../Core/IMoverController.cs | 14 + .../Core/IMoverController.cs.meta | 11 + .../Core/KCC.asmdef | 3 + .../Core/KCC.asmdef.meta | 7 + .../Core/KCCSettings.cs | 33 + .../Core/KCCSettings.cs.meta | 11 + .../Core/KinematicCharacterMotor.cs | 2689 +++++++++++++++++ .../Core/KinematicCharacterMotor.cs.meta | 11 + .../Core/KinematicCharacterSystem.cs | 294 ++ .../Core/KinematicCharacterSystem.cs.meta | 12 + .../Core/PhysicsMover.cs | 261 ++ .../Core/PhysicsMover.cs.meta | 12 + .../Core/ReadOnlyAttribute.cs | 8 + .../Core/ReadOnlyAttribute.cs.meta | 13 + .../ExampleCharacter.meta | 8 + .../ExampleCharacter/Materials.meta | 8 + .../ExampleCharacter/Materials/White.mat | 76 + .../ExampleCharacter/Materials/White.mat.meta | 9 + .../ExampleCharacter/Prefabs.meta | 8 + .../Prefabs/ExampleCamera.prefab | 127 + .../Prefabs/ExampleCamera.prefab.meta | 9 + .../Prefabs/ExampleCharacter.prefab | 394 +++ .../Prefabs/ExampleCharacter.prefab.meta | 9 + .../ExampleCharacter/Scripts.meta | 8 + .../Scripts/ExampleCharacterCamera.cs | 179 ++ .../Scripts/ExampleCharacterCamera.cs.meta | 12 + .../Scripts/ExampleCharacterController.cs | 516 ++++ .../ExampleCharacterController.cs.meta | 13 + .../ExampleCharacter/Scripts/ExamplePlayer.cs | 99 + .../Scripts/ExamplePlayer.cs.meta | 12 + 42 files changed, 5061 insertions(+) create mode 100644 Assets/Plugins.meta create mode 100644 Assets/Plugins/KinematicCharacterController.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core/Editor.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core/Editor/KCC.Editor.asmdef create mode 100644 Assets/Plugins/KinematicCharacterController/Core/Editor/KCC.Editor.asmdef.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core/Editor/KinematicCharacterMotorEditor.cs create mode 100644 Assets/Plugins/KinematicCharacterController/Core/Editor/KinematicCharacterMotorEditor.cs.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core/Editor/ReadOnlyPropertyDrawer.cs create mode 100644 Assets/Plugins/KinematicCharacterController/Core/Editor/ReadOnlyPropertyDrawer.cs.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core/ICharacterController.cs create mode 100644 Assets/Plugins/KinematicCharacterController/Core/ICharacterController.cs.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core/IMoverController.cs create mode 100644 Assets/Plugins/KinematicCharacterController/Core/IMoverController.cs.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core/KCC.asmdef create mode 100644 Assets/Plugins/KinematicCharacterController/Core/KCC.asmdef.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core/KCCSettings.cs create mode 100644 Assets/Plugins/KinematicCharacterController/Core/KCCSettings.cs.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterMotor.cs create mode 100644 Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterMotor.cs.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterSystem.cs create mode 100644 Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterSystem.cs.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core/PhysicsMover.cs create mode 100644 Assets/Plugins/KinematicCharacterController/Core/PhysicsMover.cs.meta create mode 100644 Assets/Plugins/KinematicCharacterController/Core/ReadOnlyAttribute.cs create mode 100644 Assets/Plugins/KinematicCharacterController/Core/ReadOnlyAttribute.cs.meta create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter.meta create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Materials.meta create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Materials/White.mat create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Materials/White.mat.meta create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs.meta create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCamera.prefab create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCamera.prefab.meta create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCharacter.prefab create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCharacter.prefab.meta create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts.meta create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterCamera.cs create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterCamera.cs.meta create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterController.cs create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterController.cs.meta create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExamplePlayer.cs create mode 100644 Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExamplePlayer.cs.meta diff --git a/Assets/Plugins.meta b/Assets/Plugins.meta new file mode 100644 index 0000000..d9bb659 --- /dev/null +++ b/Assets/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f5f3393f99206534a9f7c642a8fba028 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController.meta b/Assets/Plugins/KinematicCharacterController.meta new file mode 100644 index 0000000..65500f4 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 80ae4a9ddbd64d846a0b7de1a75c9f83 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core.meta b/Assets/Plugins/KinematicCharacterController/Core.meta new file mode 100644 index 0000000..fe4fdf5 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: c8854cb84deea8146b0cc7833ff338fd +folderAsset: yes +timeCreated: 1499134826 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core/Editor.meta b/Assets/Plugins/KinematicCharacterController/Core/Editor.meta new file mode 100644 index 0000000..965efb3 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/Editor.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 220667f043f505a4eb52d0208e14818e +folderAsset: yes +timeCreated: 1504492780 +licenseType: Store +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core/Editor/KCC.Editor.asmdef b/Assets/Plugins/KinematicCharacterController/Core/Editor/KCC.Editor.asmdef new file mode 100644 index 0000000..089a778 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/Editor/KCC.Editor.asmdef @@ -0,0 +1,17 @@ +{ + "name": "KCC.Editor", + "references": [ + "GUID:1bf68a6e05395544c9eb0c8d1ab94588" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/Core/Editor/KCC.Editor.asmdef.meta b/Assets/Plugins/KinematicCharacterController/Core/Editor/KCC.Editor.asmdef.meta new file mode 100644 index 0000000..96c148d --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/Editor/KCC.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8b7c931d99dfb09428a98748ecfd6169 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core/Editor/KinematicCharacterMotorEditor.cs b/Assets/Plugins/KinematicCharacterController/Core/Editor/KinematicCharacterMotorEditor.cs new file mode 100644 index 0000000..12eb811 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/Editor/KinematicCharacterMotorEditor.cs @@ -0,0 +1,28 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; + +namespace KinematicCharacterController +{ + [CustomEditor(typeof(KinematicCharacterMotor))] + public class KinematicCharacterMotorEditor : Editor + { + protected virtual void OnSceneGUI() + { + KinematicCharacterMotor motor = (target as KinematicCharacterMotor); + if (motor) + { + Vector3 characterBottom = motor.transform.position + (motor.Capsule.center + (-Vector3.up * (motor.Capsule.height * 0.5f))); + + Handles.color = Color.yellow; + Handles.CircleHandleCap( + 0, + characterBottom + (motor.transform.up * motor.MaxStepHeight), + Quaternion.LookRotation(motor.transform.up, motor.transform.forward), + motor.Capsule.radius + 0.1f, + EventType.Repaint); + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/Core/Editor/KinematicCharacterMotorEditor.cs.meta b/Assets/Plugins/KinematicCharacterController/Core/Editor/KinematicCharacterMotorEditor.cs.meta new file mode 100644 index 0000000..8a2539c --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/Editor/KinematicCharacterMotorEditor.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 7db3ed6d74cbf2b489cab5e1586bed7a +timeCreated: 1518400757 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core/Editor/ReadOnlyPropertyDrawer.cs b/Assets/Plugins/KinematicCharacterController/Core/Editor/ReadOnlyPropertyDrawer.cs new file mode 100644 index 0000000..5a1aa7c --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/Editor/ReadOnlyPropertyDrawer.cs @@ -0,0 +1,21 @@ +using UnityEngine; +using UnityEditor; + +namespace KinematicCharacterController +{ + [CustomPropertyDrawer(typeof(ReadOnlyAttribute))] + public class ReadOnlyPropertyDrawer : PropertyDrawer + { + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + return EditorGUI.GetPropertyHeight(property, label, true); + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + GUI.enabled = false; + EditorGUI.PropertyField(position, property, label, true); + GUI.enabled = true; + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/Core/Editor/ReadOnlyPropertyDrawer.cs.meta b/Assets/Plugins/KinematicCharacterController/Core/Editor/ReadOnlyPropertyDrawer.cs.meta new file mode 100644 index 0000000..3363fc6 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/Editor/ReadOnlyPropertyDrawer.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 0d4351ab88853824b85a8f458928d825 +timeCreated: 1504492796 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core/ICharacterController.cs b/Assets/Plugins/KinematicCharacterController/Core/ICharacterController.cs new file mode 100644 index 0000000..4ae54ac --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/ICharacterController.cs @@ -0,0 +1,50 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace KinematicCharacterController +{ + public interface ICharacterController + { + /// + /// This is called when the motor wants to know what its rotation should be right now + /// + void UpdateRotation(ref Quaternion currentRotation, float deltaTime); + /// + /// This is called when the motor wants to know what its velocity should be right now + /// + void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime); + /// + /// This is called before the motor does anything + /// + void BeforeCharacterUpdate(float deltaTime); + /// + /// This is called after the motor has finished its ground probing, but before PhysicsMover/Velocity/etc.... handling + /// + void PostGroundingUpdate(float deltaTime); + /// + /// This is called after the motor has finished everything in its update + /// + void AfterCharacterUpdate(float deltaTime); + /// + /// This is called after when the motor wants to know if the collider can be collided with (or if we just go through it) + /// + bool IsColliderValidForCollisions(Collider coll); + /// + /// This is called when the motor's ground probing detects a ground hit + /// + void OnGroundHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, ref HitStabilityReport hitStabilityReport); + /// + /// This is called when the motor's movement logic detects a hit + /// + void OnMovementHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, ref HitStabilityReport hitStabilityReport); + /// + /// This is called after every move hit, to give you an opportunity to modify the HitStabilityReport to your liking + /// + void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, Vector3 atCharacterPosition, Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport); + /// + /// This is called when the character detects discrete collisions (collisions that don't result from the motor's capsuleCasts when moving) + /// + void OnDiscreteCollisionDetected(Collider hitCollider); + } +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/Core/ICharacterController.cs.meta b/Assets/Plugins/KinematicCharacterController/Core/ICharacterController.cs.meta new file mode 100644 index 0000000..e87f49a --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/ICharacterController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f13aa252bd3bf9943b1d65e68d93dcfb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core/IMoverController.cs b/Assets/Plugins/KinematicCharacterController/Core/IMoverController.cs new file mode 100644 index 0000000..f582ec1 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/IMoverController.cs @@ -0,0 +1,14 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace KinematicCharacterController +{ + public interface IMoverController + { + /// + /// This is called to let you tell the PhysicsMover where it should be right now + /// + void UpdateMovement(out Vector3 goalPosition, out Quaternion goalRotation, float deltaTime); + } +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/Core/IMoverController.cs.meta b/Assets/Plugins/KinematicCharacterController/Core/IMoverController.cs.meta new file mode 100644 index 0000000..553f65e --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/IMoverController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8ae7eceec45f1be4c8450edc46c0ad36 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core/KCC.asmdef b/Assets/Plugins/KinematicCharacterController/Core/KCC.asmdef new file mode 100644 index 0000000..2dbd7e7 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/KCC.asmdef @@ -0,0 +1,3 @@ +{ + "name": "KCC" +} diff --git a/Assets/Plugins/KinematicCharacterController/Core/KCC.asmdef.meta b/Assets/Plugins/KinematicCharacterController/Core/KCC.asmdef.meta new file mode 100644 index 0000000..2de9d19 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/KCC.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1bf68a6e05395544c9eb0c8d1ab94588 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core/KCCSettings.cs b/Assets/Plugins/KinematicCharacterController/Core/KCCSettings.cs new file mode 100644 index 0000000..23e6848 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/KCCSettings.cs @@ -0,0 +1,33 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace KinematicCharacterController +{ + [CreateAssetMenu] + public class KCCSettings : ScriptableObject + { + /// + /// Determines if the system simulates automatically. + /// If true, the simulation is done on FixedUpdate + /// + [Tooltip("Determines if the system simulates automatically. If true, the simulation is done on FixedUpdate")] + public bool AutoSimulation = true; + /// + /// Should interpolation of characters and PhysicsMovers be handled + /// + [Tooltip("Should interpolation of characters and PhysicsMovers be handled")] + public bool Interpolate = true; + /// + + /// Initial capacity of the system's list of Motors (will resize automatically if needed, but setting a high initial capacity can help preventing GC allocs) + /// + [Tooltip("Initial capacity of the system's list of Motors (will resize automatically if needed, but setting a high initial capacity can help preventing GC allocs)")] + public int MotorsListInitialCapacity = 100; + /// + /// Initial capacity of the system's list of Movers (will resize automatically if needed, but setting a high initial capacity can help preventing GC allocs) + /// + [Tooltip("Initial capacity of the system's list of Movers (will resize automatically if needed, but setting a high initial capacity can help preventing GC allocs)")] + public int MoversListInitialCapacity = 100; + } +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/Core/KCCSettings.cs.meta b/Assets/Plugins/KinematicCharacterController/Core/KCCSettings.cs.meta new file mode 100644 index 0000000..daa4144 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/KCCSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2b43d8459fe4812488b2c47ac60e41c3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterMotor.cs b/Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterMotor.cs new file mode 100644 index 0000000..bd04d3a --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterMotor.cs @@ -0,0 +1,2689 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace KinematicCharacterController +{ + public enum RigidbodyInteractionType + { + None, + Kinematic, + SimulatedDynamic + } + + public enum StepHandlingMethod + { + None, + Standard, + Extra + } + + public enum MovementSweepState + { + Initial, + AfterFirstHit, + FoundBlockingCrease, + FoundBlockingCorner, + } + + /// + /// Represents the entire state of a character motor that is pertinent for simulation. + /// Use this to save state or revert to past state + /// + [System.Serializable] + public struct KinematicCharacterMotorState + { + public Vector3 Position; + public Quaternion Rotation; + public Vector3 BaseVelocity; + + public bool MustUnground; + public float MustUngroundTime; + public bool LastMovementIterationFoundAnyGround; + public CharacterTransientGroundingReport GroundingStatus; + + public Rigidbody AttachedRigidbody; + public Vector3 AttachedRigidbodyVelocity; + } + + /// + /// Describes an overlap between the character capsule and another collider + /// + public struct OverlapResult + { + public Vector3 Normal; + public Collider Collider; + + public OverlapResult(Vector3 normal, Collider collider) + { + Normal = normal; + Collider = collider; + } + } + + /// + /// Contains all the information for the motor's grounding status + /// + public struct CharacterGroundingReport + { + public bool FoundAnyGround; + public bool IsStableOnGround; + public bool SnappingPrevented; + public Vector3 GroundNormal; + public Vector3 InnerGroundNormal; + public Vector3 OuterGroundNormal; + + public Collider GroundCollider; + public Vector3 GroundPoint; + + public void CopyFrom(CharacterTransientGroundingReport transientGroundingReport) + { + FoundAnyGround = transientGroundingReport.FoundAnyGround; + IsStableOnGround = transientGroundingReport.IsStableOnGround; + SnappingPrevented = transientGroundingReport.SnappingPrevented; + GroundNormal = transientGroundingReport.GroundNormal; + InnerGroundNormal = transientGroundingReport.InnerGroundNormal; + OuterGroundNormal = transientGroundingReport.OuterGroundNormal; + + GroundCollider = null; + GroundPoint = Vector3.zero; + } + } + + /// + /// Contains the simulation-relevant information for the motor's grounding status + /// + public struct CharacterTransientGroundingReport + { + public bool FoundAnyGround; + public bool IsStableOnGround; + public bool SnappingPrevented; + public Vector3 GroundNormal; + public Vector3 InnerGroundNormal; + public Vector3 OuterGroundNormal; + + public void CopyFrom(CharacterGroundingReport groundingReport) + { + FoundAnyGround = groundingReport.FoundAnyGround; + IsStableOnGround = groundingReport.IsStableOnGround; + SnappingPrevented = groundingReport.SnappingPrevented; + GroundNormal = groundingReport.GroundNormal; + InnerGroundNormal = groundingReport.InnerGroundNormal; + OuterGroundNormal = groundingReport.OuterGroundNormal; + } + } + + /// + /// Contains all the information from a hit stability evaluation + /// + public struct HitStabilityReport + { + public bool IsStable; + + public bool FoundInnerNormal; + public Vector3 InnerNormal; + public bool FoundOuterNormal; + public Vector3 OuterNormal; + + public bool ValidStepDetected; + public Collider SteppedCollider; + + public bool LedgeDetected; + public bool IsOnEmptySideOfLedge; + public float DistanceFromLedge; + public bool IsMovingTowardsEmptySideOfLedge; + public Vector3 LedgeGroundNormal; + public Vector3 LedgeRightDirection; + public Vector3 LedgeFacingDirection; + } + + /// + /// Contains the information of hit rigidbodies during the movement phase, so they can be processed afterwards + /// + public struct RigidbodyProjectionHit + { + public Rigidbody Rigidbody; + public Vector3 HitPoint; + public Vector3 EffectiveHitNormal; + public Vector3 HitVelocity; + public bool StableOnHit; + } + + /// + /// Component that manages character collisions and movement solving + /// + [RequireComponent(typeof(CapsuleCollider))] + public class KinematicCharacterMotor : MonoBehaviour + { +#pragma warning disable 0414 + [Header("Components")] + /// + /// The capsule collider of this motor + /// + [ReadOnly] + public CapsuleCollider Capsule; + + [Header("Capsule Settings")] + /// + /// Radius of the character's capsule + /// + [SerializeField] + [Tooltip("Radius of the Character Capsule")] + private float CapsuleRadius = 0.5f; + /// + /// Height of the character's capsule + /// + [SerializeField] + [Tooltip("Height of the Character Capsule")] + private float CapsuleHeight = 2f; + /// + /// Local y position of the character's capsule center + /// + [SerializeField] + [Tooltip("Height of the Character Capsule")] + private float CapsuleYOffset = 1f; + /// + /// Physics material of the character's capsule + /// + [SerializeField] + [Tooltip("Physics material of the Character Capsule (Does not affect character movement. Only affects things colliding with it)")] +#pragma warning disable 0649 + private PhysicMaterial CapsulePhysicsMaterial; +#pragma warning restore 0649 + + + [Header("Grounding settings")] + /// + /// Increases the range of ground detection, to allow snapping to ground at very high speeds + /// + [Tooltip("Increases the range of ground detection, to allow snapping to ground at very high speeds")] + public float GroundDetectionExtraDistance = 0f; + /// + /// Maximum slope angle on which the character can be stable + /// + [Range(0f, 89f)] + [Tooltip("Maximum slope angle on which the character can be stable")] + public float MaxStableSlopeAngle = 60f; + /// + /// Which layers can the character be considered stable on + /// + [Tooltip("Which layers can the character be considered stable on")] + public LayerMask StableGroundLayers = -1; + /// + /// Notifies the Character Controller when discrete collisions are detected + /// + [Tooltip("Notifies the Character Controller when discrete collisions are detected")] + public bool DiscreteCollisionEvents = false; + + + [Header("Step settings")] + /// + /// Handles properly detecting grounding status on steps, but has a performance cost. + /// + [Tooltip("Handles properly detecting grounding status on steps, but has a performance cost.")] + public StepHandlingMethod StepHandling = StepHandlingMethod.Standard; + /// + /// Maximum height of a step which the character can climb + /// + [Tooltip("Maximum height of a step which the character can climb")] + public float MaxStepHeight = 0.5f; + /// + /// Can the character step up obstacles even if it is not currently stable? + /// + [Tooltip("Can the character step up obstacles even if it is not currently stable?")] + public bool AllowSteppingWithoutStableGrounding = false; + /// + /// Minimum length of a step that the character can step on (used in Extra stepping method. Use this to let the character step on steps that are smaller that its radius + /// + [Tooltip("Minimum length of a step that the character can step on (used in Extra stepping method). Use this to let the character step on steps that are smaller that its radius")] + public float MinRequiredStepDepth = 0.1f; + + + [Header("Ledge settings")] + /// + /// Handles properly detecting ledge information and grounding status, but has a performance cost. + /// + [Tooltip("Handles properly detecting ledge information and grounding status, but has a performance cost.")] + public bool LedgeAndDenivelationHandling = true; + /// + /// The distance from the capsule central axis at which the character can stand on a ledge and still be stable + /// + [Tooltip("The distance from the capsule central axis at which the character can stand on a ledge and still be stable")] + public float MaxStableDistanceFromLedge = 0.5f; + /// + /// Prevents snapping to ground on ledges beyond a certain velocity + /// + [Tooltip("Prevents snapping to ground on ledges beyond a certain velocity")] + public float MaxVelocityForLedgeSnap = 0f; + /// + /// The maximun downward slope angle change that the character can be subjected to and still be snapping to the ground + /// + [Tooltip("The maximun downward slope angle change that the character can be subjected to and still be snapping to the ground")] + [Range(1f, 180f)] + public float MaxStableDenivelationAngle = 180f; + + + [Header("Rigidbody interaction settings")] + /// + /// Handles properly being pushed by and standing on PhysicsMovers or dynamic rigidbodies. Also handles pushing dynamic rigidbodies + /// + [Tooltip("Handles properly being pushed by and standing on PhysicsMovers or dynamic rigidbodies. Also handles pushing dynamic rigidbodies")] + public bool InteractiveRigidbodyHandling = true; + /// + /// How the character interacts with non-kinematic rigidbodies. \"Kinematic\" mode means the character pushes the rigidbodies with infinite force (as a kinematic body would). \"SimulatedDynamic\" pushes the rigidbodies with a simulated mass value. + /// + [Tooltip("How the character interacts with non-kinematic rigidbodies. \"Kinematic\" mode means the character pushes the rigidbodies with infinite force (as a kinematic body would). \"SimulatedDynamic\" pushes the rigidbodies with a simulated mass value.")] + public RigidbodyInteractionType RigidbodyInteractionType; + [Tooltip("Mass used for pushing bodies")] + public float SimulatedCharacterMass = 1f; + /// + /// Determines if the character preserves moving platform velocities when de-grounding from them + /// + [Tooltip("Determines if the character preserves moving platform velocities when de-grounding from them")] + public bool PreserveAttachedRigidbodyMomentum = true; + + + [Header("Constraints settings")] + /// + /// Determines if the character's movement uses the planar constraint + /// + [Tooltip("Determines if the character's movement uses the planar constraint")] + public bool HasPlanarConstraint = false; + /// + /// Defines the plane that the character's movement is constrained on, if HasMovementConstraintPlane is active + /// + [Tooltip("Defines the plane that the character's movement is constrained on, if HasMovementConstraintPlane is active")] + public Vector3 PlanarConstraintAxis = Vector3.forward; + + [Header("Other settings")] + /// + /// How many times can we sweep for movement per update + /// + [Tooltip("How many times can we sweep for movement per update")] + public int MaxMovementIterations = 5; + /// + /// How many times can we check for decollision per update + /// + [Tooltip("How many times can we check for decollision per update")] + public int MaxDecollisionIterations = 1; + /// + /// Checks for overlaps before casting movement, making sure all collisions are detected even when already intersecting geometry (has a performance cost, but provides safety against tunneling through colliders) + /// + [Tooltip("Checks for overlaps before casting movement, making sure all collisions are detected even when already intersecting geometry (has a performance cost, but provides safety against tunneling through colliders)")] + public bool CheckMovementInitialOverlaps = true; + /// + /// Sets the velocity to zero if exceed max movement iterations + /// + [Tooltip("Sets the velocity to zero if exceed max movement iterations")] + public bool KillVelocityWhenExceedMaxMovementIterations = true; + /// + /// Sets the remaining movement to zero if exceed max movement iterations + /// + [Tooltip("Sets the remaining movement to zero if exceed max movement iterations")] + public bool KillRemainingMovementWhenExceedMaxMovementIterations = true; + + /// + /// Contains the current grounding information + /// + [System.NonSerialized] + public CharacterGroundingReport GroundingStatus = new CharacterGroundingReport(); + /// + /// Contains the previous grounding information + /// + [System.NonSerialized] + public CharacterTransientGroundingReport LastGroundingStatus = new CharacterTransientGroundingReport(); + /// + /// Specifies the LayerMask that the character's movement algorithm can detect collisions with. By default, this uses the rigidbody's layer's collision matrix + /// + [System.NonSerialized] + public LayerMask CollidableLayers = -1; + + /// + /// The Transform of the character motor + /// + public Transform Transform { get { return _transform; } } + private Transform _transform; + /// + /// The character's goal position in its movement calculations (always up-to-date during the character update phase) + /// + public Vector3 TransientPosition { get { return _transientPosition; } } + private Vector3 _transientPosition; + /// + /// The character's up direction (always up-to-date during the character update phase) + /// + public Vector3 CharacterUp { get { return _characterUp; } } + private Vector3 _characterUp; + /// + /// The character's forward direction (always up-to-date during the character update phase) + /// + public Vector3 CharacterForward { get { return _characterForward; } } + private Vector3 _characterForward; + /// + /// The character's right direction (always up-to-date during the character update phase) + /// + public Vector3 CharacterRight { get { return _characterRight; } } + private Vector3 _characterRight; + /// + /// The character's position before the movement calculations began + /// + public Vector3 InitialSimulationPosition { get { return _initialSimulationPosition; } } + private Vector3 _initialSimulationPosition; + /// + /// The character's rotation before the movement calculations began + /// + public Quaternion InitialSimulationRotation { get { return _initialSimulationRotation; } } + private Quaternion _initialSimulationRotation; + /// + /// Represents the Rigidbody to stay attached to + /// + public Rigidbody AttachedRigidbody { get { return _attachedRigidbody; } } + private Rigidbody _attachedRigidbody; + /// + /// Vector3 from the character transform position to the capsule center + /// + public Vector3 CharacterTransformToCapsuleCenter { get { return _characterTransformToCapsuleCenter; } } + private Vector3 _characterTransformToCapsuleCenter; + /// + /// Vector3 from the character transform position to the capsule bottom + /// + public Vector3 CharacterTransformToCapsuleBottom { get { return _characterTransformToCapsuleBottom; } } + private Vector3 _characterTransformToCapsuleBottom; + /// + /// Vector3 from the character transform position to the capsule top + /// + public Vector3 CharacterTransformToCapsuleTop { get { return _characterTransformToCapsuleTop; } } + private Vector3 _characterTransformToCapsuleTop; + /// + /// Vector3 from the character transform position to the capsule bottom hemi center + /// + public Vector3 CharacterTransformToCapsuleBottomHemi { get { return _characterTransformToCapsuleBottomHemi; } } + private Vector3 _characterTransformToCapsuleBottomHemi; + /// + /// Vector3 from the character transform position to the capsule top hemi center + /// + public Vector3 CharacterTransformToCapsuleTopHemi { get { return _characterTransformToCapsuleTopHemi; } } + private Vector3 _characterTransformToCapsuleTopHemi; + /// + /// The character's velocity resulting from standing on rigidbodies or PhysicsMover + /// + public Vector3 AttachedRigidbodyVelocity { get { return _attachedRigidbodyVelocity; } } + private Vector3 _attachedRigidbodyVelocity; + /// + /// The number of overlaps detected so far during character update (is reset at the beginning of the update) + /// + public int OverlapsCount { get { return _overlapsCount; } } + private int _overlapsCount; + /// + /// The overlaps detected so far during character update + /// + public OverlapResult[] Overlaps { get { return _overlaps; } } + private OverlapResult[] _overlaps = new OverlapResult[MaxRigidbodyOverlapsCount]; + + /// + /// The motor's assigned controller + /// + [NonSerialized] + public ICharacterController CharacterController; + /// + /// Did the motor's last swept collision detection find a ground? + /// + [NonSerialized] + public bool LastMovementIterationFoundAnyGround; + /// + /// Index of this motor in KinematicCharacterSystem arrays + /// + [NonSerialized] + public int IndexInCharacterSystem; + /// + /// Remembers initial position before all simulation are done + /// + [NonSerialized] + public Vector3 InitialTickPosition; + /// + /// Remembers initial rotation before all simulation are done + /// + [NonSerialized] + public Quaternion InitialTickRotation; + /// + /// Specifies a Rigidbody to stay attached to + /// + [NonSerialized] + public Rigidbody AttachedRigidbodyOverride; + /// + /// The character's velocity resulting from direct movement + /// + [NonSerialized] + public Vector3 BaseVelocity; + + // Private + private RaycastHit[] _internalCharacterHits = new RaycastHit[MaxHitsBudget]; + private Collider[] _internalProbedColliders = new Collider[MaxCollisionBudget]; + private List _rigidbodiesPushedThisMove = new List(16); + private RigidbodyProjectionHit[] _internalRigidbodyProjectionHits = new RigidbodyProjectionHit[MaxRigidbodyOverlapsCount]; + private Rigidbody _lastAttachedRigidbody; + private bool _solveMovementCollisions = true; + private bool _solveGrounding = true; + private bool _movePositionDirty = false; + private Vector3 _movePositionTarget = Vector3.zero; + private bool _moveRotationDirty = false; + private Quaternion _moveRotationTarget = Quaternion.identity; + private bool _lastSolvedOverlapNormalDirty = false; + private Vector3 _lastSolvedOverlapNormal = Vector3.forward; + private int _rigidbodyProjectionHitCount = 0; + private bool _isMovingFromAttachedRigidbody = false; + private bool _mustUnground = false; + private float _mustUngroundTimeCounter = 0f; + private Vector3 _cachedWorldUp = Vector3.up; + private Vector3 _cachedWorldForward = Vector3.forward; + private Vector3 _cachedWorldRight = Vector3.right; + private Vector3 _cachedZeroVector = Vector3.zero; + + private Quaternion _transientRotation; + /// + /// The character's goal rotation in its movement calculations (always up-to-date during the character update phase) + /// + public Quaternion TransientRotation + { + get + { + return _transientRotation; + } + private set + { + _transientRotation = value; + _characterUp = _transientRotation * _cachedWorldUp; + _characterForward = _transientRotation * _cachedWorldForward; + _characterRight = _transientRotation * _cachedWorldRight; + } + } + + /// + /// The character's total velocity, including velocity from standing on rigidbodies or PhysicsMover + /// + public Vector3 Velocity + { + get + { + return BaseVelocity + _attachedRigidbodyVelocity; + } + } + + // Warning: Don't touch these constants unless you know exactly what you're doing! + public const int MaxHitsBudget = 16; + public const int MaxCollisionBudget = 16; + public const int MaxGroundingSweepIterations = 2; + public const int MaxSteppingSweepIterations = 3; + public const int MaxRigidbodyOverlapsCount = 16; + public const float CollisionOffset = 0.01f; + public const float GroundProbeReboundDistance = 0.02f; + public const float MinimumGroundProbingDistance = 0.005f; + public const float GroundProbingBackstepDistance = 0.1f; + public const float SweepProbingBackstepDistance = 0.002f; + public const float SecondaryProbesVertical = 0.02f; + public const float SecondaryProbesHorizontal = 0.001f; + public const float MinVelocityMagnitude = 0.01f; + public const float SteppingForwardDistance = 0.03f; + public const float MinDistanceForLedge = 0.05f; + public const float CorrelationForVerticalObstruction = 0.01f; + public const float ExtraSteppingForwardDistance = 0.01f; + public const float ExtraStepHeightPadding = 0.01f; +#pragma warning restore 0414 + + private void OnEnable() + { + KinematicCharacterSystem.EnsureCreation(); + KinematicCharacterSystem.RegisterCharacterMotor(this); + } + + private void OnDisable() + { + KinematicCharacterSystem.UnregisterCharacterMotor(this); + } + + private void Reset() + { + ValidateData(); + } + + private void OnValidate() + { + ValidateData(); + } + + [ContextMenu("Remove Component")] + private void HandleRemoveComponent() + { + CapsuleCollider tmpCapsule = gameObject.GetComponent(); + DestroyImmediate(this); + DestroyImmediate(tmpCapsule); + } + + /// + /// Handle validating all required values + /// + public void ValidateData() + { + Capsule = GetComponent(); + CapsuleRadius = Mathf.Clamp(CapsuleRadius, 0f, CapsuleHeight * 0.5f); + Capsule.direction = 1; + Capsule.sharedMaterial = CapsulePhysicsMaterial; + SetCapsuleDimensions(CapsuleRadius, CapsuleHeight, CapsuleYOffset); + + MaxStepHeight = Mathf.Clamp(MaxStepHeight, 0f, Mathf.Infinity); + MinRequiredStepDepth = Mathf.Clamp(MinRequiredStepDepth, 0f, CapsuleRadius); + MaxStableDistanceFromLedge = Mathf.Clamp(MaxStableDistanceFromLedge, 0f, CapsuleRadius); + + transform.localScale = Vector3.one; + +#if UNITY_EDITOR + Capsule.hideFlags = HideFlags.NotEditable; + if (!Mathf.Approximately(transform.lossyScale.x, 1f) || !Mathf.Approximately(transform.lossyScale.y, 1f) || !Mathf.Approximately(transform.lossyScale.z, 1f)) + { + Debug.LogError("Character's lossy scale is not (1,1,1). This is not allowed. Make sure the character's transform and all of its parents have a (1,1,1) scale.", this.gameObject); + } +#endif + } + + /// + /// Sets whether or not the capsule collider will detect collisions + /// + public void SetCapsuleCollisionsActivation(bool collisionsActive) + { + Capsule.isTrigger = !collisionsActive; + } + + /// + /// Sets whether or not the motor will solve collisions when moving (or moved onto) + /// + public void SetMovementCollisionsSolvingActivation(bool movementCollisionsSolvingActive) + { + _solveMovementCollisions = movementCollisionsSolvingActive; + } + + /// + /// Sets whether or not grounding will be evaluated for all hits + /// + public void SetGroundSolvingActivation(bool stabilitySolvingActive) + { + _solveGrounding = stabilitySolvingActive; + } + + /// + /// Sets the character's position directly + /// + public void SetPosition(Vector3 position, bool bypassInterpolation = true) + { + _transform.position = position; + _initialSimulationPosition = position; + _transientPosition = position; + + if (bypassInterpolation) + { + InitialTickPosition = position; + } + } + + /// + /// Sets the character's rotation directly + /// + public void SetRotation(Quaternion rotation, bool bypassInterpolation = true) + { + _transform.rotation = rotation; + _initialSimulationRotation = rotation; + TransientRotation = rotation; + + if (bypassInterpolation) + { + InitialTickRotation = rotation; + } + } + + /// + /// Sets the character's position and rotation directly + /// + public void SetPositionAndRotation(Vector3 position, Quaternion rotation, bool bypassInterpolation = true) + { + _transform.SetPositionAndRotation(position, rotation); + _initialSimulationPosition = position; + _initialSimulationRotation = rotation; + _transientPosition = position; + TransientRotation = rotation; + + if (bypassInterpolation) + { + InitialTickPosition = position; + InitialTickRotation = rotation; + } + } + + /// + /// Moves the character position, taking all movement collision solving int account. The actual move is done the next time the motor updates are called + /// + public void MoveCharacter(Vector3 toPosition) + { + _movePositionDirty = true; + _movePositionTarget = toPosition; + } + + /// + /// Moves the character rotation. The actual move is done the next time the motor updates are called + /// + public void RotateCharacter(Quaternion toRotation) + { + _moveRotationDirty = true; + _moveRotationTarget = toRotation; + } + + /// + /// Returns all the state information of the motor that is pertinent for simulation + /// + public KinematicCharacterMotorState GetState() + { + KinematicCharacterMotorState state = new KinematicCharacterMotorState(); + + state.Position = _transientPosition; + state.Rotation = _transientRotation; + + state.BaseVelocity = BaseVelocity; + state.AttachedRigidbodyVelocity = _attachedRigidbodyVelocity; + + state.MustUnground = _mustUnground; + state.MustUngroundTime = _mustUngroundTimeCounter; + state.LastMovementIterationFoundAnyGround = LastMovementIterationFoundAnyGround; + state.GroundingStatus.CopyFrom(GroundingStatus); + state.AttachedRigidbody = _attachedRigidbody; + + return state; + } + + /// + /// Applies a motor state instantly + /// + public void ApplyState(KinematicCharacterMotorState state, bool bypassInterpolation = true) + { + SetPositionAndRotation(state.Position, state.Rotation, bypassInterpolation); + + BaseVelocity = state.BaseVelocity; + _attachedRigidbodyVelocity = state.AttachedRigidbodyVelocity; + + _mustUnground = state.MustUnground; + _mustUngroundTimeCounter = state.MustUngroundTime; + LastMovementIterationFoundAnyGround = state.LastMovementIterationFoundAnyGround; + GroundingStatus.CopyFrom(state.GroundingStatus); + _attachedRigidbody = state.AttachedRigidbody; + } + + /// + /// Resizes capsule. ALso caches importand capsule size data + /// + public void SetCapsuleDimensions(float radius, float height, float yOffset) + { + height = Mathf.Max(height, (radius * 2f) + 0.01f); // Safety to prevent invalid capsule geometries + + CapsuleRadius = radius; + CapsuleHeight = height; + CapsuleYOffset = yOffset; + + Capsule.radius = CapsuleRadius; + Capsule.height = Mathf.Clamp(CapsuleHeight, CapsuleRadius * 2f, CapsuleHeight); + Capsule.center = new Vector3(0f, CapsuleYOffset, 0f); + + _characterTransformToCapsuleCenter = Capsule.center; + _characterTransformToCapsuleBottom = Capsule.center + (-_cachedWorldUp * (Capsule.height * 0.5f)); + _characterTransformToCapsuleTop = Capsule.center + (_cachedWorldUp * (Capsule.height * 0.5f)); + _characterTransformToCapsuleBottomHemi = Capsule.center + (-_cachedWorldUp * (Capsule.height * 0.5f)) + (_cachedWorldUp * Capsule.radius); + _characterTransformToCapsuleTopHemi = Capsule.center + (_cachedWorldUp * (Capsule.height * 0.5f)) + (-_cachedWorldUp * Capsule.radius); + } + + private void Awake() + { + _transform = this.transform; + ValidateData(); + + _transientPosition = _transform.position; + TransientRotation = _transform.rotation; + + // Build CollidableLayers mask + CollidableLayers = 0; + for (int i = 0; i < 32; i++) + { + if (!Physics.GetIgnoreLayerCollision(this.gameObject.layer, i)) + { + CollidableLayers |= (1 << i); + } + } + + SetCapsuleDimensions(CapsuleRadius, CapsuleHeight, CapsuleYOffset); + } + + /// + /// Update phase 1 is meant to be called after physics movers have calculated their velocities, but + /// before they have simulated their goal positions/rotations. It is responsible for: + /// - Initializing all values for update + /// - Handling MovePosition calls + /// - Solving initial collision overlaps + /// - Ground probing + /// - Handle detecting potential interactable rigidbodies + /// + public void UpdatePhase1(float deltaTime) + { + // NaN propagation safety stop + if (float.IsNaN(BaseVelocity.x) || float.IsNaN(BaseVelocity.y) || float.IsNaN(BaseVelocity.z)) + { + BaseVelocity = Vector3.zero; + } + if (float.IsNaN(_attachedRigidbodyVelocity.x) || float.IsNaN(_attachedRigidbodyVelocity.y) || float.IsNaN(_attachedRigidbodyVelocity.z)) + { + _attachedRigidbodyVelocity = Vector3.zero; + } + +#if UNITY_EDITOR + if (!Mathf.Approximately(_transform.lossyScale.x, 1f) || !Mathf.Approximately(_transform.lossyScale.y, 1f) || !Mathf.Approximately(_transform.lossyScale.z, 1f)) + { + Debug.LogError("Character's lossy scale is not (1,1,1). This is not allowed. Make sure the character's transform and all of its parents have a (1,1,1) scale.", this.gameObject); + } +#endif + + _rigidbodiesPushedThisMove.Clear(); + + // Before update + CharacterController.BeforeCharacterUpdate(deltaTime); + + _transientPosition = _transform.position; + TransientRotation = _transform.rotation; + _initialSimulationPosition = _transientPosition; + _initialSimulationRotation = _transientRotation; + _rigidbodyProjectionHitCount = 0; + _overlapsCount = 0; + _lastSolvedOverlapNormalDirty = false; + + #region Handle Move Position + if (_movePositionDirty) + { + if (_solveMovementCollisions) + { + Vector3 tmpVelocity = GetVelocityFromMovement(_movePositionTarget - _transientPosition, deltaTime); + if (InternalCharacterMove(ref tmpVelocity, deltaTime)) + { + if (InteractiveRigidbodyHandling) + { + ProcessVelocityForRigidbodyHits(ref tmpVelocity, deltaTime); + } + } + } + else + { + _transientPosition = _movePositionTarget; + } + + _movePositionDirty = false; + } + #endregion + + LastGroundingStatus.CopyFrom(GroundingStatus); + GroundingStatus = new CharacterGroundingReport(); + GroundingStatus.GroundNormal = _characterUp; + + if (_solveMovementCollisions) + { + #region Resolve initial overlaps + Vector3 resolutionDirection = _cachedWorldUp; + float resolutionDistance = 0f; + int iterationsMade = 0; + bool overlapSolved = false; + while (iterationsMade < MaxDecollisionIterations && !overlapSolved) + { + int nbOverlaps = CharacterCollisionsOverlap(_transientPosition, _transientRotation, _internalProbedColliders); + + if (nbOverlaps > 0) + { + // Solve overlaps that aren't against dynamic rigidbodies or physics movers + for (int i = 0; i < nbOverlaps; i++) + { + if (GetInteractiveRigidbody(_internalProbedColliders[i]) == null) + { + // Process overlap + Transform overlappedTransform = _internalProbedColliders[i].GetComponent(); + if (Physics.ComputePenetration( + Capsule, + _transientPosition, + _transientRotation, + _internalProbedColliders[i], + overlappedTransform.position, + overlappedTransform.rotation, + out resolutionDirection, + out resolutionDistance)) + { + // Resolve along obstruction direction + HitStabilityReport mockReport = new HitStabilityReport(); + mockReport.IsStable = IsStableOnNormal(resolutionDirection); + resolutionDirection = GetObstructionNormal(resolutionDirection, mockReport.IsStable); + + // Solve overlap + Vector3 resolutionMovement = resolutionDirection * (resolutionDistance + CollisionOffset); + _transientPosition += resolutionMovement; + + // Remember overlaps + if (_overlapsCount < _overlaps.Length) + { + _overlaps[_overlapsCount] = new OverlapResult(resolutionDirection, _internalProbedColliders[i]); + _overlapsCount++; + } + + break; + } + } + } + } + else + { + overlapSolved = true; + } + + iterationsMade++; + } + #endregion + } + + #region Ground Probing and Snapping + // Handle ungrounding + if (_solveGrounding) + { + if (MustUnground()) + { + _transientPosition += _characterUp * (MinimumGroundProbingDistance * 1.5f); + } + else + { + // Choose the appropriate ground probing distance + float selectedGroundProbingDistance = MinimumGroundProbingDistance; + if (!LastGroundingStatus.SnappingPrevented && (LastGroundingStatus.IsStableOnGround || LastMovementIterationFoundAnyGround)) + { + if (StepHandling != StepHandlingMethod.None) + { + selectedGroundProbingDistance = Mathf.Max(CapsuleRadius, MaxStepHeight); + } + else + { + selectedGroundProbingDistance = CapsuleRadius; + } + + selectedGroundProbingDistance += GroundDetectionExtraDistance; + } + + ProbeGround(ref _transientPosition, _transientRotation, selectedGroundProbingDistance, ref GroundingStatus); + + if (!LastGroundingStatus.IsStableOnGround && GroundingStatus.IsStableOnGround) + { + // Handle stable landing + BaseVelocity = Vector3.ProjectOnPlane(BaseVelocity, CharacterUp); + BaseVelocity = GetDirectionTangentToSurface(BaseVelocity, GroundingStatus.GroundNormal) * BaseVelocity.magnitude; + } + } + } + + LastMovementIterationFoundAnyGround = false; + + if (_mustUngroundTimeCounter > 0f) + { + _mustUngroundTimeCounter -= deltaTime; + } + _mustUnground = false; + #endregion + + if (_solveGrounding) + { + CharacterController.PostGroundingUpdate(deltaTime); + } + + if (InteractiveRigidbodyHandling) + { + #region Interactive Rigidbody Handling + _lastAttachedRigidbody = _attachedRigidbody; + if (AttachedRigidbodyOverride) + { + _attachedRigidbody = AttachedRigidbodyOverride; + } + else + { + // Detect interactive rigidbodies from grounding + if (GroundingStatus.IsStableOnGround && GroundingStatus.GroundCollider.attachedRigidbody) + { + Rigidbody interactiveRigidbody = GetInteractiveRigidbody(GroundingStatus.GroundCollider); + if (interactiveRigidbody) + { + _attachedRigidbody = interactiveRigidbody; + } + } + else + { + _attachedRigidbody = null; + } + } + + Vector3 tmpVelocityFromCurrentAttachedRigidbody = Vector3.zero; + Vector3 tmpAngularVelocityFromCurrentAttachedRigidbody = Vector3.zero; + if (_attachedRigidbody) + { + GetVelocityFromRigidbodyMovement(_attachedRigidbody, _transientPosition, deltaTime, out tmpVelocityFromCurrentAttachedRigidbody, out tmpAngularVelocityFromCurrentAttachedRigidbody); + } + + // Conserve momentum when de-stabilized from an attached rigidbody + if (PreserveAttachedRigidbodyMomentum && _lastAttachedRigidbody != null && _attachedRigidbody != _lastAttachedRigidbody) + { + BaseVelocity += _attachedRigidbodyVelocity; + BaseVelocity -= tmpVelocityFromCurrentAttachedRigidbody; + } + + // Process additionnal Velocity from attached rigidbody + _attachedRigidbodyVelocity = _cachedZeroVector; + if (_attachedRigidbody) + { + _attachedRigidbodyVelocity = tmpVelocityFromCurrentAttachedRigidbody; + + // Rotation from attached rigidbody + Vector3 newForward = Vector3.ProjectOnPlane(Quaternion.Euler(Mathf.Rad2Deg * tmpAngularVelocityFromCurrentAttachedRigidbody * deltaTime) * _characterForward, _characterUp).normalized; + TransientRotation = Quaternion.LookRotation(newForward, _characterUp); + } + + // Cancel out horizontal velocity upon landing on an attached rigidbody + if (GroundingStatus.GroundCollider && + GroundingStatus.GroundCollider.attachedRigidbody && + GroundingStatus.GroundCollider.attachedRigidbody == _attachedRigidbody && + _attachedRigidbody != null && + _lastAttachedRigidbody == null) + { + BaseVelocity -= Vector3.ProjectOnPlane(_attachedRigidbodyVelocity, _characterUp); + } + + // Movement from Attached Rigidbody + if (_attachedRigidbodyVelocity.sqrMagnitude > 0f) + { + _isMovingFromAttachedRigidbody = true; + + if (_solveMovementCollisions) + { + // Perform the move from rgdbdy velocity + InternalCharacterMove(ref _attachedRigidbodyVelocity, deltaTime); + } + else + { + _transientPosition += _attachedRigidbodyVelocity * deltaTime; + } + + _isMovingFromAttachedRigidbody = false; + } + #endregion + } + } + + /// + /// Update phase 2 is meant to be called after physics movers have simulated their goal positions/rotations. + /// At the end of this, the TransientPosition/Rotation values will be up-to-date with where the motor should be at the end of its move. + /// It is responsible for: + /// - Solving Rotation + /// - Handle MoveRotation calls + /// - Solving potential attached rigidbody overlaps + /// - Solving Velocity + /// - Applying planar constraint + /// + public void UpdatePhase2(float deltaTime) + { + // Handle rotation + CharacterController.UpdateRotation(ref _transientRotation, deltaTime); + TransientRotation = _transientRotation; + + // Handle move rotation + if (_moveRotationDirty) + { + TransientRotation = _moveRotationTarget; + _moveRotationDirty = false; + } + + if (_solveMovementCollisions && InteractiveRigidbodyHandling) + { + if (InteractiveRigidbodyHandling) + { + #region Solve potential attached rigidbody overlap + if (_attachedRigidbody) + { + float upwardsOffset = Capsule.radius; + + RaycastHit closestHit; + if (CharacterGroundSweep( + _transientPosition + (_characterUp * upwardsOffset), + _transientRotation, + -_characterUp, + upwardsOffset, + out closestHit)) + { + if (closestHit.collider.attachedRigidbody == _attachedRigidbody && IsStableOnNormal(closestHit.normal)) + { + float distanceMovedUp = (upwardsOffset - closestHit.distance); + _transientPosition = _transientPosition + (_characterUp * distanceMovedUp) + (_characterUp * CollisionOffset); + } + } + } + #endregion + } + + if (InteractiveRigidbodyHandling) + { + #region Resolve overlaps that could've been caused by rotation or physics movers simulation pushing the character + Vector3 resolutionDirection = _cachedWorldUp; + float resolutionDistance = 0f; + int iterationsMade = 0; + bool overlapSolved = false; + while (iterationsMade < MaxDecollisionIterations && !overlapSolved) + { + int nbOverlaps = CharacterCollisionsOverlap(_transientPosition, _transientRotation, _internalProbedColliders); + if (nbOverlaps > 0) + { + for (int i = 0; i < nbOverlaps; i++) + { + // Process overlap + Transform overlappedTransform = _internalProbedColliders[i].GetComponent(); + if (Physics.ComputePenetration( + Capsule, + _transientPosition, + _transientRotation, + _internalProbedColliders[i], + overlappedTransform.position, + overlappedTransform.rotation, + out resolutionDirection, + out resolutionDistance)) + { + // Resolve along obstruction direction + HitStabilityReport mockReport = new HitStabilityReport(); + mockReport.IsStable = IsStableOnNormal(resolutionDirection); + resolutionDirection = GetObstructionNormal(resolutionDirection, mockReport.IsStable); + + // Solve overlap + Vector3 resolutionMovement = resolutionDirection * (resolutionDistance + CollisionOffset); + _transientPosition += resolutionMovement; + + // If interactiveRigidbody, register as rigidbody hit for velocity + if (InteractiveRigidbodyHandling) + { + Rigidbody probedRigidbody = GetInteractiveRigidbody(_internalProbedColliders[i]); + if (probedRigidbody != null) + { + HitStabilityReport tmpReport = new HitStabilityReport(); + tmpReport.IsStable = IsStableOnNormal(resolutionDirection); + if (tmpReport.IsStable) + { + LastMovementIterationFoundAnyGround = tmpReport.IsStable; + } + if (probedRigidbody != _attachedRigidbody) + { + Vector3 characterCenter = _transientPosition + (_transientRotation * _characterTransformToCapsuleCenter); + Vector3 estimatedCollisionPoint = _transientPosition; + + + StoreRigidbodyHit( + probedRigidbody, + Velocity, + estimatedCollisionPoint, + resolutionDirection, + tmpReport); + } + } + } + + // Remember overlaps + if (_overlapsCount < _overlaps.Length) + { + _overlaps[_overlapsCount] = new OverlapResult(resolutionDirection, _internalProbedColliders[i]); + _overlapsCount++; + } + + break; + } + } + } + else + { + overlapSolved = true; + } + + iterationsMade++; + } + #endregion + } + } + + // Handle velocity + CharacterController.UpdateVelocity(ref BaseVelocity, deltaTime); + + //this.CharacterController.UpdateVelocity(ref BaseVelocity, deltaTime); + if (BaseVelocity.magnitude < MinVelocityMagnitude) + { + BaseVelocity = Vector3.zero; + } + + #region Calculate Character movement from base velocity + // Perform the move from base velocity + if (BaseVelocity.sqrMagnitude > 0f) + { + if (_solveMovementCollisions) + { + InternalCharacterMove(ref BaseVelocity, deltaTime); + } + else + { + _transientPosition += BaseVelocity * deltaTime; + } + } + + // Process rigidbody hits/overlaps to affect velocity + if (InteractiveRigidbodyHandling) + { + ProcessVelocityForRigidbodyHits(ref BaseVelocity, deltaTime); + } + #endregion + + // Handle planar constraint + if (HasPlanarConstraint) + { + _transientPosition = _initialSimulationPosition + Vector3.ProjectOnPlane(_transientPosition - _initialSimulationPosition, PlanarConstraintAxis.normalized); + } + + // Discrete collision detection + if (DiscreteCollisionEvents) + { + int nbOverlaps = CharacterCollisionsOverlap(_transientPosition, _transientRotation, _internalProbedColliders, CollisionOffset * 2f); + for (int i = 0; i < nbOverlaps; i++) + { + CharacterController.OnDiscreteCollisionDetected(_internalProbedColliders[i]); + } + } + + CharacterController.AfterCharacterUpdate(deltaTime); + } + + /// + /// Determines if motor can be considered stable on given slope normal + /// + private bool IsStableOnNormal(Vector3 normal) + { + return Vector3.Angle(_characterUp, normal) <= MaxStableSlopeAngle; + } + + /// + /// Determines if motor can be considered stable on given slope normal + /// + private bool IsStableWithSpecialCases(ref HitStabilityReport stabilityReport, Vector3 velocity) + { + if (LedgeAndDenivelationHandling) + { + if (stabilityReport.LedgeDetected) + { + if (stabilityReport.IsMovingTowardsEmptySideOfLedge) + { + // Max snap vel + Vector3 velocityOnLedgeNormal = Vector3.Project(velocity, stabilityReport.LedgeFacingDirection); + if (velocityOnLedgeNormal.magnitude >= MaxVelocityForLedgeSnap) + { + return false; + } + } + + // Distance from ledge + if (stabilityReport.IsOnEmptySideOfLedge && stabilityReport.DistanceFromLedge > MaxStableDistanceFromLedge) + { + return false; + } + } + + // "Launching" off of slopes of a certain denivelation angle + if (LastGroundingStatus.FoundAnyGround && stabilityReport.InnerNormal.sqrMagnitude != 0f && stabilityReport.OuterNormal.sqrMagnitude != 0f) + { + float denivelationAngle = Vector3.Angle(stabilityReport.InnerNormal, stabilityReport.OuterNormal); + if (denivelationAngle > MaxStableDenivelationAngle) + { + return false; + } + else + { + denivelationAngle = Vector3.Angle(LastGroundingStatus.InnerGroundNormal, stabilityReport.OuterNormal); + if (denivelationAngle > MaxStableDenivelationAngle) + { + return false; + } + } + } + } + + return true; + } + + /// + /// Probes for valid ground and midifies the input transientPosition if ground snapping occurs + /// + public void ProbeGround(ref Vector3 probingPosition, Quaternion atRotation, float probingDistance, ref CharacterGroundingReport groundingReport) + { + if (probingDistance < MinimumGroundProbingDistance) + { + probingDistance = MinimumGroundProbingDistance; + } + + int groundSweepsMade = 0; + RaycastHit groundSweepHit = new RaycastHit(); + bool groundSweepingIsOver = false; + Vector3 groundSweepPosition = probingPosition; + Vector3 groundSweepDirection = (atRotation * -_cachedWorldUp); + float groundProbeDistanceRemaining = probingDistance; + while (groundProbeDistanceRemaining > 0 && (groundSweepsMade <= MaxGroundingSweepIterations) && !groundSweepingIsOver) + { + // Sweep for ground detection + if (CharacterGroundSweep( + groundSweepPosition, // position + atRotation, // rotation + groundSweepDirection, // direction + groundProbeDistanceRemaining, // distance + out groundSweepHit)) // hit + { + Vector3 targetPosition = groundSweepPosition + (groundSweepDirection * groundSweepHit.distance); + HitStabilityReport groundHitStabilityReport = new HitStabilityReport(); + EvaluateHitStability(groundSweepHit.collider, groundSweepHit.normal, groundSweepHit.point, targetPosition, _transientRotation, BaseVelocity, ref groundHitStabilityReport); + + groundingReport.FoundAnyGround = true; + groundingReport.GroundNormal = groundSweepHit.normal; + groundingReport.InnerGroundNormal = groundHitStabilityReport.InnerNormal; + groundingReport.OuterGroundNormal = groundHitStabilityReport.OuterNormal; + groundingReport.GroundCollider = groundSweepHit.collider; + groundingReport.GroundPoint = groundSweepHit.point; + groundingReport.SnappingPrevented = false; + + // Found stable ground + if (groundHitStabilityReport.IsStable) + { + // Find all scenarios where ground snapping should be canceled + groundingReport.SnappingPrevented = !IsStableWithSpecialCases(ref groundHitStabilityReport, BaseVelocity); + + groundingReport.IsStableOnGround = true; + + // Ground snapping + if (!groundingReport.SnappingPrevented) + { + probingPosition = groundSweepPosition + (groundSweepDirection * (groundSweepHit.distance - CollisionOffset)); + } + + CharacterController.OnGroundHit(groundSweepHit.collider, groundSweepHit.normal, groundSweepHit.point, ref groundHitStabilityReport); + groundSweepingIsOver = true; + } + else + { + // Calculate movement from this iteration and advance position + Vector3 sweepMovement = (groundSweepDirection * groundSweepHit.distance) + ((atRotation * _cachedWorldUp) * Mathf.Max(CollisionOffset, groundSweepHit.distance)); + groundSweepPosition = groundSweepPosition + sweepMovement; + + // Set remaining distance + groundProbeDistanceRemaining = Mathf.Min(GroundProbeReboundDistance, Mathf.Max(groundProbeDistanceRemaining - sweepMovement.magnitude, 0f)); + + // Reorient direction + groundSweepDirection = Vector3.ProjectOnPlane(groundSweepDirection, groundSweepHit.normal).normalized; + } + } + else + { + groundSweepingIsOver = true; + } + + groundSweepsMade++; + } + } + + /// + /// Forces the character to unground itself on its next grounding update + /// + public void ForceUnground(float time = 0.1f) + { + _mustUnground = true; + _mustUngroundTimeCounter = time; + } + + public bool MustUnground() + { + return _mustUnground || _mustUngroundTimeCounter > 0f; + } + + /// + /// Returns the direction adjusted to be tangent to a specified surface normal relatively to the character's up direction. + /// Useful for reorienting a direction on a slope without any lateral deviation in trajectory + /// + public Vector3 GetDirectionTangentToSurface(Vector3 direction, Vector3 surfaceNormal) + { + Vector3 directionRight = Vector3.Cross(direction, _characterUp); + return Vector3.Cross(surfaceNormal, directionRight).normalized; + } + + /// + /// Moves the character's position by given movement while taking into account all physics simulation, step-handling and + /// velocity projection rules that affect the character motor + /// + /// Returns false if movement could not be solved until the end + private bool InternalCharacterMove(ref Vector3 transientVelocity, float deltaTime) + { + if (deltaTime <= 0f) + return false; + + // Planar constraint + if (HasPlanarConstraint) + { + transientVelocity = Vector3.ProjectOnPlane(transientVelocity, PlanarConstraintAxis.normalized); + } + + bool wasCompleted = true; + Vector3 remainingMovementDirection = transientVelocity.normalized; + float remainingMovementMagnitude = transientVelocity.magnitude * deltaTime; + Vector3 originalVelocityDirection = remainingMovementDirection; + int sweepsMade = 0; + bool hitSomethingThisSweepIteration = true; + Vector3 tmpMovedPosition = _transientPosition; + bool previousHitIsStable = false; + Vector3 previousVelocity = _cachedZeroVector; + Vector3 previousObstructionNormal = _cachedZeroVector; + MovementSweepState sweepState = MovementSweepState.Initial; + + // Project movement against current overlaps before doing the sweeps + for (int i = 0; i < _overlapsCount; i++) + { + Vector3 overlapNormal = _overlaps[i].Normal; + if (Vector3.Dot(remainingMovementDirection, overlapNormal) < 0f) + { + bool stableOnHit = IsStableOnNormal(overlapNormal) && !MustUnground(); + Vector3 velocityBeforeProjection = transientVelocity; + Vector3 obstructionNormal = GetObstructionNormal(overlapNormal, stableOnHit); + + InternalHandleVelocityProjection( + stableOnHit, + overlapNormal, + obstructionNormal, + originalVelocityDirection, + ref sweepState, + previousHitIsStable, + previousVelocity, + previousObstructionNormal, + ref transientVelocity, + ref remainingMovementMagnitude, + ref remainingMovementDirection); + + previousHitIsStable = stableOnHit; + previousVelocity = velocityBeforeProjection; + previousObstructionNormal = obstructionNormal; + } + } + + // Sweep the desired movement to detect collisions + while (remainingMovementMagnitude > 0f && + (sweepsMade <= MaxMovementIterations) && + hitSomethingThisSweepIteration) + { + bool foundClosestHit = false; + Vector3 closestSweepHitPoint = default; + Vector3 closestSweepHitNormal = default; + float closestSweepHitDistance = 0f; + Collider closestSweepHitCollider = null; + + if (CheckMovementInitialOverlaps) + { + int numOverlaps = CharacterCollisionsOverlap( + tmpMovedPosition, + _transientRotation, + _internalProbedColliders, + 0f, + false); + if (numOverlaps > 0) + { + closestSweepHitDistance = 0f; + + float mostObstructingOverlapNormalDotProduct = 2f; + + for (int i = 0; i < numOverlaps; i++) + { + Collider tmpCollider = _internalProbedColliders[i]; + + if (Physics.ComputePenetration( + Capsule, + tmpMovedPosition, + _transientRotation, + tmpCollider, + tmpCollider.transform.position, + tmpCollider.transform.rotation, + out Vector3 resolutionDirection, + out float resolutionDistance)) + { + float dotProduct = Vector3.Dot(remainingMovementDirection, resolutionDirection); + if (dotProduct < 0f && dotProduct < mostObstructingOverlapNormalDotProduct) + { + mostObstructingOverlapNormalDotProduct = dotProduct; + + closestSweepHitNormal = resolutionDirection; + closestSweepHitCollider = tmpCollider; + closestSweepHitPoint = tmpMovedPosition + (_transientRotation * CharacterTransformToCapsuleCenter) + (resolutionDirection * resolutionDistance); + + foundClosestHit = true; + } + } + } + } + } + + if (!foundClosestHit && CharacterCollisionsSweep( + tmpMovedPosition, // position + _transientRotation, // rotation + remainingMovementDirection, // direction + remainingMovementMagnitude + CollisionOffset, // distance + out RaycastHit closestSweepHit, // closest hit + _internalCharacterHits) // all hits + > 0) + { + closestSweepHitNormal = closestSweepHit.normal; + closestSweepHitDistance = closestSweepHit.distance; + closestSweepHitCollider = closestSweepHit.collider; + closestSweepHitPoint = closestSweepHit.point; + + foundClosestHit = true; + } + + if (foundClosestHit) + { + // Calculate movement from this iteration + Vector3 sweepMovement = (remainingMovementDirection * (Mathf.Max(0f, closestSweepHitDistance - CollisionOffset))); + tmpMovedPosition += sweepMovement; + remainingMovementMagnitude -= sweepMovement.magnitude; + + // Evaluate if hit is stable + HitStabilityReport moveHitStabilityReport = new HitStabilityReport(); + EvaluateHitStability(closestSweepHitCollider, closestSweepHitNormal, closestSweepHitPoint, tmpMovedPosition, _transientRotation, transientVelocity, ref moveHitStabilityReport); + + // Handle stepping up steps points higher than bottom capsule radius + bool foundValidStepHit = false; + if (_solveGrounding && StepHandling != StepHandlingMethod.None && moveHitStabilityReport.ValidStepDetected) + { + float obstructionCorrelation = Mathf.Abs(Vector3.Dot(closestSweepHitNormal, _characterUp)); + if (obstructionCorrelation <= CorrelationForVerticalObstruction) + { + Vector3 stepForwardDirection = Vector3.ProjectOnPlane(-closestSweepHitNormal, _characterUp).normalized; + Vector3 stepCastStartPoint = (tmpMovedPosition + (stepForwardDirection * SteppingForwardDistance)) + + (_characterUp * MaxStepHeight); + + // Cast downward from the top of the stepping height + int nbStepHits = CharacterCollisionsSweep( + stepCastStartPoint, // position + _transientRotation, // rotation + -_characterUp, // direction + MaxStepHeight, // distance + out RaycastHit closestStepHit, // closest hit + _internalCharacterHits, + 0f, + true); // all hits + + // Check for hit corresponding to stepped collider + for (int i = 0; i < nbStepHits; i++) + { + if (_internalCharacterHits[i].collider == moveHitStabilityReport.SteppedCollider) + { + Vector3 endStepPosition = stepCastStartPoint + (-_characterUp * (_internalCharacterHits[i].distance - CollisionOffset)); + tmpMovedPosition = endStepPosition; + foundValidStepHit = true; + + // Project velocity on ground normal at step + transientVelocity = Vector3.ProjectOnPlane(transientVelocity, CharacterUp); + remainingMovementDirection = transientVelocity.normalized; + + break; + } + } + } + } + + // Handle movement solving + if (!foundValidStepHit) + { + Vector3 obstructionNormal = GetObstructionNormal(closestSweepHitNormal, moveHitStabilityReport.IsStable); + + // Movement hit callback + CharacterController.OnMovementHit(closestSweepHitCollider, closestSweepHitNormal, closestSweepHitPoint, ref moveHitStabilityReport); + + // Handle remembering rigidbody hits + if (InteractiveRigidbodyHandling && closestSweepHitCollider.attachedRigidbody) + { + StoreRigidbodyHit( + closestSweepHitCollider.attachedRigidbody, + transientVelocity, + closestSweepHitPoint, + obstructionNormal, + moveHitStabilityReport); + } + + bool stableOnHit = moveHitStabilityReport.IsStable && !MustUnground(); + Vector3 velocityBeforeProj = transientVelocity; + + // Project velocity for next iteration + InternalHandleVelocityProjection( + stableOnHit, + closestSweepHitNormal, + obstructionNormal, + originalVelocityDirection, + ref sweepState, + previousHitIsStable, + previousVelocity, + previousObstructionNormal, + ref transientVelocity, + ref remainingMovementMagnitude, + ref remainingMovementDirection); + + previousHitIsStable = stableOnHit; + previousVelocity = velocityBeforeProj; + previousObstructionNormal = obstructionNormal; + } + } + // If we hit nothing... + else + { + hitSomethingThisSweepIteration = false; + } + + // Safety for exceeding max sweeps allowed + sweepsMade++; + if (sweepsMade > MaxMovementIterations) + { + if (KillRemainingMovementWhenExceedMaxMovementIterations) + { + remainingMovementMagnitude = 0f; + } + + if (KillVelocityWhenExceedMaxMovementIterations) + { + transientVelocity = Vector3.zero; + } + wasCompleted = false; + } + } + + // Move position for the remainder of the movement + tmpMovedPosition += (remainingMovementDirection * remainingMovementMagnitude); + _transientPosition = tmpMovedPosition; + + return wasCompleted; + } + + /// + /// Gets the effective normal for movement obstruction depending on current grounding status + /// + private Vector3 GetObstructionNormal(Vector3 hitNormal, bool stableOnHit) + { + // Find hit/obstruction/offset normal + Vector3 obstructionNormal = hitNormal; + if (GroundingStatus.IsStableOnGround && !MustUnground() && !stableOnHit) + { + Vector3 obstructionLeftAlongGround = Vector3.Cross(GroundingStatus.GroundNormal, obstructionNormal).normalized; + obstructionNormal = Vector3.Cross(obstructionLeftAlongGround, _characterUp).normalized; + } + + // Catch cases where cross product between parallel normals returned 0 + if (obstructionNormal.sqrMagnitude == 0f) + { + obstructionNormal = hitNormal; + } + + return obstructionNormal; + } + + /// + /// Remembers a rigidbody hit for processing later + /// + private void StoreRigidbodyHit(Rigidbody hitRigidbody, Vector3 hitVelocity, Vector3 hitPoint, Vector3 obstructionNormal, HitStabilityReport hitStabilityReport) + { + if (_rigidbodyProjectionHitCount < _internalRigidbodyProjectionHits.Length) + { + if (!hitRigidbody.GetComponent()) + { + RigidbodyProjectionHit rph = new RigidbodyProjectionHit(); + rph.Rigidbody = hitRigidbody; + rph.HitPoint = hitPoint; + rph.EffectiveHitNormal = obstructionNormal; + rph.HitVelocity = hitVelocity; + rph.StableOnHit = hitStabilityReport.IsStable; + + _internalRigidbodyProjectionHits[_rigidbodyProjectionHitCount] = rph; + _rigidbodyProjectionHitCount++; + } + } + } + + public void SetTransientPosition(Vector3 newPos) + { + _transientPosition = newPos; + } + + /// + /// Processes movement projection upon detecting a hit + /// + private void InternalHandleVelocityProjection(bool stableOnHit, Vector3 hitNormal, Vector3 obstructionNormal, Vector3 originalDirection, + ref MovementSweepState sweepState, bool previousHitIsStable, Vector3 previousVelocity, Vector3 previousObstructionNormal, + ref Vector3 transientVelocity, ref float remainingMovementMagnitude, ref Vector3 remainingMovementDirection) + { + if (transientVelocity.sqrMagnitude <= 0f) + { + return; + } + + Vector3 velocityBeforeProjection = transientVelocity; + + if (stableOnHit) + { + LastMovementIterationFoundAnyGround = true; + HandleVelocityProjection(ref transientVelocity, obstructionNormal, stableOnHit); + } + else + { + // Handle projection + if (sweepState == MovementSweepState.Initial) + { + HandleVelocityProjection(ref transientVelocity, obstructionNormal, stableOnHit); + sweepState = MovementSweepState.AfterFirstHit; + } + // Blocking crease handling + else if (sweepState == MovementSweepState.AfterFirstHit) + { + EvaluateCrease( + transientVelocity, + previousVelocity, + obstructionNormal, + previousObstructionNormal, + stableOnHit, + previousHitIsStable, + GroundingStatus.IsStableOnGround && !MustUnground(), + out bool foundCrease, + out Vector3 creaseDirection); + + if (foundCrease) + { + if (GroundingStatus.IsStableOnGround && !MustUnground()) + { + transientVelocity = Vector3.zero; + sweepState = MovementSweepState.FoundBlockingCorner; + } + else + { + transientVelocity = Vector3.Project(transientVelocity, creaseDirection); + sweepState = MovementSweepState.FoundBlockingCrease; + } + } + else + { + HandleVelocityProjection(ref transientVelocity, obstructionNormal, stableOnHit); + } + } + // Blocking corner handling + else if (sweepState == MovementSweepState.FoundBlockingCrease) + { + transientVelocity = Vector3.zero; + sweepState = MovementSweepState.FoundBlockingCorner; + } + } + + if (HasPlanarConstraint) + { + transientVelocity = Vector3.ProjectOnPlane(transientVelocity, PlanarConstraintAxis.normalized); + } + + float newVelocityFactor = transientVelocity.magnitude / velocityBeforeProjection.magnitude; + remainingMovementMagnitude *= newVelocityFactor; + remainingMovementDirection = transientVelocity.normalized; + } + + private void EvaluateCrease( + Vector3 currentCharacterVelocity, + Vector3 previousCharacterVelocity, + Vector3 currentHitNormal, + Vector3 previousHitNormal, + bool currentHitIsStable, + bool previousHitIsStable, + bool characterIsStable, + out bool isValidCrease, + out Vector3 creaseDirection) + { + isValidCrease = false; + creaseDirection = default; + + if (!characterIsStable || !currentHitIsStable || !previousHitIsStable) + { + Vector3 tmpBlockingCreaseDirection = Vector3.Cross(currentHitNormal, previousHitNormal).normalized; + float dotPlanes = Vector3.Dot(currentHitNormal, previousHitNormal); + bool isVelocityConstrainedByCrease = false; + + // Avoid calculations if the two planes are the same + if (dotPlanes < 0.999f) + { + // TODO: can this whole part be made simpler? (with 2d projections, etc) + Vector3 normalAOnCreasePlane = Vector3.ProjectOnPlane(currentHitNormal, tmpBlockingCreaseDirection).normalized; + Vector3 normalBOnCreasePlane = Vector3.ProjectOnPlane(previousHitNormal, tmpBlockingCreaseDirection).normalized; + float dotPlanesOnCreasePlane = Vector3.Dot(normalAOnCreasePlane, normalBOnCreasePlane); + + Vector3 enteringVelocityDirectionOnCreasePlane = Vector3.ProjectOnPlane(previousCharacterVelocity, tmpBlockingCreaseDirection).normalized; + + if (dotPlanesOnCreasePlane <= (Vector3.Dot(-enteringVelocityDirectionOnCreasePlane, normalAOnCreasePlane) + 0.001f) && + dotPlanesOnCreasePlane <= (Vector3.Dot(-enteringVelocityDirectionOnCreasePlane, normalBOnCreasePlane) + 0.001f)) + { + isVelocityConstrainedByCrease = true; + } + } + + if (isVelocityConstrainedByCrease) + { + // Flip crease direction to make it representative of the real direction our velocity would be projected to + if (Vector3.Dot(tmpBlockingCreaseDirection, currentCharacterVelocity) < 0f) + { + tmpBlockingCreaseDirection = -tmpBlockingCreaseDirection; + } + + isValidCrease = true; + creaseDirection = tmpBlockingCreaseDirection; + } + } + } + + /// + /// Allows you to override the way velocity is projected on an obstruction + /// + public virtual void HandleVelocityProjection(ref Vector3 velocity, Vector3 obstructionNormal, bool stableOnHit) + { + if (GroundingStatus.IsStableOnGround && !MustUnground()) + { + // On stable slopes, simply reorient the movement without any loss + if (stableOnHit) + { + velocity = GetDirectionTangentToSurface(velocity, obstructionNormal) * velocity.magnitude; + } + // On blocking hits, project the movement on the obstruction while following the grounding plane + else + { + Vector3 obstructionRightAlongGround = Vector3.Cross(obstructionNormal, GroundingStatus.GroundNormal).normalized; + Vector3 obstructionUpAlongGround = Vector3.Cross(obstructionRightAlongGround, obstructionNormal).normalized; + velocity = GetDirectionTangentToSurface(velocity, obstructionUpAlongGround) * velocity.magnitude; + velocity = Vector3.ProjectOnPlane(velocity, obstructionNormal); + } + } + else + { + if (stableOnHit) + { + // Handle stable landing + velocity = Vector3.ProjectOnPlane(velocity, CharacterUp); + velocity = GetDirectionTangentToSurface(velocity, obstructionNormal) * velocity.magnitude; + } + // Handle generic obstruction + else + { + velocity = Vector3.ProjectOnPlane(velocity, obstructionNormal); + } + } + } + + /// + /// Allows you to override the way hit rigidbodies are pushed / interacted with. + /// ProcessedVelocity is what must be modified if this interaction affects the character's velocity. + /// + public virtual void HandleSimulatedRigidbodyInteraction(ref Vector3 processedVelocity, RigidbodyProjectionHit hit, float deltaTime) + { + } + + /// + /// Takes into account rigidbody hits for adding to the velocity + /// + private void ProcessVelocityForRigidbodyHits(ref Vector3 processedVelocity, float deltaTime) + { + for (int i = 0; i < _rigidbodyProjectionHitCount; i++) + { + RigidbodyProjectionHit bodyHit = _internalRigidbodyProjectionHits[i]; + + if (bodyHit.Rigidbody && !_rigidbodiesPushedThisMove.Contains(bodyHit.Rigidbody)) + { + if (_internalRigidbodyProjectionHits[i].Rigidbody != _attachedRigidbody) + { + // Remember we hit this rigidbody + _rigidbodiesPushedThisMove.Add(bodyHit.Rigidbody); + + float characterMass = SimulatedCharacterMass; + Vector3 characterVelocity = bodyHit.HitVelocity; + + KinematicCharacterMotor hitCharacterMotor = bodyHit.Rigidbody.GetComponent(); + bool hitBodyIsCharacter = hitCharacterMotor != null; + bool hitBodyIsDynamic = !bodyHit.Rigidbody.isKinematic; + float hitBodyMass = bodyHit.Rigidbody.mass; + float hitBodyMassAtPoint = bodyHit.Rigidbody.mass; // todo + Vector3 hitBodyVelocity = bodyHit.Rigidbody.velocity; + if (hitBodyIsCharacter) + { + hitBodyMass = hitCharacterMotor.SimulatedCharacterMass; + hitBodyMassAtPoint = hitCharacterMotor.SimulatedCharacterMass; // todo + hitBodyVelocity = hitCharacterMotor.BaseVelocity; + } + else if (!hitBodyIsDynamic) + { + PhysicsMover physicsMover = bodyHit.Rigidbody.GetComponent(); + if(physicsMover) + { + hitBodyVelocity = physicsMover.Velocity; + } + } + + // Calculate the ratio of the total mass that the character mass represents + float characterToBodyMassRatio = 1f; + { + if (characterMass + hitBodyMassAtPoint > 0f) + { + characterToBodyMassRatio = characterMass / (characterMass + hitBodyMassAtPoint); + } + else + { + characterToBodyMassRatio = 0.5f; + } + + // Hitting a non-dynamic body + if (!hitBodyIsDynamic) + { + characterToBodyMassRatio = 0f; + } + // Emulate kinematic body interaction + else if (RigidbodyInteractionType == RigidbodyInteractionType.Kinematic && !hitBodyIsCharacter) + { + characterToBodyMassRatio = 1f; + } + } + + ComputeCollisionResolutionForHitBody( + bodyHit.EffectiveHitNormal, + characterVelocity, + hitBodyVelocity, + characterToBodyMassRatio, + out Vector3 velocityChangeOnCharacter, + out Vector3 velocityChangeOnBody); + + processedVelocity += velocityChangeOnCharacter; + + if (hitBodyIsCharacter) + { + hitCharacterMotor.BaseVelocity += velocityChangeOnCharacter; + } + else if (hitBodyIsDynamic) + { + bodyHit.Rigidbody.AddForceAtPosition(velocityChangeOnBody, bodyHit.HitPoint, ForceMode.VelocityChange); + } + + if (RigidbodyInteractionType == RigidbodyInteractionType.SimulatedDynamic) + { + HandleSimulatedRigidbodyInteraction(ref processedVelocity, bodyHit, deltaTime); + } + } + } + } + + } + + public void ComputeCollisionResolutionForHitBody( + Vector3 hitNormal, + Vector3 characterVelocity, + Vector3 bodyVelocity, + float characterToBodyMassRatio, + out Vector3 velocityChangeOnCharacter, + out Vector3 velocityChangeOnBody) + { + velocityChangeOnCharacter = default; + velocityChangeOnBody = default; + + float bodyToCharacterMassRatio = 1f - characterToBodyMassRatio; + float characterVelocityMagnitudeOnHitNormal = Vector3.Dot(characterVelocity, hitNormal); + float bodyVelocityMagnitudeOnHitNormal = Vector3.Dot(bodyVelocity, hitNormal); + + // if character velocity was going against the obstruction, restore the portion of the velocity that got projected during the movement phase + if (characterVelocityMagnitudeOnHitNormal < 0f) + { + Vector3 restoredCharacterVelocity = hitNormal * characterVelocityMagnitudeOnHitNormal; + velocityChangeOnCharacter += restoredCharacterVelocity; + } + + // solve impulse velocities on both bodies, but only if the body velocity would be giving resistance to the character in any way + if (bodyVelocityMagnitudeOnHitNormal > characterVelocityMagnitudeOnHitNormal) + { + Vector3 relativeImpactVelocity = hitNormal * (bodyVelocityMagnitudeOnHitNormal - characterVelocityMagnitudeOnHitNormal); + velocityChangeOnCharacter += relativeImpactVelocity * bodyToCharacterMassRatio; + velocityChangeOnBody += -relativeImpactVelocity * characterToBodyMassRatio; + } + } + + /// + /// Determines if the input collider is valid for collision processing + /// + /// Returns true if the collider is valid + private bool CheckIfColliderValidForCollisions(Collider coll) + { + // Ignore self + if (coll == Capsule) + { + return false; + } + + if (!InternalIsColliderValidForCollisions(coll)) + { + return false; + } + + return true; + } + + /// + /// Determines if the input collider is valid for collision processing + /// + private bool InternalIsColliderValidForCollisions(Collider coll) + { + Rigidbody colliderAttachedRigidbody = coll.attachedRigidbody; + if (colliderAttachedRigidbody) + { + bool isRigidbodyKinematic = colliderAttachedRigidbody.isKinematic; + + // If movement is made from AttachedRigidbody, ignore the AttachedRigidbody + if (_isMovingFromAttachedRigidbody && (!isRigidbodyKinematic || colliderAttachedRigidbody == _attachedRigidbody)) + { + return false; + } + + // don't collide with dynamic rigidbodies if our RigidbodyInteractionType is kinematic + if (RigidbodyInteractionType == RigidbodyInteractionType.Kinematic && !isRigidbodyKinematic) + { + // wake up rigidbody + if (coll.attachedRigidbody) + { + coll.attachedRigidbody.WakeUp(); + } + + return false; + } + } + + // Custom checks + bool colliderValid = CharacterController.IsColliderValidForCollisions(coll); + if (!colliderValid) + { + return false; + } + + return true; + } + + /// + /// Determines if the motor is considered stable on a given hit + /// + public void EvaluateHitStability(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, Vector3 atCharacterPosition, Quaternion atCharacterRotation, Vector3 withCharacterVelocity, ref HitStabilityReport stabilityReport) + { + if (!_solveGrounding) + { + stabilityReport.IsStable = false; + return; + } + + Vector3 atCharacterUp = atCharacterRotation * _cachedWorldUp; + Vector3 innerHitDirection = Vector3.ProjectOnPlane(hitNormal, atCharacterUp).normalized; + + stabilityReport.IsStable = this.IsStableOnNormal(hitNormal); + + stabilityReport.FoundInnerNormal = false; + stabilityReport.FoundOuterNormal = false; + stabilityReport.InnerNormal = hitNormal; + stabilityReport.OuterNormal = hitNormal; + + // Ledge handling + if (LedgeAndDenivelationHandling) + { + float ledgeCheckHeight = MinDistanceForLedge; + if (StepHandling != StepHandlingMethod.None) + { + ledgeCheckHeight = MaxStepHeight; + } + + bool isStableLedgeInner = false; + bool isStableLedgeOuter = false; + + if (CharacterCollisionsRaycast( + hitPoint + (atCharacterUp * SecondaryProbesVertical) + (innerHitDirection * SecondaryProbesHorizontal), + -atCharacterUp, + ledgeCheckHeight + SecondaryProbesVertical, + out RaycastHit innerLedgeHit, + _internalCharacterHits) > 0) + { + Vector3 innerLedgeNormal = innerLedgeHit.normal; + stabilityReport.InnerNormal = innerLedgeNormal; + stabilityReport.FoundInnerNormal = true; + isStableLedgeInner = IsStableOnNormal(innerLedgeNormal); + } + + if (CharacterCollisionsRaycast( + hitPoint + (atCharacterUp * SecondaryProbesVertical) + (-innerHitDirection * SecondaryProbesHorizontal), + -atCharacterUp, + ledgeCheckHeight + SecondaryProbesVertical, + out RaycastHit outerLedgeHit, + _internalCharacterHits) > 0) + { + Vector3 outerLedgeNormal = outerLedgeHit.normal; + stabilityReport.OuterNormal = outerLedgeNormal; + stabilityReport.FoundOuterNormal = true; + isStableLedgeOuter = IsStableOnNormal(outerLedgeNormal); + } + + stabilityReport.LedgeDetected = (isStableLedgeInner != isStableLedgeOuter); + if (stabilityReport.LedgeDetected) + { + stabilityReport.IsOnEmptySideOfLedge = isStableLedgeOuter && !isStableLedgeInner; + stabilityReport.LedgeGroundNormal = isStableLedgeOuter ? stabilityReport.OuterNormal : stabilityReport.InnerNormal; + stabilityReport.LedgeRightDirection = Vector3.Cross(hitNormal, stabilityReport.LedgeGroundNormal).normalized; + stabilityReport.LedgeFacingDirection = Vector3.ProjectOnPlane(Vector3.Cross(stabilityReport.LedgeGroundNormal, stabilityReport.LedgeRightDirection), CharacterUp).normalized; + stabilityReport.DistanceFromLedge = Vector3.ProjectOnPlane((hitPoint - (atCharacterPosition + (atCharacterRotation * _characterTransformToCapsuleBottom))), atCharacterUp).magnitude; + stabilityReport.IsMovingTowardsEmptySideOfLedge = Vector3.Dot(withCharacterVelocity.normalized, stabilityReport.LedgeFacingDirection) > 0f; + } + + if (stabilityReport.IsStable) + { + stabilityReport.IsStable = IsStableWithSpecialCases(ref stabilityReport, withCharacterVelocity); + } + } + + // Step handling + if (StepHandling != StepHandlingMethod.None && !stabilityReport.IsStable) + { + // Stepping not supported on dynamic rigidbodies + Rigidbody hitRigidbody = hitCollider.attachedRigidbody; + if (!(hitRigidbody && !hitRigidbody.isKinematic)) + { + DetectSteps(atCharacterPosition, atCharacterRotation, hitPoint, innerHitDirection, ref stabilityReport); + + if (stabilityReport.ValidStepDetected) + { + stabilityReport.IsStable = true; + } + } + } + + CharacterController.ProcessHitStabilityReport(hitCollider, hitNormal, hitPoint, atCharacterPosition, atCharacterRotation, ref stabilityReport); + } + + private void DetectSteps(Vector3 characterPosition, Quaternion characterRotation, Vector3 hitPoint, Vector3 innerHitDirection, ref HitStabilityReport stabilityReport) + { + int nbStepHits = 0; + Collider tmpCollider; + RaycastHit outerStepHit; + Vector3 characterUp = characterRotation * _cachedWorldUp; + Vector3 verticalCharToHit = Vector3.Project((hitPoint - characterPosition), characterUp); + Vector3 horizontalCharToHitDirection = Vector3.ProjectOnPlane((hitPoint - characterPosition), characterUp).normalized; + Vector3 stepCheckStartPos = (hitPoint - verticalCharToHit) + (characterUp * MaxStepHeight) + (horizontalCharToHitDirection * CollisionOffset * 3f); + + // Do outer step check with capsule cast on hit point + nbStepHits = CharacterCollisionsSweep( + stepCheckStartPos, + characterRotation, + -characterUp, + MaxStepHeight + CollisionOffset, + out outerStepHit, + _internalCharacterHits, + 0f, + true); + + // Check for overlaps and obstructions at the hit position + if (CheckStepValidity(nbStepHits, characterPosition, characterRotation, innerHitDirection, stepCheckStartPos, out tmpCollider)) + { + stabilityReport.ValidStepDetected = true; + stabilityReport.SteppedCollider = tmpCollider; + } + + if (StepHandling == StepHandlingMethod.Extra && !stabilityReport.ValidStepDetected) + { + // Do min reach step check with capsule cast on hit point + stepCheckStartPos = characterPosition + (characterUp * MaxStepHeight) + (-innerHitDirection * MinRequiredStepDepth); + nbStepHits = CharacterCollisionsSweep( + stepCheckStartPos, + characterRotation, + -characterUp, + MaxStepHeight - CollisionOffset, + out outerStepHit, + _internalCharacterHits, + 0f, + true); + + // Check for overlaps and obstructions at the hit position + if (CheckStepValidity(nbStepHits, characterPosition, characterRotation, innerHitDirection, stepCheckStartPos, out tmpCollider)) + { + stabilityReport.ValidStepDetected = true; + stabilityReport.SteppedCollider = tmpCollider; + } + } + } + + private bool CheckStepValidity(int nbStepHits, Vector3 characterPosition, Quaternion characterRotation, Vector3 innerHitDirection, Vector3 stepCheckStartPos, out Collider hitCollider) + { + hitCollider = null; + Vector3 characterUp = characterRotation * Vector3.up; + + // Find the farthest valid hit for stepping + bool foundValidStepPosition = false; + + while (nbStepHits > 0 && !foundValidStepPosition) + { + // Get farthest hit among the remaining hits + RaycastHit farthestHit = new RaycastHit(); + float farthestDistance = 0f; + int farthestIndex = 0; + for (int i = 0; i < nbStepHits; i++) + { + float hitDistance = _internalCharacterHits[i].distance; + if (hitDistance > farthestDistance) + { + farthestDistance = hitDistance; + farthestHit = _internalCharacterHits[i]; + farthestIndex = i; + } + } + + Vector3 characterPositionAtHit = stepCheckStartPos + (-characterUp * (farthestHit.distance - CollisionOffset)); + + int atStepOverlaps = CharacterCollisionsOverlap(characterPositionAtHit, characterRotation, _internalProbedColliders); + if (atStepOverlaps <= 0) + { + // Check for outer hit slope normal stability at the step position + if (CharacterCollisionsRaycast( + farthestHit.point + (characterUp * SecondaryProbesVertical) + (-innerHitDirection * SecondaryProbesHorizontal), + -characterUp, + MaxStepHeight + SecondaryProbesVertical, + out RaycastHit outerSlopeHit, + _internalCharacterHits, + true) > 0) + { + if (IsStableOnNormal(outerSlopeHit.normal)) + { + // Cast upward to detect any obstructions to moving there + if (CharacterCollisionsSweep( + characterPosition, // position + characterRotation, // rotation + characterUp, // direction + MaxStepHeight - farthestHit.distance, // distance + out RaycastHit tmpUpObstructionHit, // closest hit + _internalCharacterHits) // all hits + <= 0) + { + // Do inner step check... + bool innerStepValid = false; + RaycastHit innerStepHit; + + if (AllowSteppingWithoutStableGrounding) + { + innerStepValid = true; + } + else + { + // At the capsule center at the step height + if (CharacterCollisionsRaycast( + characterPosition + Vector3.Project((characterPositionAtHit - characterPosition), characterUp), + -characterUp, + MaxStepHeight, + out innerStepHit, + _internalCharacterHits, + true) > 0) + { + if (IsStableOnNormal(innerStepHit.normal)) + { + innerStepValid = true; + } + } + } + + if (!innerStepValid) + { + // At inner step of the step point + if (CharacterCollisionsRaycast( + farthestHit.point + (innerHitDirection * SecondaryProbesHorizontal), + -characterUp, + MaxStepHeight, + out innerStepHit, + _internalCharacterHits, + true) > 0) + { + if (IsStableOnNormal(innerStepHit.normal)) + { + innerStepValid = true; + } + } + } + + // Final validation of step + if (innerStepValid) + { + hitCollider = farthestHit.collider; + foundValidStepPosition = true; + return true; + } + } + } + } + } + + // Discard hit if not valid step + if (!foundValidStepPosition) + { + nbStepHits--; + if (farthestIndex < nbStepHits) + { + _internalCharacterHits[farthestIndex] = _internalCharacterHits[nbStepHits]; + } + } + } + + return false; + } + + /// + /// Get true linear velocity (taking into account rotational velocity) on a given point of a rigidbody + /// + public void GetVelocityFromRigidbodyMovement(Rigidbody interactiveRigidbody, Vector3 atPoint, float deltaTime, out Vector3 linearVelocity, out Vector3 angularVelocity) + { + if (deltaTime > 0f) + { + linearVelocity = interactiveRigidbody.velocity; + angularVelocity = interactiveRigidbody.angularVelocity; + if(interactiveRigidbody.isKinematic) + { + PhysicsMover physicsMover = interactiveRigidbody.GetComponent(); + if (physicsMover) + { + linearVelocity = physicsMover.Velocity; + angularVelocity = physicsMover.AngularVelocity; + } + } + + if (angularVelocity != Vector3.zero) + { + Vector3 centerOfRotation = interactiveRigidbody.transform.TransformPoint(interactiveRigidbody.centerOfMass); + + Vector3 centerOfRotationToPoint = atPoint - centerOfRotation; + Quaternion rotationFromInteractiveRigidbody = Quaternion.Euler(Mathf.Rad2Deg * angularVelocity * deltaTime); + Vector3 finalPointPosition = centerOfRotation + (rotationFromInteractiveRigidbody * centerOfRotationToPoint); + linearVelocity += (finalPointPosition - atPoint) / deltaTime; + } + } + else + { + linearVelocity = default; + angularVelocity = default; + return; + } + } + + /// + /// Determines if a collider has an attached interactive rigidbody + /// + private Rigidbody GetInteractiveRigidbody(Collider onCollider) + { + Rigidbody colliderAttachedRigidbody = onCollider.attachedRigidbody; + if (colliderAttachedRigidbody) + { + if (colliderAttachedRigidbody.gameObject.GetComponent()) + { + return colliderAttachedRigidbody; + } + + if (!colliderAttachedRigidbody.isKinematic) + { + return colliderAttachedRigidbody; + } + } + return null; + } + + /// + /// Calculates the velocity required to move the character to the target position over a specific deltaTime. + /// Useful for when you wish to work with positions rather than velocities in the UpdateVelocity callback + /// + public Vector3 GetVelocityForMovePosition(Vector3 fromPosition, Vector3 toPosition, float deltaTime) + { + return GetVelocityFromMovement(toPosition - fromPosition, deltaTime); + } + + public Vector3 GetVelocityFromMovement(Vector3 movement, float deltaTime) + { + if (deltaTime <= 0f) + return Vector3.zero; + + return movement / deltaTime; + } + + /// + /// Trims a vector to make it restricted against a plane + /// + private void RestrictVectorToPlane(ref Vector3 vector, Vector3 toPlane) + { + if (vector.x > 0 != toPlane.x > 0) + { + vector.x = 0; + } + if (vector.y > 0 != toPlane.y > 0) + { + vector.y = 0; + } + if (vector.z > 0 != toPlane.z > 0) + { + vector.z = 0; + } + } + + /// + /// Detect if the character capsule is overlapping with anything collidable + /// + /// Returns number of overlaps + public int CharacterCollisionsOverlap(Vector3 position, Quaternion rotation, Collider[] overlappedColliders, float inflate = 0f, bool acceptOnlyStableGroundLayer = false) + { + int queryLayers = CollidableLayers; + if (acceptOnlyStableGroundLayer) + { + queryLayers = CollidableLayers & StableGroundLayers; + } + + Vector3 bottom = position + (rotation * _characterTransformToCapsuleBottomHemi); + Vector3 top = position + (rotation * _characterTransformToCapsuleTopHemi); + if (inflate != 0f) + { + bottom += (rotation * Vector3.down * inflate); + top += (rotation * Vector3.up * inflate); + } + + int nbHits = 0; + int nbUnfilteredHits = Physics.OverlapCapsuleNonAlloc( + bottom, + top, + Capsule.radius + inflate, + overlappedColliders, + queryLayers, + QueryTriggerInteraction.Ignore); + + // Filter out invalid colliders + nbHits = nbUnfilteredHits; + for (int i = nbUnfilteredHits - 1; i >= 0; i--) + { + if (!CheckIfColliderValidForCollisions(overlappedColliders[i])) + { + nbHits--; + if (i < nbHits) + { + overlappedColliders[i] = overlappedColliders[nbHits]; + } + } + } + + return nbHits; + } + + /// + /// Detect if the character capsule is overlapping with anything + /// + /// Returns number of overlaps + public int CharacterOverlap(Vector3 position, Quaternion rotation, Collider[] overlappedColliders, LayerMask layers, QueryTriggerInteraction triggerInteraction, float inflate = 0f) + { + Vector3 bottom = position + (rotation * _characterTransformToCapsuleBottomHemi); + Vector3 top = position + (rotation * _characterTransformToCapsuleTopHemi); + if (inflate != 0f) + { + bottom += (rotation * Vector3.down * inflate); + top += (rotation * Vector3.up * inflate); + } + + int nbHits = 0; + int nbUnfilteredHits = Physics.OverlapCapsuleNonAlloc( + bottom, + top, + Capsule.radius + inflate, + overlappedColliders, + layers, + triggerInteraction); + + // Filter out the character capsule itself + nbHits = nbUnfilteredHits; + for (int i = nbUnfilteredHits - 1; i >= 0; i--) + { + if (overlappedColliders[i] == Capsule) + { + nbHits--; + if (i < nbHits) + { + overlappedColliders[i] = overlappedColliders[nbHits]; + } + } + } + + return nbHits; + } + + /// + /// Sweeps the capsule's volume to detect collision hits + /// + /// Returns the number of hits + public int CharacterCollisionsSweep(Vector3 position, Quaternion rotation, Vector3 direction, float distance, out RaycastHit closestHit, RaycastHit[] hits, float inflate = 0f, bool acceptOnlyStableGroundLayer = false) + { + int queryLayers = CollidableLayers; + if (acceptOnlyStableGroundLayer) + { + queryLayers = CollidableLayers & StableGroundLayers; + } + + Vector3 bottom = position + (rotation * _characterTransformToCapsuleBottomHemi) - (direction * SweepProbingBackstepDistance); + Vector3 top = position + (rotation * _characterTransformToCapsuleTopHemi) - (direction * SweepProbingBackstepDistance); + if (inflate != 0f) + { + bottom += (rotation * Vector3.down * inflate); + top += (rotation * Vector3.up * inflate); + } + + // Capsule cast + int nbHits = 0; + int nbUnfilteredHits = Physics.CapsuleCastNonAlloc( + bottom, + top, + Capsule.radius + inflate, + direction, + hits, + distance + SweepProbingBackstepDistance, + queryLayers, + QueryTriggerInteraction.Ignore); + + // Hits filter + closestHit = new RaycastHit(); + float closestDistance = Mathf.Infinity; + nbHits = nbUnfilteredHits; + for (int i = nbUnfilteredHits - 1; i >= 0; i--) + { + hits[i].distance -= SweepProbingBackstepDistance; + + RaycastHit hit = hits[i]; + float hitDistance = hit.distance; + + // Filter out the invalid hits + if (hitDistance <= 0f || !CheckIfColliderValidForCollisions(hit.collider)) + { + nbHits--; + if (i < nbHits) + { + hits[i] = hits[nbHits]; + } + } + else + { + // Remember closest valid hit + if (hitDistance < closestDistance) + { + closestHit = hit; + closestDistance = hitDistance; + } + } + } + + return nbHits; + } + + /// + /// Sweeps the capsule's volume to detect hits + /// + /// Returns the number of hits + public int CharacterSweep(Vector3 position, Quaternion rotation, Vector3 direction, float distance, out RaycastHit closestHit, RaycastHit[] hits, LayerMask layers, QueryTriggerInteraction triggerInteraction, float inflate = 0f) + { + closestHit = new RaycastHit(); + + Vector3 bottom = position + (rotation * _characterTransformToCapsuleBottomHemi); + Vector3 top = position + (rotation * _characterTransformToCapsuleTopHemi); + if (inflate != 0f) + { + bottom += (rotation * Vector3.down * inflate); + top += (rotation * Vector3.up * inflate); + } + + // Capsule cast + int nbHits = 0; + int nbUnfilteredHits = Physics.CapsuleCastNonAlloc( + bottom, + top, + Capsule.radius + inflate, + direction, + hits, + distance, + layers, + triggerInteraction); + + // Hits filter + float closestDistance = Mathf.Infinity; + nbHits = nbUnfilteredHits; + for (int i = nbUnfilteredHits - 1; i >= 0; i--) + { + RaycastHit hit = hits[i]; + + // Filter out the character capsule + if (hit.distance <= 0f || hit.collider == Capsule) + { + nbHits--; + if (i < nbHits) + { + hits[i] = hits[nbHits]; + } + } + else + { + // Remember closest valid hit + float hitDistance = hit.distance; + if (hitDistance < closestDistance) + { + closestHit = hit; + closestDistance = hitDistance; + } + } + } + + return nbHits; + } + + /// + /// Casts the character volume in the character's downward direction to detect ground + /// + /// Returns the number of hits + private bool CharacterGroundSweep(Vector3 position, Quaternion rotation, Vector3 direction, float distance, out RaycastHit closestHit) + { + closestHit = new RaycastHit(); + + // Capsule cast + int nbUnfilteredHits = Physics.CapsuleCastNonAlloc( + position + (rotation * _characterTransformToCapsuleBottomHemi) - (direction * GroundProbingBackstepDistance), + position + (rotation * _characterTransformToCapsuleTopHemi) - (direction * GroundProbingBackstepDistance), + Capsule.radius, + direction, + _internalCharacterHits, + distance + GroundProbingBackstepDistance, + CollidableLayers & StableGroundLayers, + QueryTriggerInteraction.Ignore); + + // Hits filter + bool foundValidHit = false; + float closestDistance = Mathf.Infinity; + for (int i = 0; i < nbUnfilteredHits; i++) + { + RaycastHit hit = _internalCharacterHits[i]; + float hitDistance = hit.distance; + + // Find the closest valid hit + if (hitDistance > 0f && CheckIfColliderValidForCollisions(hit.collider)) + { + if (hitDistance < closestDistance) + { + closestHit = hit; + closestHit.distance -= GroundProbingBackstepDistance; + closestDistance = hitDistance; + + foundValidHit = true; + } + } + } + + return foundValidHit; + } + + /// + /// Raycasts to detect collision hits + /// + /// Returns the number of hits + public int CharacterCollisionsRaycast(Vector3 position, Vector3 direction, float distance, out RaycastHit closestHit, RaycastHit[] hits, bool acceptOnlyStableGroundLayer = false) + { + int queryLayers = CollidableLayers; + if (acceptOnlyStableGroundLayer) + { + queryLayers = CollidableLayers & StableGroundLayers; + } + + // Raycast + int nbHits = 0; + int nbUnfilteredHits = Physics.RaycastNonAlloc( + position, + direction, + hits, + distance, + queryLayers, + QueryTriggerInteraction.Ignore); + + // Hits filter + closestHit = new RaycastHit(); + float closestDistance = Mathf.Infinity; + nbHits = nbUnfilteredHits; + for (int i = nbUnfilteredHits - 1; i >= 0; i--) + { + RaycastHit hit = hits[i]; + float hitDistance = hit.distance; + + // Filter out the invalid hits + if (hitDistance <= 0f || + !CheckIfColliderValidForCollisions(hit.collider)) + { + nbHits--; + if (i < nbHits) + { + hits[i] = hits[nbHits]; + } + } + else + { + // Remember closest valid hit + if (hitDistance < closestDistance) + { + closestHit = hit; + closestDistance = hitDistance; + } + } + } + + return nbHits; + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterMotor.cs.meta b/Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterMotor.cs.meta new file mode 100644 index 0000000..fb3d392 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterMotor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4d1bc5515e3ab954e80599c538834774 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterSystem.cs b/Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterSystem.cs new file mode 100644 index 0000000..b838d14 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterSystem.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace KinematicCharacterController +{ + /// + /// The system that manages the simulation of KinematicCharacterMotor and PhysicsMover + /// + [DefaultExecutionOrder(-100)] + public class KinematicCharacterSystem : MonoBehaviour + { + private static KinematicCharacterSystem _instance; + + public static List CharacterMotors = new List(); + public static List PhysicsMovers = new List(); + + private static float _lastCustomInterpolationStartTime = -1f; + private static float _lastCustomInterpolationDeltaTime = -1f; + + public static KCCSettings Settings; + + /// + /// Creates a KinematicCharacterSystem instance if there isn't already one + /// + public static void EnsureCreation() + { + if (_instance == null) + { + GameObject systemGameObject = new GameObject("KinematicCharacterSystem"); + _instance = systemGameObject.AddComponent(); + + systemGameObject.hideFlags = HideFlags.NotEditable; + _instance.hideFlags = HideFlags.NotEditable; + + Settings = ScriptableObject.CreateInstance(); + + GameObject.DontDestroyOnLoad(systemGameObject); + } + } + + /// + /// Gets the KinematicCharacterSystem instance if any + /// + /// + public static KinematicCharacterSystem GetInstance() + { + return _instance; + } + + /// + /// Sets the maximum capacity of the character motors list, to prevent allocations when adding characters + /// + /// + public static void SetCharacterMotorsCapacity(int capacity) + { + if (capacity < CharacterMotors.Count) + { + capacity = CharacterMotors.Count; + } + CharacterMotors.Capacity = capacity; + } + + /// + /// Registers a KinematicCharacterMotor into the system + /// + public static void RegisterCharacterMotor(KinematicCharacterMotor motor) + { + CharacterMotors.Add(motor); + } + + /// + /// Unregisters a KinematicCharacterMotor from the system + /// + public static void UnregisterCharacterMotor(KinematicCharacterMotor motor) + { + CharacterMotors.Remove(motor); + } + + /// + /// Sets the maximum capacity of the physics movers list, to prevent allocations when adding movers + /// + /// + public static void SetPhysicsMoversCapacity(int capacity) + { + if (capacity < PhysicsMovers.Count) + { + capacity = PhysicsMovers.Count; + } + PhysicsMovers.Capacity = capacity; + } + + /// + /// Registers a PhysicsMover into the system + /// + public static void RegisterPhysicsMover(PhysicsMover mover) + { + PhysicsMovers.Add(mover); + + mover.Rigidbody.interpolation = RigidbodyInterpolation.None; + } + + /// + /// Unregisters a PhysicsMover from the system + /// + public static void UnregisterPhysicsMover(PhysicsMover mover) + { + PhysicsMovers.Remove(mover); + } + + // This is to prevent duplicating the singleton gameobject on script recompiles + private void OnDisable() + { + Destroy(this.gameObject); + } + + private void Awake() + { + _instance = this; + } + + private void FixedUpdate() + { + if (Settings.AutoSimulation) + { + float deltaTime = Time.deltaTime; + + if (Settings.Interpolate) + { + PreSimulationInterpolationUpdate(deltaTime); + } + + Simulate(deltaTime, CharacterMotors, PhysicsMovers); + + if (Settings.Interpolate) + { + PostSimulationInterpolationUpdate(deltaTime); + } + } + } + + private void LateUpdate() + { + if (Settings.Interpolate) + { + CustomInterpolationUpdate(); + } + } + + /// + /// Remembers the point to interpolate from for KinematicCharacterMotors and PhysicsMovers + /// + public static void PreSimulationInterpolationUpdate(float deltaTime) + { + // Save pre-simulation poses and place transform at transient pose + for (int i = 0; i < CharacterMotors.Count; i++) + { + KinematicCharacterMotor motor = CharacterMotors[i]; + + motor.InitialTickPosition = motor.TransientPosition; + motor.InitialTickRotation = motor.TransientRotation; + + motor.Transform.SetPositionAndRotation(motor.TransientPosition, motor.TransientRotation); + } + + for (int i = 0; i < PhysicsMovers.Count; i++) + { + PhysicsMover mover = PhysicsMovers[i]; + + mover.InitialTickPosition = mover.TransientPosition; + mover.InitialTickRotation = mover.TransientRotation; + + mover.Transform.SetPositionAndRotation(mover.TransientPosition, mover.TransientRotation); + mover.Rigidbody.position = mover.TransientPosition; + mover.Rigidbody.rotation = mover.TransientRotation; + } + } + + /// + /// Ticks characters and/or movers + /// + public static void Simulate(float deltaTime, List motors, List movers) + { + int characterMotorsCount = motors.Count; + int physicsMoversCount = movers.Count; + +#pragma warning disable 0162 + // Update PhysicsMover velocities + for (int i = 0; i < physicsMoversCount; i++) + { + movers[i].VelocityUpdate(deltaTime); + } + + // Character controller update phase 1 + for (int i = 0; i < characterMotorsCount; i++) + { + motors[i].UpdatePhase1(deltaTime); + } + + // Simulate PhysicsMover displacement + for (int i = 0; i < physicsMoversCount; i++) + { + PhysicsMover mover = movers[i]; + + mover.Transform.SetPositionAndRotation(mover.TransientPosition, mover.TransientRotation); + mover.Rigidbody.position = mover.TransientPosition; + mover.Rigidbody.rotation = mover.TransientRotation; + } + + // Character controller update phase 2 and move + for (int i = 0; i < characterMotorsCount; i++) + { + KinematicCharacterMotor motor = motors[i]; + + motor.UpdatePhase2(deltaTime); + + motor.Transform.SetPositionAndRotation(motor.TransientPosition, motor.TransientRotation); + } +#pragma warning restore 0162 + } + + /// + /// Initiates the interpolation for KinematicCharacterMotors and PhysicsMovers + /// + public static void PostSimulationInterpolationUpdate(float deltaTime) + { + _lastCustomInterpolationStartTime = Time.time; + _lastCustomInterpolationDeltaTime = deltaTime; + + // Return interpolated roots to their initial poses + for (int i = 0; i < CharacterMotors.Count; i++) + { + KinematicCharacterMotor motor = CharacterMotors[i]; + + motor.Transform.SetPositionAndRotation(motor.InitialTickPosition, motor.InitialTickRotation); + } + + for (int i = 0; i < PhysicsMovers.Count; i++) + { + PhysicsMover mover = PhysicsMovers[i]; + + if (mover.MoveWithPhysics) + { + mover.Rigidbody.position = mover.InitialTickPosition; + mover.Rigidbody.rotation = mover.InitialTickRotation; + + mover.Rigidbody.MovePosition(mover.TransientPosition); + mover.Rigidbody.MoveRotation(mover.TransientRotation); + } + else + { + mover.Rigidbody.position = (mover.TransientPosition); + mover.Rigidbody.rotation = (mover.TransientRotation); + } + } + } + + /// + /// Handles per-frame interpolation + /// + private static void CustomInterpolationUpdate() + { + float interpolationFactor = Mathf.Clamp01((Time.time - _lastCustomInterpolationStartTime) / _lastCustomInterpolationDeltaTime); + + // Handle characters interpolation + for (int i = 0; i < CharacterMotors.Count; i++) + { + KinematicCharacterMotor motor = CharacterMotors[i]; + + motor.Transform.SetPositionAndRotation( + Vector3.Lerp(motor.InitialTickPosition, motor.TransientPosition, interpolationFactor), + Quaternion.Slerp(motor.InitialTickRotation, motor.TransientRotation, interpolationFactor)); + } + + // Handle PhysicsMovers interpolation + for (int i = 0; i < PhysicsMovers.Count; i++) + { + PhysicsMover mover = PhysicsMovers[i]; + + mover.Transform.SetPositionAndRotation( + Vector3.Lerp(mover.InitialTickPosition, mover.TransientPosition, interpolationFactor), + Quaternion.Slerp(mover.InitialTickRotation, mover.TransientRotation, interpolationFactor)); + + Vector3 newPos = mover.Transform.position; + Quaternion newRot = mover.Transform.rotation; + mover.PositionDeltaFromInterpolation = newPos - mover.LatestInterpolationPosition; + mover.RotationDeltaFromInterpolation = Quaternion.Inverse(mover.LatestInterpolationRotation) * newRot; + mover.LatestInterpolationPosition = newPos; + mover.LatestInterpolationRotation = newRot; + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterSystem.cs.meta b/Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterSystem.cs.meta new file mode 100644 index 0000000..2d1bcb3 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/KinematicCharacterSystem.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: dbd80c9a62d41084aad50ae44255beb9 +timeCreated: 1500241576 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core/PhysicsMover.cs b/Assets/Plugins/KinematicCharacterController/Core/PhysicsMover.cs new file mode 100644 index 0000000..2af310c --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/PhysicsMover.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace KinematicCharacterController +{ + /// + /// Represents the entire state of a PhysicsMover that is pertinent for simulation. + /// Use this to save state or revert to past state + /// + [System.Serializable] + public struct PhysicsMoverState + { + public Vector3 Position; + public Quaternion Rotation; + public Vector3 Velocity; + public Vector3 AngularVelocity; + } + + /// + /// Component that manages the movement of moving kinematic rigidbodies for + /// proper interaction with characters + /// + [RequireComponent(typeof(Rigidbody))] + public class PhysicsMover : MonoBehaviour + { + /// + /// The mover's Rigidbody + /// + [ReadOnly] + public Rigidbody Rigidbody; + + /// + /// Determines if the platform moves with rigidbody.MovePosition (when true), or with rigidbody.position (when false) + /// + public bool MoveWithPhysics = true; + + /// + /// Index of this motor in KinematicCharacterSystem arrays + /// + [NonSerialized] + public IMoverController MoverController; + /// + /// Remembers latest position in interpolation + /// + [NonSerialized] + public Vector3 LatestInterpolationPosition; + /// + /// Remembers latest rotation in interpolation + /// + [NonSerialized] + public Quaternion LatestInterpolationRotation; + /// + /// The latest movement made by interpolation + /// + [NonSerialized] + public Vector3 PositionDeltaFromInterpolation; + /// + /// The latest rotation made by interpolation + /// + [NonSerialized] + public Quaternion RotationDeltaFromInterpolation; + + /// + /// Index of this motor in KinematicCharacterSystem arrays + /// + public int IndexInCharacterSystem { get; set; } + /// + /// Remembers initial position before all simulation are done + /// + public Vector3 Velocity { get; protected set; } + /// + /// Remembers initial position before all simulation are done + /// + public Vector3 AngularVelocity { get; protected set; } + /// + /// Remembers initial position before all simulation are done + /// + public Vector3 InitialTickPosition { get; set; } + /// + /// Remembers initial rotation before all simulation are done + /// + public Quaternion InitialTickRotation { get; set; } + + /// + /// The mover's Transform + /// + public Transform Transform { get; private set; } + /// + /// The character's position before the movement calculations began + /// + public Vector3 InitialSimulationPosition { get; private set; } + /// + /// The character's rotation before the movement calculations began + /// + public Quaternion InitialSimulationRotation { get; private set; } + + private Vector3 _internalTransientPosition; + + /// + /// The mover's rotation (always up-to-date during the character update phase) + /// + public Vector3 TransientPosition + { + get + { + return _internalTransientPosition; + } + private set + { + _internalTransientPosition = value; + } + } + + private Quaternion _internalTransientRotation; + /// + /// The mover's rotation (always up-to-date during the character update phase) + /// + public Quaternion TransientRotation + { + get + { + return _internalTransientRotation; + } + private set + { + _internalTransientRotation = value; + } + } + + + private void Reset() + { + ValidateData(); + } + + private void OnValidate() + { + ValidateData(); + } + + /// + /// Handle validating all required values + /// + public void ValidateData() + { + Rigidbody = gameObject.GetComponent(); + + Rigidbody.centerOfMass = Vector3.zero; + Rigidbody.maxAngularVelocity = Mathf.Infinity; + Rigidbody.maxDepenetrationVelocity = Mathf.Infinity; + Rigidbody.isKinematic = true; + Rigidbody.interpolation = RigidbodyInterpolation.None; + } + + private void OnEnable() + { + KinematicCharacterSystem.EnsureCreation(); + KinematicCharacterSystem.RegisterPhysicsMover(this); + } + + private void OnDisable() + { + KinematicCharacterSystem.UnregisterPhysicsMover(this); + } + + private void Awake() + { + Transform = this.transform; + ValidateData(); + + TransientPosition = Rigidbody.position; + TransientRotation = Rigidbody.rotation; + InitialSimulationPosition = Rigidbody.position; + InitialSimulationRotation = Rigidbody.rotation; + LatestInterpolationPosition = Transform.position; + LatestInterpolationRotation = Transform.rotation; + } + + /// + /// Sets the mover's position directly + /// + public void SetPosition(Vector3 position) + { + Transform.position = position; + Rigidbody.position = position; + InitialSimulationPosition = position; + TransientPosition = position; + } + + /// + /// Sets the mover's rotation directly + /// + public void SetRotation(Quaternion rotation) + { + Transform.rotation = rotation; + Rigidbody.rotation = rotation; + InitialSimulationRotation = rotation; + TransientRotation = rotation; + } + + /// + /// Sets the mover's position and rotation directly + /// + public void SetPositionAndRotation(Vector3 position, Quaternion rotation) + { + Transform.SetPositionAndRotation(position, rotation); + Rigidbody.position = position; + Rigidbody.rotation = rotation; + InitialSimulationPosition = position; + InitialSimulationRotation = rotation; + TransientPosition = position; + TransientRotation = rotation; + } + + /// + /// Returns all the state information of the mover that is pertinent for simulation + /// + public PhysicsMoverState GetState() + { + PhysicsMoverState state = new PhysicsMoverState(); + + state.Position = TransientPosition; + state.Rotation = TransientRotation; + state.Velocity = Velocity; + state.AngularVelocity = AngularVelocity; + + return state; + } + + /// + /// Applies a mover state instantly + /// + public void ApplyState(PhysicsMoverState state) + { + SetPositionAndRotation(state.Position, state.Rotation); + Velocity = state.Velocity; + AngularVelocity = state.AngularVelocity; + } + + /// + /// Caches velocity values based on deltatime and target position/rotations + /// + public void VelocityUpdate(float deltaTime) + { + InitialSimulationPosition = TransientPosition; + InitialSimulationRotation = TransientRotation; + + MoverController.UpdateMovement(out _internalTransientPosition, out _internalTransientRotation, deltaTime); + + if (deltaTime > 0f) + { + Velocity = (TransientPosition - InitialSimulationPosition) / deltaTime; + + Quaternion rotationFromCurrentToGoal = TransientRotation * (Quaternion.Inverse(InitialSimulationRotation)); + AngularVelocity = (Mathf.Deg2Rad * rotationFromCurrentToGoal.eulerAngles) / deltaTime; + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/Core/PhysicsMover.cs.meta b/Assets/Plugins/KinematicCharacterController/Core/PhysicsMover.cs.meta new file mode 100644 index 0000000..22c397e --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/PhysicsMover.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 075ccbe9837b0744985e09ba3f015b9b +timeCreated: 1488089271 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/Core/ReadOnlyAttribute.cs b/Assets/Plugins/KinematicCharacterController/Core/ReadOnlyAttribute.cs new file mode 100644 index 0000000..918d460 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/ReadOnlyAttribute.cs @@ -0,0 +1,8 @@ +using UnityEngine; + +namespace KinematicCharacterController +{ + public class ReadOnlyAttribute : PropertyAttribute + { + } +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/Core/ReadOnlyAttribute.cs.meta b/Assets/Plugins/KinematicCharacterController/Core/ReadOnlyAttribute.cs.meta new file mode 100644 index 0000000..f75dd61 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/Core/ReadOnlyAttribute.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 63e8828991cf67f42bfeb498686aad0b +timeCreated: 1504493014 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter.meta b/Assets/Plugins/KinematicCharacterController/ExampleCharacter.meta new file mode 100644 index 0000000..0736ab8 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b8da84cc169c79b4894eefab60565279 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Materials.meta b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Materials.meta new file mode 100644 index 0000000..6e28967 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Materials.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f1cc217451b989d4db08290832cb9dc0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Materials/White.mat b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Materials/White.mat new file mode 100644 index 0000000..6805a97 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Materials/White.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: White + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 4, y: 4} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 4, y: 4} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 0.5 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.661 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Materials/White.mat.meta b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Materials/White.mat.meta new file mode 100644 index 0000000..deb2159 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Materials/White.mat.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: cf774b4c734b1d04e9fbf6d71e964591 +timeCreated: 1493180259 +licenseType: Store +NativeFormatImporter: + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs.meta b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs.meta new file mode 100644 index 0000000..7b5f9ad --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 974715e4a94547d4189e0801712b2685 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCamera.prefab b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCamera.prefab new file mode 100644 index 0000000..6683575 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCamera.prefab @@ -0,0 +1,127 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1937979026721672 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4353896453404184} + - component: {fileID: 20127477809793788} + - component: {fileID: 124551908420297706} + - component: {fileID: 81410469953920030} + - component: {fileID: 114014416615829144} + m_Layer: 0 + m_Name: ExampleCamera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4353896453404184 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1937979026721672} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -5} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!20 &20127477809793788 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1937979026721672} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_GateFitMode: 2 + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 75 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!124 &124551908420297706 +Behaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1937979026721672} + m_Enabled: 1 +--- !u!81 &81410469953920030 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1937979026721672} + m_Enabled: 1 +--- !u!114 &114014416615829144 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1937979026721672} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 24092a40b02616e479baeb940325e339, type: 3} + m_Name: + m_EditorClassIdentifier: + Camera: {fileID: 20127477809793788} + FollowPointFraming: {x: 0, y: 0} + FollowingSharpness: 10000 + DefaultDistance: 6 + MinDistance: 0 + MaxDistance: 10 + DistanceMovementSpeed: 5 + DistanceMovementSharpness: 10 + InvertX: 0 + InvertY: 0 + DefaultVerticalAngle: 20 + MinVerticalAngle: -90 + MaxVerticalAngle: 90 + RotationSpeed: 1.3 + RotationSharpness: 10000 + ObstructionCheckRadius: 0.2 + ObstructionLayers: + serializedVersion: 2 + m_Bits: 4294965759 + ObstructionSharpness: 10000 + IgnoredColliders: [] diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCamera.prefab.meta b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCamera.prefab.meta new file mode 100644 index 0000000..bee0535 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCamera.prefab.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: c75e91cf060bbf04e89204fa400606df +timeCreated: 1499181144 +licenseType: Store +NativeFormatImporter: + mainObjectFileID: 100100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCharacter.prefab b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCharacter.prefab new file mode 100644 index 0000000..8de8f9c --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCharacter.prefab @@ -0,0 +1,394 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &43330285 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 43330286} + m_Layer: 0 + m_Name: Root + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &43330286 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 43330285} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 4036232530805280} + - {fileID: 4718917724592500} + m_Father: {fileID: 4898694727102280} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1072008595159340 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4036232530805280} + m_Layer: 0 + m_Name: Meshes + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4036232530805280 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1072008595159340} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 4903610365175460} + m_Father: {fileID: 43330286} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1339159333115884 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4903610365175460} + - component: {fileID: 33711778101729860} + - component: {fileID: 23310449951372792} + m_Layer: 0 + m_Name: Capsule + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4903610365175460 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1339159333115884} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 4485742127011420} + m_Father: {fileID: 4036232530805280} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!33 &33711778101729860 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1339159333115884} + m_Mesh: {fileID: 10208, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &23310449951372792 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1339159333115884} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RenderingLayerMask: 4294967295 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: cf774b4c734b1d04e9fbf6d71e964591, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 1 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!1 &1398904762131290 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4898694727102280} + - component: {fileID: 136457490459165436} + - component: {fileID: 114565867274256808} + - component: {fileID: 114967586934651944} + - component: {fileID: 6177337246575538918} + m_Layer: 0 + m_Name: ExampleCharacter + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4898694727102280 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1398904762131290} + m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.70710677} + m_LocalPosition: {x: -40.14, y: 0, z: -29.24} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 43330286} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 90.00001, z: 0} +--- !u!136 &136457490459165436 +CapsuleCollider: + m_ObjectHideFlags: 8 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1398904762131290} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + m_Radius: 0.5 + m_Height: 2 + m_Direction: 1 + m_Center: {x: 0, y: 1, z: 0} +--- !u!114 &114565867274256808 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1398904762131290} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76020eee813ed7844bcea94c5d5ce76a, type: 3} + m_Name: + m_EditorClassIdentifier: + Motor: {fileID: 114967586934651944} + MaxStableMoveSpeed: 10 + StableMovementSharpness: 15 + OrientationSharpness: 10 + OrientationMethod: 1 + MaxAirMoveSpeed: 15 + AirAccelerationSpeed: 18 + Drag: 0.1 + AllowJumpingWhenSliding: 0 + JumpUpSpeed: 10 + JumpScalableForwardSpeed: 10 + JumpPreGroundingGraceTime: 0 + JumpPostGroundingGraceTime: 0 + IgnoredColliders: + - {fileID: 0} + - {fileID: 0} + BonusOrientationMethod: 0 + BonusOrientationSharpness: 10 + Gravity: {x: 0, y: -30, z: 0} + MeshRoot: {fileID: 4036232530805280} + CameraFollowPoint: {fileID: 4718917724592500} +--- !u!114 &114967586934651944 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1398904762131290} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4d1bc5515e3ab954e80599c538834774, type: 3} + m_Name: + m_EditorClassIdentifier: + Capsule: {fileID: 136457490459165436} + CapsuleRadius: 0.5 + CapsuleHeight: 2 + CapsuleYOffset: 1 + CapsulePhysicsMaterial: {fileID: 0} + GroundDetectionExtraDistance: 0 + MaxStableSlopeAngle: 60 + StableGroundLayers: + serializedVersion: 2 + m_Bits: 4294967295 + DiscreteCollisionEvents: 0 + StepHandling: 1 + MaxStepHeight: 0.5 + AllowSteppingWithoutStableGrounding: 0 + MinRequiredStepDepth: 0.1 + LedgeAndDenivelationHandling: 1 + MaxStableDistanceFromLedge: 0.5 + MaxVelocityForLedgeSnap: 0 + MaxStableDenivelationAngle: 180 + InteractiveRigidbodyHandling: 1 + RigidbodyInteractionType: 0 + SimulatedCharacterMass: 1 + PreserveAttachedRigidbodyMomentum: 1 + HasPlanarConstraint: 0 + PlanarConstraintAxis: {x: 0, y: 0, z: 1} + MaxMovementIterations: 5 + MaxDecollisionIterations: 1 + CheckMovementInitialOverlaps: 1 + KillVelocityWhenExceedMaxMovementIterations: 1 + KillRemainingMovementWhenExceedMaxMovementIterations: 1 +--- !u!54 &6177337246575538918 +Rigidbody: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1398904762131290} + serializedVersion: 2 + m_Mass: 1 + m_Drag: 0 + m_AngularDrag: 0.05 + m_UseGravity: 0 + m_IsKinematic: 1 + m_Interpolate: 0 + m_Constraints: 0 + m_CollisionDetection: 0 +--- !u!1 &1758411822872852 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4485742127011420} + - component: {fileID: 33111717300596878} + - component: {fileID: 23624829931232870} + m_Layer: 0 + m_Name: Cylinder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4485742127011420 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1758411822872852} + m_LocalRotation: {x: 0.70710677, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0.44099998, z: 0.18199922} + m_LocalScale: {x: 0.88040376, y: 0.34093377, z: 0.51656276} + m_Children: [] + m_Father: {fileID: 4903610365175460} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 90.00001, y: 0, z: 0} +--- !u!33 &33111717300596878 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1758411822872852} + m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &23624829931232870 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1758411822872852} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RenderingLayerMask: 4294967295 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: cf774b4c734b1d04e9fbf6d71e964591, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 1 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!1 &1854746833484672 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4718917724592500} + m_Layer: 0 + m_Name: CameraTarget + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4718917724592500 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1854746833484672} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 1.43, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 43330286} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCharacter.prefab.meta b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCharacter.prefab.meta new file mode 100644 index 0000000..ba4ee11 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Prefabs/ExampleCharacter.prefab.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: d295472d4ce25404e9aed2627c34801f +timeCreated: 1499181148 +licenseType: Store +NativeFormatImporter: + mainObjectFileID: 100100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts.meta b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts.meta new file mode 100644 index 0000000..7fb4034 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9751300dc66704b4f87d358c2a60f913 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterCamera.cs b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterCamera.cs new file mode 100644 index 0000000..44ded71 --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterCamera.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace KinematicCharacterController.Examples +{ + public class ExampleCharacterCamera : MonoBehaviour + { + [Header("Framing")] + public Camera Camera; + public Vector2 FollowPointFraming = new Vector2(0f, 0f); + public float FollowingSharpness = 10000f; + + [Header("Distance")] + public float DefaultDistance = 6f; + public float MinDistance = 0f; + public float MaxDistance = 10f; + public float DistanceMovementSpeed = 5f; + public float DistanceMovementSharpness = 10f; + + [Header("Rotation")] + public bool InvertX = false; + public bool InvertY = false; + [Range(-90f, 90f)] + public float DefaultVerticalAngle = 20f; + [Range(-90f, 90f)] + public float MinVerticalAngle = -90f; + [Range(-90f, 90f)] + public float MaxVerticalAngle = 90f; + public float RotationSpeed = 1f; + public float RotationSharpness = 10000f; + public bool RotateWithPhysicsMover = false; + + [Header("Obstruction")] + public float ObstructionCheckRadius = 0.2f; + public LayerMask ObstructionLayers = -1; + public float ObstructionSharpness = 10000f; + public List IgnoredColliders = new List(); + + public Transform Transform { get; private set; } + public Transform FollowTransform { get; private set; } + + public Vector3 PlanarDirection { get; set; } + public float TargetDistance { get; set; } + + private bool _distanceIsObstructed; + private float _currentDistance; + private float _targetVerticalAngle; + private RaycastHit _obstructionHit; + private int _obstructionCount; + private RaycastHit[] _obstructions = new RaycastHit[MaxObstructions]; + private float _obstructionTime; + private Vector3 _currentFollowPosition; + + private const int MaxObstructions = 32; + + void OnValidate() + { + DefaultDistance = Mathf.Clamp(DefaultDistance, MinDistance, MaxDistance); + DefaultVerticalAngle = Mathf.Clamp(DefaultVerticalAngle, MinVerticalAngle, MaxVerticalAngle); + } + + void Awake() + { + Transform = this.transform; + + _currentDistance = DefaultDistance; + TargetDistance = _currentDistance; + + _targetVerticalAngle = 0f; + + PlanarDirection = Vector3.forward; + } + + // Set the transform that the camera will orbit around + public void SetFollowTransform(Transform t) + { + FollowTransform = t; + PlanarDirection = FollowTransform.forward; + _currentFollowPosition = FollowTransform.position; + } + + public void UpdateWithInput(float deltaTime, float zoomInput, Vector3 rotationInput) + { + if (FollowTransform) + { + if (InvertX) + { + rotationInput.x *= -1f; + } + if (InvertY) + { + rotationInput.y *= -1f; + } + + // Process rotation input + Quaternion rotationFromInput = Quaternion.Euler(FollowTransform.up * (rotationInput.x * RotationSpeed)); + PlanarDirection = rotationFromInput * PlanarDirection; + PlanarDirection = Vector3.Cross(FollowTransform.up, Vector3.Cross(PlanarDirection, FollowTransform.up)); + Quaternion planarRot = Quaternion.LookRotation(PlanarDirection, FollowTransform.up); + + _targetVerticalAngle -= (rotationInput.y * RotationSpeed); + _targetVerticalAngle = Mathf.Clamp(_targetVerticalAngle, MinVerticalAngle, MaxVerticalAngle); + Quaternion verticalRot = Quaternion.Euler(_targetVerticalAngle, 0, 0); + Quaternion targetRotation = Quaternion.Slerp(Transform.rotation, planarRot * verticalRot, 1f - Mathf.Exp(-RotationSharpness * deltaTime)); + + // Apply rotation + Transform.rotation = targetRotation; + + // Process distance input + if (_distanceIsObstructed && Mathf.Abs(zoomInput) > 0f) + { + TargetDistance = _currentDistance; + } + TargetDistance += zoomInput * DistanceMovementSpeed; + TargetDistance = Mathf.Clamp(TargetDistance, MinDistance, MaxDistance); + + // Find the smoothed follow position + _currentFollowPosition = Vector3.Lerp(_currentFollowPosition, FollowTransform.position, 1f - Mathf.Exp(-FollowingSharpness * deltaTime)); + + // Handle obstructions + { + RaycastHit closestHit = new RaycastHit(); + closestHit.distance = Mathf.Infinity; + _obstructionCount = Physics.SphereCastNonAlloc(_currentFollowPosition, ObstructionCheckRadius, -Transform.forward, _obstructions, TargetDistance, ObstructionLayers, QueryTriggerInteraction.Ignore); + for (int i = 0; i < _obstructionCount; i++) + { + bool isIgnored = false; + for (int j = 0; j < IgnoredColliders.Count; j++) + { + if (IgnoredColliders[j] == _obstructions[i].collider) + { + isIgnored = true; + break; + } + } + for (int j = 0; j < IgnoredColliders.Count; j++) + { + if (IgnoredColliders[j] == _obstructions[i].collider) + { + isIgnored = true; + break; + } + } + + if (!isIgnored && _obstructions[i].distance < closestHit.distance && _obstructions[i].distance > 0) + { + closestHit = _obstructions[i]; + } + } + + // If obstructions detecter + if (closestHit.distance < Mathf.Infinity) + { + _distanceIsObstructed = true; + _currentDistance = Mathf.Lerp(_currentDistance, closestHit.distance, 1 - Mathf.Exp(-ObstructionSharpness * deltaTime)); + } + // If no obstruction + else + { + _distanceIsObstructed = false; + _currentDistance = Mathf.Lerp(_currentDistance, TargetDistance, 1 - Mathf.Exp(-DistanceMovementSharpness * deltaTime)); + } + } + + // Find the smoothed camera orbit position + Vector3 targetPosition = _currentFollowPosition - ((targetRotation * Vector3.forward) * _currentDistance); + + // Handle framing + targetPosition += Transform.right * FollowPointFraming.x; + targetPosition += Transform.up * FollowPointFraming.y; + + // Apply position + Transform.position = targetPosition; + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterCamera.cs.meta b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterCamera.cs.meta new file mode 100644 index 0000000..64ae17e --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterCamera.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 24092a40b02616e479baeb940325e339 +timeCreated: 1485657083 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterController.cs b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterController.cs new file mode 100644 index 0000000..090780d --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterController.cs @@ -0,0 +1,516 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using KinematicCharacterController; +using System; + +namespace KinematicCharacterController.Examples +{ + public enum CharacterState + { + Default, + } + + public enum OrientationMethod + { + TowardsCamera, + TowardsMovement, + } + + public struct PlayerCharacterInputs + { + public float MoveAxisForward; + public float MoveAxisRight; + public Quaternion CameraRotation; + public bool JumpDown; + public bool CrouchDown; + public bool CrouchUp; + } + + public struct AICharacterInputs + { + public Vector3 MoveVector; + public Vector3 LookVector; + } + + public enum BonusOrientationMethod + { + None, + TowardsGravity, + TowardsGroundSlopeAndGravity, + } + + public class ExampleCharacterController : MonoBehaviour, ICharacterController + { + public KinematicCharacterMotor Motor; + + [Header("Stable Movement")] + public float MaxStableMoveSpeed = 10f; + public float StableMovementSharpness = 15f; + public float OrientationSharpness = 10f; + public OrientationMethod OrientationMethod = OrientationMethod.TowardsCamera; + + [Header("Air Movement")] + public float MaxAirMoveSpeed = 15f; + public float AirAccelerationSpeed = 15f; + public float Drag = 0.1f; + + [Header("Jumping")] + public bool AllowJumpingWhenSliding = false; + public float JumpUpSpeed = 10f; + public float JumpScalableForwardSpeed = 10f; + public float JumpPreGroundingGraceTime = 0f; + public float JumpPostGroundingGraceTime = 0f; + + [Header("Misc")] + public List IgnoredColliders = new List(); + public BonusOrientationMethod BonusOrientationMethod = BonusOrientationMethod.None; + public float BonusOrientationSharpness = 10f; + public Vector3 Gravity = new Vector3(0, -30f, 0); + public Transform MeshRoot; + public Transform CameraFollowPoint; + public float CrouchedCapsuleHeight = 1f; + + public CharacterState CurrentCharacterState { get; private set; } + + private Collider[] _probedColliders = new Collider[8]; + private RaycastHit[] _probedHits = new RaycastHit[8]; + private Vector3 _moveInputVector; + private Vector3 _lookInputVector; + private bool _jumpRequested = false; + private bool _jumpConsumed = false; + private bool _jumpedThisFrame = false; + private float _timeSinceJumpRequested = Mathf.Infinity; + private float _timeSinceLastAbleToJump = 0f; + private Vector3 _internalVelocityAdd = Vector3.zero; + private bool _shouldBeCrouching = false; + private bool _isCrouching = false; + + private Vector3 lastInnerNormal = Vector3.zero; + private Vector3 lastOuterNormal = Vector3.zero; + + private void Awake() + { + // Handle initial state + TransitionToState(CharacterState.Default); + + // Assign the characterController to the motor + Motor.CharacterController = this; + } + + /// + /// Handles movement state transitions and enter/exit callbacks + /// + public void TransitionToState(CharacterState newState) + { + CharacterState tmpInitialState = CurrentCharacterState; + OnStateExit(tmpInitialState, newState); + CurrentCharacterState = newState; + OnStateEnter(newState, tmpInitialState); + } + + /// + /// Event when entering a state + /// + public void OnStateEnter(CharacterState state, CharacterState fromState) + { + switch (state) + { + case CharacterState.Default: + { + break; + } + } + } + + /// + /// Event when exiting a state + /// + public void OnStateExit(CharacterState state, CharacterState toState) + { + switch (state) + { + case CharacterState.Default: + { + break; + } + } + } + + /// + /// This is called every frame by ExamplePlayer in order to tell the character what its inputs are + /// + public void SetInputs(ref PlayerCharacterInputs inputs) + { + // Clamp input + Vector3 moveInputVector = Vector3.ClampMagnitude(new Vector3(inputs.MoveAxisRight, 0f, inputs.MoveAxisForward), 1f); + + // Calculate camera direction and rotation on the character plane + Vector3 cameraPlanarDirection = Vector3.ProjectOnPlane(inputs.CameraRotation * Vector3.forward, Motor.CharacterUp).normalized; + if (cameraPlanarDirection.sqrMagnitude == 0f) + { + cameraPlanarDirection = Vector3.ProjectOnPlane(inputs.CameraRotation * Vector3.up, Motor.CharacterUp).normalized; + } + Quaternion cameraPlanarRotation = Quaternion.LookRotation(cameraPlanarDirection, Motor.CharacterUp); + + switch (CurrentCharacterState) + { + case CharacterState.Default: + { + // Move and look inputs + _moveInputVector = cameraPlanarRotation * moveInputVector; + + switch (OrientationMethod) + { + case OrientationMethod.TowardsCamera: + _lookInputVector = cameraPlanarDirection; + break; + case OrientationMethod.TowardsMovement: + _lookInputVector = _moveInputVector.normalized; + break; + } + + // Jumping input + if (inputs.JumpDown) + { + _timeSinceJumpRequested = 0f; + _jumpRequested = true; + } + + // Crouching input + if (inputs.CrouchDown) + { + _shouldBeCrouching = true; + + if (!_isCrouching) + { + _isCrouching = true; + Motor.SetCapsuleDimensions(0.5f, CrouchedCapsuleHeight, CrouchedCapsuleHeight * 0.5f); + MeshRoot.localScale = new Vector3(1f, 0.5f, 1f); + } + } + else if (inputs.CrouchUp) + { + _shouldBeCrouching = false; + } + + break; + } + } + } + + /// + /// This is called every frame by the AI script in order to tell the character what its inputs are + /// + public void SetInputs(ref AICharacterInputs inputs) + { + _moveInputVector = inputs.MoveVector; + _lookInputVector = inputs.LookVector; + } + + private Quaternion _tmpTransientRot; + + /// + /// (Called by KinematicCharacterMotor during its update cycle) + /// This is called before the character begins its movement update + /// + public void BeforeCharacterUpdate(float deltaTime) + { + } + + /// + /// (Called by KinematicCharacterMotor during its update cycle) + /// This is where you tell your character what its rotation should be right now. + /// This is the ONLY place where you should set the character's rotation + /// + public void UpdateRotation(ref Quaternion currentRotation, float deltaTime) + { + switch (CurrentCharacterState) + { + case CharacterState.Default: + { + if (_lookInputVector.sqrMagnitude > 0f && OrientationSharpness > 0f) + { + // Smoothly interpolate from current to target look direction + Vector3 smoothedLookInputDirection = Vector3.Slerp(Motor.CharacterForward, _lookInputVector, 1 - Mathf.Exp(-OrientationSharpness * deltaTime)).normalized; + + // Set the current rotation (which will be used by the KinematicCharacterMotor) + currentRotation = Quaternion.LookRotation(smoothedLookInputDirection, Motor.CharacterUp); + } + + Vector3 currentUp = (currentRotation * Vector3.up); + if (BonusOrientationMethod == BonusOrientationMethod.TowardsGravity) + { + // Rotate from current up to invert gravity + Vector3 smoothedGravityDir = Vector3.Slerp(currentUp, -Gravity.normalized, 1 - Mathf.Exp(-BonusOrientationSharpness * deltaTime)); + currentRotation = Quaternion.FromToRotation(currentUp, smoothedGravityDir) * currentRotation; + } + else if (BonusOrientationMethod == BonusOrientationMethod.TowardsGroundSlopeAndGravity) + { + if (Motor.GroundingStatus.IsStableOnGround) + { + Vector3 initialCharacterBottomHemiCenter = Motor.TransientPosition + (currentUp * Motor.Capsule.radius); + + Vector3 smoothedGroundNormal = Vector3.Slerp(Motor.CharacterUp, Motor.GroundingStatus.GroundNormal, 1 - Mathf.Exp(-BonusOrientationSharpness * deltaTime)); + currentRotation = Quaternion.FromToRotation(currentUp, smoothedGroundNormal) * currentRotation; + + // Move the position to create a rotation around the bottom hemi center instead of around the pivot + Motor.SetTransientPosition(initialCharacterBottomHemiCenter + (currentRotation * Vector3.down * Motor.Capsule.radius)); + } + else + { + Vector3 smoothedGravityDir = Vector3.Slerp(currentUp, -Gravity.normalized, 1 - Mathf.Exp(-BonusOrientationSharpness * deltaTime)); + currentRotation = Quaternion.FromToRotation(currentUp, smoothedGravityDir) * currentRotation; + } + } + else + { + Vector3 smoothedGravityDir = Vector3.Slerp(currentUp, Vector3.up, 1 - Mathf.Exp(-BonusOrientationSharpness * deltaTime)); + currentRotation = Quaternion.FromToRotation(currentUp, smoothedGravityDir) * currentRotation; + } + break; + } + } + } + + /// + /// (Called by KinematicCharacterMotor during its update cycle) + /// This is where you tell your character what its velocity should be right now. + /// This is the ONLY place where you can set the character's velocity + /// + public void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) + { + switch (CurrentCharacterState) + { + case CharacterState.Default: + { + // Ground movement + if (Motor.GroundingStatus.IsStableOnGround) + { + float currentVelocityMagnitude = currentVelocity.magnitude; + + Vector3 effectiveGroundNormal = Motor.GroundingStatus.GroundNormal; + + // Reorient velocity on slope + currentVelocity = Motor.GetDirectionTangentToSurface(currentVelocity, effectiveGroundNormal) * currentVelocityMagnitude; + + // Calculate target velocity + Vector3 inputRight = Vector3.Cross(_moveInputVector, Motor.CharacterUp); + Vector3 reorientedInput = Vector3.Cross(effectiveGroundNormal, inputRight).normalized * _moveInputVector.magnitude; + Vector3 targetMovementVelocity = reorientedInput * MaxStableMoveSpeed; + + // Smooth movement Velocity + currentVelocity = Vector3.Lerp(currentVelocity, targetMovementVelocity, 1f - Mathf.Exp(-StableMovementSharpness * deltaTime)); + } + // Air movement + else + { + // Add move input + if (_moveInputVector.sqrMagnitude > 0f) + { + Vector3 addedVelocity = _moveInputVector * AirAccelerationSpeed * deltaTime; + + Vector3 currentVelocityOnInputsPlane = Vector3.ProjectOnPlane(currentVelocity, Motor.CharacterUp); + + // Limit air velocity from inputs + if (currentVelocityOnInputsPlane.magnitude < MaxAirMoveSpeed) + { + // clamp addedVel to make total vel not exceed max vel on inputs plane + Vector3 newTotal = Vector3.ClampMagnitude(currentVelocityOnInputsPlane + addedVelocity, MaxAirMoveSpeed); + addedVelocity = newTotal - currentVelocityOnInputsPlane; + } + else + { + // Make sure added vel doesn't go in the direction of the already-exceeding velocity + if (Vector3.Dot(currentVelocityOnInputsPlane, addedVelocity) > 0f) + { + addedVelocity = Vector3.ProjectOnPlane(addedVelocity, currentVelocityOnInputsPlane.normalized); + } + } + + // Prevent air-climbing sloped walls + if (Motor.GroundingStatus.FoundAnyGround) + { + if (Vector3.Dot(currentVelocity + addedVelocity, addedVelocity) > 0f) + { + Vector3 perpenticularObstructionNormal = Vector3.Cross(Vector3.Cross(Motor.CharacterUp, Motor.GroundingStatus.GroundNormal), Motor.CharacterUp).normalized; + addedVelocity = Vector3.ProjectOnPlane(addedVelocity, perpenticularObstructionNormal); + } + } + + // Apply added velocity + currentVelocity += addedVelocity; + } + + // Gravity + currentVelocity += Gravity * deltaTime; + + // Drag + currentVelocity *= (1f / (1f + (Drag * deltaTime))); + } + + // Handle jumping + _jumpedThisFrame = false; + _timeSinceJumpRequested += deltaTime; + if (_jumpRequested) + { + // See if we actually are allowed to jump + if (!_jumpConsumed && ((AllowJumpingWhenSliding ? Motor.GroundingStatus.FoundAnyGround : Motor.GroundingStatus.IsStableOnGround) || _timeSinceLastAbleToJump <= JumpPostGroundingGraceTime)) + { + // Calculate jump direction before ungrounding + Vector3 jumpDirection = Motor.CharacterUp; + if (Motor.GroundingStatus.FoundAnyGround && !Motor.GroundingStatus.IsStableOnGround) + { + jumpDirection = Motor.GroundingStatus.GroundNormal; + } + + // Makes the character skip ground probing/snapping on its next update. + // If this line weren't here, the character would remain snapped to the ground when trying to jump. Try commenting this line out and see. + Motor.ForceUnground(); + + // Add to the return velocity and reset jump state + currentVelocity += (jumpDirection * JumpUpSpeed) - Vector3.Project(currentVelocity, Motor.CharacterUp); + currentVelocity += (_moveInputVector * JumpScalableForwardSpeed); + _jumpRequested = false; + _jumpConsumed = true; + _jumpedThisFrame = true; + } + } + + // Take into account additive velocity + if (_internalVelocityAdd.sqrMagnitude > 0f) + { + currentVelocity += _internalVelocityAdd; + _internalVelocityAdd = Vector3.zero; + } + break; + } + } + } + + /// + /// (Called by KinematicCharacterMotor during its update cycle) + /// This is called after the character has finished its movement update + /// + public void AfterCharacterUpdate(float deltaTime) + { + switch (CurrentCharacterState) + { + case CharacterState.Default: + { + // Handle jump-related values + { + // Handle jumping pre-ground grace period + if (_jumpRequested && _timeSinceJumpRequested > JumpPreGroundingGraceTime) + { + _jumpRequested = false; + } + + if (AllowJumpingWhenSliding ? Motor.GroundingStatus.FoundAnyGround : Motor.GroundingStatus.IsStableOnGround) + { + // If we're on a ground surface, reset jumping values + if (!_jumpedThisFrame) + { + _jumpConsumed = false; + } + _timeSinceLastAbleToJump = 0f; + } + else + { + // Keep track of time since we were last able to jump (for grace period) + _timeSinceLastAbleToJump += deltaTime; + } + } + + // Handle uncrouching + if (_isCrouching && !_shouldBeCrouching) + { + // Do an overlap test with the character's standing height to see if there are any obstructions + Motor.SetCapsuleDimensions(0.5f, 2f, 1f); + if (Motor.CharacterOverlap( + Motor.TransientPosition, + Motor.TransientRotation, + _probedColliders, + Motor.CollidableLayers, + QueryTriggerInteraction.Ignore) > 0) + { + // If obstructions, just stick to crouching dimensions + Motor.SetCapsuleDimensions(0.5f, CrouchedCapsuleHeight, CrouchedCapsuleHeight * 0.5f); + } + else + { + // If no obstructions, uncrouch + MeshRoot.localScale = new Vector3(1f, 1f, 1f); + _isCrouching = false; + } + } + break; + } + } + } + + public void PostGroundingUpdate(float deltaTime) + { + // Handle landing and leaving ground + if (Motor.GroundingStatus.IsStableOnGround && !Motor.LastGroundingStatus.IsStableOnGround) + { + OnLanded(); + } + else if (!Motor.GroundingStatus.IsStableOnGround && Motor.LastGroundingStatus.IsStableOnGround) + { + OnLeaveStableGround(); + } + } + + public bool IsColliderValidForCollisions(Collider coll) + { + if (IgnoredColliders.Count == 0) + { + return true; + } + + if (IgnoredColliders.Contains(coll)) + { + return false; + } + + return true; + } + + public void OnGroundHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, ref HitStabilityReport hitStabilityReport) + { + } + + public void OnMovementHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, ref HitStabilityReport hitStabilityReport) + { + } + + public void AddVelocity(Vector3 velocity) + { + switch (CurrentCharacterState) + { + case CharacterState.Default: + { + _internalVelocityAdd += velocity; + break; + } + } + } + + public void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, Vector3 atCharacterPosition, Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport) + { + } + + protected void OnLanded() + { + } + + protected void OnLeaveStableGround() + { + } + + public void OnDiscreteCollisionDetected(Collider hitCollider) + { + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterController.cs.meta b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterController.cs.meta new file mode 100644 index 0000000..196499c --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExampleCharacterController.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 76020eee813ed7844bcea94c5d5ce76a +timeCreated: 1503446428 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExamplePlayer.cs b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExamplePlayer.cs new file mode 100644 index 0000000..02c7f3b --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExamplePlayer.cs @@ -0,0 +1,99 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using KinematicCharacterController; +using KinematicCharacterController.Examples; + +namespace KinematicCharacterController.Examples +{ + public class ExamplePlayer : MonoBehaviour + { + public ExampleCharacterController Character; + public ExampleCharacterCamera CharacterCamera; + + private const string MouseXInput = "Mouse X"; + private const string MouseYInput = "Mouse Y"; + private const string MouseScrollInput = "Mouse ScrollWheel"; + private const string HorizontalInput = "Horizontal"; + private const string VerticalInput = "Vertical"; + + private void Start() + { + Cursor.lockState = CursorLockMode.Locked; + + // Tell camera to follow transform + CharacterCamera.SetFollowTransform(Character.CameraFollowPoint); + + // Ignore the character's collider(s) for camera obstruction checks + CharacterCamera.IgnoredColliders.Clear(); + CharacterCamera.IgnoredColliders.AddRange(Character.GetComponentsInChildren()); + } + + private void Update() + { + if (Input.GetMouseButtonDown(0)) + { + Cursor.lockState = CursorLockMode.Locked; + } + + HandleCharacterInput(); + } + + private void LateUpdate() + { + // Handle rotating the camera along with physics movers + if (CharacterCamera.RotateWithPhysicsMover && Character.Motor.AttachedRigidbody != null) + { + CharacterCamera.PlanarDirection = Character.Motor.AttachedRigidbody.GetComponent().RotationDeltaFromInterpolation * CharacterCamera.PlanarDirection; + CharacterCamera.PlanarDirection = Vector3.ProjectOnPlane(CharacterCamera.PlanarDirection, Character.Motor.CharacterUp).normalized; + } + + HandleCameraInput(); + } + + private void HandleCameraInput() + { + // Create the look input vector for the camera + float mouseLookAxisUp = Input.GetAxisRaw(MouseYInput); + float mouseLookAxisRight = Input.GetAxisRaw(MouseXInput); + Vector3 lookInputVector = new Vector3(mouseLookAxisRight, mouseLookAxisUp, 0f); + + // Prevent moving the camera while the cursor isn't locked + if (Cursor.lockState != CursorLockMode.Locked) + { + lookInputVector = Vector3.zero; + } + + // Input for zooming the camera (disabled in WebGL because it can cause problems) + float scrollInput = -Input.GetAxis(MouseScrollInput); +#if UNITY_WEBGL + scrollInput = 0f; +#endif + + // Apply inputs to the camera + CharacterCamera.UpdateWithInput(Time.deltaTime, scrollInput, lookInputVector); + + // Handle toggling zoom level + if (Input.GetMouseButtonDown(1)) + { + CharacterCamera.TargetDistance = (CharacterCamera.TargetDistance == 0f) ? CharacterCamera.DefaultDistance : 0f; + } + } + + private void HandleCharacterInput() + { + PlayerCharacterInputs characterInputs = new PlayerCharacterInputs(); + + // Build the CharacterInputs struct + characterInputs.MoveAxisForward = Input.GetAxisRaw(VerticalInput); + characterInputs.MoveAxisRight = Input.GetAxisRaw(HorizontalInput); + characterInputs.CameraRotation = CharacterCamera.Transform.rotation; + characterInputs.JumpDown = Input.GetKeyDown(KeyCode.Space); + characterInputs.CrouchDown = Input.GetKeyDown(KeyCode.C); + characterInputs.CrouchUp = Input.GetKeyUp(KeyCode.C); + + // Apply inputs to character + Character.SetInputs(ref characterInputs); + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExamplePlayer.cs.meta b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExamplePlayer.cs.meta new file mode 100644 index 0000000..e09302d --- /dev/null +++ b/Assets/Plugins/KinematicCharacterController/ExampleCharacter/Scripts/ExamplePlayer.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 461cd396e3fc7cc4eb9c92bde05c1b9a +timeCreated: 1485657184 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: