This commit is contained in:
CortexCore
2023-10-04 16:50:27 +08:00
parent 947e52e748
commit 5cd094ed9a
263 changed files with 144068 additions and 66 deletions

View File

@@ -0,0 +1,109 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.Utilities;
using Lightbug.CharacterControllerPro.Core;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Character/AI/Follow Behaviour")]
public class AIFollowBehaviour : CharacterAIBehaviour
{
[Tooltip("The target transform used by the follow behaviour.")]
[SerializeField]
Transform followTarget = null;
[Tooltip("Desired distance to the target. if the distance to the target is less than this value the character will not move.")]
[SerializeField]
float reachDistance = 3f;
[Tooltip("The wait time between actions updates.")]
[Min(0f)]
[SerializeField]
float refreshTime = 0.65f;
float timer = 0f;
NavMeshPath navMeshPath = null;
protected CharacterStateController stateController = null;
protected override void Awake()
{
base.Awake();
stateController = this.GetComponentInBranch<CharacterActor, CharacterStateController>();
stateController.MovementReferenceMode = MovementReferenceParameters.MovementReferenceMode.World;
}
void OnEnable()
{
navMeshPath = new NavMeshPath();
;
}
public override void EnterBehaviour(float dt)
{
timer = refreshTime;
}
public override void UpdateBehaviour(float dt)
{
if (timer >= refreshTime)
{
timer = 0f;
UpdateFollowTargetBehaviour(dt);
}
else
{
timer += dt;
}
}
// Follow Behaviour --------------------------------------------------------------------------------------------------
/// <summary>
/// Sets the target to follow (only for the follow behaviour).
/// </summary>
public void SetFollowTarget(Transform followTarget, bool forceUpdate = true)
{
this.followTarget = followTarget;
if (forceUpdate)
timer = refreshTime + Mathf.Epsilon;
}
void UpdateFollowTargetBehaviour(float dt)
{
if (followTarget == null)
return;
characterActions.Reset();
NavMesh.CalculatePath(transform.position, followTarget.position, NavMesh.AllAreas, navMeshPath);
if (navMeshPath.corners.Length < 2)
return;
Vector3 path = navMeshPath.corners[1] - navMeshPath.corners[0];
bool isDirectPath = navMeshPath.corners.Length == 2;
if (isDirectPath && path.magnitude <= reachDistance)
return;
if (navMeshPath.corners.Length > 1)
SetMovementAction(path);
}
}
}

View File

@@ -0,0 +1,114 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Character/AI/Sequence Behaviour")]
public class AISequenceBehaviour : CharacterAIBehaviour
{
const float DefaultDelayTime = 0.5f;
[SerializeField]
List<CharacterAIAction> actionSequence = new List<CharacterAIAction>();
// float timer = 0f;
float durationWaitTime = 0f;
float wallHitWaitTime = 0f;
int currentActionIndex = 0;
void OnEnable()
{
CharacterActor.OnWallHit += OnWallHit;
}
void OnDisable()
{
CharacterActor.OnWallHit -= OnWallHit;
}
public override void EnterBehaviour(float dt)
{
currentActionIndex = 0;
characterActions = actionSequence[currentActionIndex].action;
if (actionSequence[currentActionIndex].sequenceType == SequenceType.Duration)
{
durationWaitTime = actionSequence[currentActionIndex].duration;
}
}
public override void UpdateBehaviour(float dt)
{
// Process the timers
if (wallHitWaitTime > 0)
wallHitWaitTime = Mathf.Max(0f, wallHitWaitTime - dt);
if (durationWaitTime > 0)
durationWaitTime = Mathf.Max(0f, durationWaitTime - dt);
switch (actionSequence[currentActionIndex].sequenceType)
{
case SequenceType.Duration:
if (durationWaitTime == 0f)
SelectNextSequenceElement();
break;
case SequenceType.OnWallHit:
break;
}
}
void SelectNextSequenceElement()
{
if (currentActionIndex == (actionSequence.Count - 1))
currentActionIndex = 0;
else
currentActionIndex++;
characterActions = actionSequence[currentActionIndex].action;
durationWaitTime = actionSequence[currentActionIndex].duration;
}
void OnWallHit(Contact contact)
{
if (actionSequence[currentActionIndex].sequenceType != SequenceType.OnWallHit)
return;
if (wallHitWaitTime > 0f)
return;
bool characterCollision = contact.gameObject.GetComponent<CharacterActor>() != null;
if (characterCollision)
return;
SelectNextSequenceElement();
wallHitWaitTime = DefaultDelayTime;
}
}
}

View File

@@ -0,0 +1,114 @@
using UnityEngine;
using Lightbug.Utilities;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
namespace Lightbug.CharacterControllerPro.Demo
{
[CustomEditor(typeof(AISequenceBehaviour)), CanEditMultipleObjects]
public class AISequenceBehaviourEditor : Editor
{
ReorderableList reorderableList = null;
SerializedProperty actionSequence = null;
void OnEnable()
{
actionSequence = serializedObject.FindProperty("actionSequence");
reorderableList = new ReorderableList(
serializedObject,
actionSequence,
true,
true,
true,
true
);
reorderableList.elementHeight = 2 * EditorGUIUtility.singleLineHeight;
reorderableList.drawElementCallback += OnDrawElement;
reorderableList.drawHeaderCallback += OnDrawHeader;
reorderableList.elementHeightCallback += OnElementHeight;
}
void OnDisable()
{
reorderableList.drawElementCallback -= OnDrawElement;
reorderableList.drawHeaderCallback -= OnDrawHeader;
reorderableList.elementHeightCallback -= OnElementHeight;
}
void OnDrawHeader(Rect rect)
{
GUI.Label(rect, "Sequence");
}
void OnDrawElement(Rect rect, int index, bool isActive, bool isFocused)
{
SerializedProperty element = actionSequence.GetArrayElementAtIndex(index);
SerializedProperty sequenceType = element.FindPropertyRelative("sequenceType");
SerializedProperty duration = element.FindPropertyRelative("duration");
SerializedProperty action = element.FindPropertyRelative("action");
GUI.Box(rect, "", EditorStyles.helpBox);
Rect fieldRect = rect;
fieldRect.height = EditorGUIUtility.singleLineHeight;
fieldRect.x += 20;
fieldRect.width -= 30;
fieldRect.y += 0.5f * fieldRect.height;
EditorGUI.PropertyField(fieldRect, sequenceType);
fieldRect.y += 2 * fieldRect.height;
if (sequenceType.enumValueIndex == (int)SequenceType.Duration)
EditorGUI.PropertyField(fieldRect, duration);
fieldRect.y += 2 * fieldRect.height;
EditorGUI.PropertyField(fieldRect, action, true);
fieldRect.y += fieldRect.height;
}
float OnElementHeight(int index)
{
SerializedProperty element = actionSequence.GetArrayElementAtIndex(index);
SerializedProperty action = element.FindPropertyRelative("action");
float actionHeight = action.isExpanded ? EditorGUI.GetPropertyHeight(action) : EditorGUIUtility.singleLineHeight;
return 5 * EditorGUIUtility.singleLineHeight + actionHeight;
}
public override void OnInspectorGUI()
{
CustomUtilities.DrawMonoBehaviourField<AISequenceBehaviour>((AISequenceBehaviour)target);
serializedObject.Update();
GUILayout.Space(10);
reorderableList.DoLayoutList();
GUILayout.Space(10);
serializedObject.ApplyModifiedProperties();
}
}
}
#endif

View File

@@ -0,0 +1,101 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Character/AI/Wander Behaviour")]
public class AIWanderBehaviour : CharacterAIBehaviour
{
[Min(0f)]
[SerializeField]
float minRandomMagnitude = 1f;
[Min(0f)]
[SerializeField]
float maxRandomMagnitude = 1f;
[Min(0f)]
[SerializeField]
float minRandomYawAngle = 100f;
[Min(0f)]
[SerializeField]
float maxRandomYawAngle = 280f;
[Min(0f)]
[SerializeField]
float waitSeconds = 3f;
float timer = 0f;
Vector3 initialPosition = default(Vector3);
Vector3 target = default(Vector3);
void OnValidate()
{
if (minRandomMagnitude > maxRandomMagnitude)
minRandomMagnitude = maxRandomMagnitude;
if (maxRandomMagnitude < minRandomMagnitude)
maxRandomMagnitude = minRandomMagnitude;
if (minRandomYawAngle > maxRandomYawAngle)
minRandomYawAngle = maxRandomYawAngle;
if (maxRandomYawAngle < minRandomYawAngle)
maxRandomYawAngle = minRandomYawAngle;
}
public override void EnterBehaviour(float dt)
{
initialPosition = transform.position;
target = initialPosition + transform.forward * Random.Range(minRandomMagnitude, maxRandomMagnitude);
timer = 0f;
}
public override void UpdateBehaviour(float dt)
{
if (timer >= waitSeconds)
{
timer = 0f;
SetTarget();
}
else
{
timer += dt;
}
float distanceToTarget = (target - CharacterActor.Position).magnitude;
if (distanceToTarget > 0.5f)
SetMovementAction(target - CharacterActor.Position);
else
characterActions.Reset();
}
void SetTarget()
{
Vector3 centerToTargetDir = target - initialPosition;
centerToTargetDir.Normalize();
centerToTargetDir = Quaternion.Euler(0, Random.Range(minRandomYawAngle, maxRandomYawAngle), 0f) * centerToTargetDir;
target = initialPosition + centerToTargetDir * Random.Range(minRandomMagnitude, maxRandomMagnitude);
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
public abstract class AddTorque : MonoBehaviour
{
[SerializeField]
protected Vector3 torque;
[SerializeField]
protected float maxAngularVelocity = 200f;
protected abstract void AddTorqueToRigidbody();
protected virtual void Awake() { }
void FixedUpdate()
{
AddTorqueToRigidbody();
}
}
}

View File

@@ -0,0 +1,26 @@
using UnityEngine;
namespace Lightbug.CharacterControllerPro.Demo
{
public class AddTorque2D : AddTorque
{
new Rigidbody2D rigidbody = null;
protected override void Awake()
{
base.Awake();
rigidbody = GetComponent<Rigidbody2D>();
}
protected override void AddTorqueToRigidbody()
{
rigidbody.AddTorque(torque.z);
rigidbody.angularVelocity = Mathf.Clamp(rigidbody.angularVelocity, -maxAngularVelocity, maxAngularVelocity);
}
}
}

View File

@@ -0,0 +1,28 @@
using UnityEngine;
namespace Lightbug.CharacterControllerPro.Demo
{
public class AddTorque3D : AddTorque
{
new Rigidbody rigidbody = null;
protected override void Awake()
{
base.Awake();
rigidbody = GetComponent<Rigidbody>();
rigidbody.maxAngularVelocity = maxAngularVelocity;
}
protected override void AddTorqueToRigidbody()
{
rigidbody.AddRelativeTorque(torque);
}
}
}

View File

@@ -0,0 +1,18 @@
using UnityEngine;
namespace Lightbug.CharacterControllerPro.Demo
{
[RequireComponent(typeof(Animator))]
public class AnimationSpeed : MonoBehaviour
{
[Min(0f)]
public float speed = 1f;
Animator animator = null;
void Awake() => animator = GetComponent<Animator>();
void Start() => animator.speed = speed;
}
}

View File

@@ -0,0 +1,226 @@
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
public enum CameraTargetMode
{
Bounds,
Point
}
[AddComponentMenu("Character Controller Pro/Demo/Camera/Camera 2D")]
public class Camera2D : MonoBehaviour
{
[Header("Target")]
[SerializeField] Transform target = null;
[Header("Camera size")]
[SerializeField]
Vector2 cameraAABBSize = new Vector2(3, 4);
[SerializeField]
Vector2 targetAABBSize = new Vector2(1, 1);
[Header("Position")]
[SerializeField]
CameraTargetMode targetMode = CameraTargetMode.Bounds;
[SerializeField]
Vector3 offset = new Vector3(0f, 0f, -10f);
[SerializeField]
float smoothTargetTime = 0.25f;
[Header("Rotation")]
[SerializeField]
bool followRotation = true;
[Min(0.1f)]
[SerializeField]
float rotationSlerpSpeed = 5f;
[Header("Look ahead")]
[Condition("targetMode", ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.VisibilityType.Hidden, (int)CameraTargetMode.Bounds)]
[SerializeField]
float lookAheadSpeed = 4;
[Condition("targetMode", ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.VisibilityType.Hidden, (int)CameraTargetMode.Bounds)]
[SerializeField]
float xLookAheadAmount = 1;
[Condition("targetMode", ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.VisibilityType.Hidden, (int)CameraTargetMode.Bounds)]
[SerializeField]
float yLookAheadAmount = 1;
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
float xCurrentLookAheadAmount = 0;
float yCurrentLookAheadAmount = 0;
Vector3 targetCameraPosition;
Vector3 smoothDampVelocity;
Bounds cameraAABB;
Bounds targetBounds;
// Vector3 FinalTargetPosition => target.position + transform.TransformVector( offset );
void Start()
{
if (target == null)
Debug.Log("Missing camera target");
Vector3 startingPosition = target.position;
startingPosition.z = transform.position.z;
transform.position = startingPosition;
targetBounds = new Bounds(target.position, new Vector3(targetAABBSize.x, targetAABBSize.y, 1f));
targetBounds.center = target.position;
cameraAABB = new Bounds(target.position, new Vector3(cameraAABBSize.x, cameraAABBSize.y, 1f));
targetCameraPosition = new Vector3(cameraAABB.center.x, cameraAABB.center.y, transform.position.z);
}
void OnDrawGizmos()
{
if (target == null)
return;
if (targetMode != CameraTargetMode.Bounds)
return;
Gizmos.color = new Color(0f, 0f, 1f, 0.2f);
Bounds bounds = new Bounds(target.position, new Vector3(cameraAABBSize.x, cameraAABBSize.y, 1f));
Gizmos.DrawCube(bounds.center, new Vector3(bounds.size.x, bounds.size.y, 1f));
}
void LateUpdate()
{
if (target == null)
return;
float dt = Time.deltaTime;
UpdateTargetAABB();
UpdateCameraAABB(dt);
if (followRotation)
UpdateRotation(dt);
UpdatePosition(dt);
}
void UpdateTargetAABB()
{
targetBounds.center = target.position;
}
void UpdateCameraAABB(float dt)
{
float deltaLookAhead = lookAheadSpeed * dt;
//X
if (targetBounds.max.x > cameraAABB.max.x)
{
float deltaX = targetBounds.max.x - cameraAABB.max.x;
cameraAABB.center += Vector3.right * deltaX;
if (xCurrentLookAheadAmount < xLookAheadAmount)
{
xCurrentLookAheadAmount += deltaLookAhead;
xCurrentLookAheadAmount = Mathf.Clamp(xCurrentLookAheadAmount, -xLookAheadAmount, xLookAheadAmount);
}
}
else if (targetBounds.min.x < cameraAABB.min.x)
{
float deltaX = cameraAABB.min.x - targetBounds.min.x;
cameraAABB.center -= Vector3.right * deltaX;
//Look Ahead
if (xCurrentLookAheadAmount > -xLookAheadAmount)
{
xCurrentLookAheadAmount -= deltaLookAhead;
xCurrentLookAheadAmount = Mathf.Clamp(xCurrentLookAheadAmount, -xLookAheadAmount, xLookAheadAmount);
}
}
//Y
if (targetBounds.max.y > cameraAABB.max.y)
{
float deltaY = targetBounds.max.y - cameraAABB.max.y;
cameraAABB.center += Vector3.up * deltaY;
//Look Ahead
if (yCurrentLookAheadAmount < yLookAheadAmount)
{
yCurrentLookAheadAmount += deltaLookAhead;
yCurrentLookAheadAmount = Mathf.Clamp(yCurrentLookAheadAmount, -yLookAheadAmount, yLookAheadAmount);
}
}
else if (targetBounds.min.y < cameraAABB.min.y)
{
float deltaY = cameraAABB.min.y - targetBounds.min.y;
cameraAABB.center -= Vector3.up * deltaY;
//Look Ahead
if (yCurrentLookAheadAmount > -yLookAheadAmount)
{
yCurrentLookAheadAmount -= deltaLookAhead;
yCurrentLookAheadAmount = Mathf.Clamp(yCurrentLookAheadAmount, -yLookAheadAmount, yLookAheadAmount);
}
}
targetCameraPosition.x = cameraAABB.center.x + xCurrentLookAheadAmount;
targetCameraPosition.y = cameraAABB.center.y + yCurrentLookAheadAmount;
}
void UpdatePosition(float dt)
{
Vector3 targetPos = Vector3.zero;
if (targetMode == CameraTargetMode.Bounds)
{
targetPos = Vector3.SmoothDamp(transform.position, targetCameraPosition + transform.TransformVector(offset), ref smoothDampVelocity, smoothTargetTime);
}
else
{
targetPos = Vector3.SmoothDamp(transform.position, target.position + transform.TransformVector(offset), ref smoothDampVelocity, smoothTargetTime);
}
transform.position = targetPos;
}
void UpdateRotation(float dt)
{
Vector3 targetUp = Vector3.ProjectOnPlane(target.up, Vector3.forward);
Quaternion deltaRotation = Quaternion.AngleAxis(Vector3.SignedAngle(transform.up, targetUp, Vector3.forward), Vector3.forward);
transform.rotation *= Quaternion.Slerp(Quaternion.identity, deltaRotation, rotationSlerpSpeed * dt);
}
}
}

View File

@@ -0,0 +1,448 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Camera/Camera 3D")]
[DefaultExecutionOrder(ExecutionOrder.CharacterGraphicsOrder + 100)] // <--- Do your job after everything else
public class Camera3D : MonoBehaviour
{
[Header("Inputs")]
[SerializeField]
InputHandlerSettings inputHandlerSettings = new InputHandlerSettings();
[SerializeField]
string axes = "Camera";
[SerializeField]
string zoomAxis = "Camera Zoom";
[Header("Target")]
[Tooltip("Select the graphics root object as your target, the one containing all the meshes, sprites, animated models, etc. \n\nImportant: This will be the considered as the actual target (visual element).")]
[SerializeField]
Transform targetTransform = null;
[SerializeField]
Vector3 offsetFromHead = Vector3.zero;
[Tooltip("The interpolation speed used when the height of the character changes.")]
[SerializeField]
float heightLerpSpeed = 10f;
[Header("View")]
public CameraMode cameraMode = CameraMode.ThirdPerson;
[Header("First Person")]
public bool hideBody = true;
[SerializeField]
GameObject bodyObject = null;
[Header("Yaw")]
public bool updateYaw = true;
public float yawSpeed = 180f;
[Header("Pitch")]
public bool updatePitch = true;
[SerializeField]
float initialPitch = 45f;
public float pitchSpeed = 180f;
[Range(1f, 85f)]
public float maxPitchAngle = 80f;
[Range(1f, 85f)]
public float minPitchAngle = 80f;
[Header("Roll")]
public bool updateRoll = false;
[Header("Zoom (Third person)")]
public bool updateZoom = true;
[Min(0f)]
[SerializeField]
float distanceToTarget = 5f;
[Min(0f)]
public float zoomInOutSpeed = 40f;
[Min(0f)]
public float zoomInOutLerpSpeed = 5f;
[Min(0f)]
public float minZoom = 2f;
[Min(0.001f)]
public float maxZoom = 12f;
[Header("Collision")]
public bool collisionDetection = true;
public bool collisionAffectsZoom = false;
public float detectionRadius = 0.5f;
public LayerMask layerMask = 0;
public bool considerKinematicRigidbodies = true;
public bool considerDynamicRigidbodies = true;
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
CharacterActor characterActor = null;
Rigidbody characterRigidbody = null;
float currentDistanceToTarget;
float smoothedDistanceToTarget;
float deltaYaw = 0f;
float deltaPitch = 0f;
float deltaZoom = 0f;
Vector3 lerpedCharacterUp = Vector3.up;
Transform viewReference = null;
Renderer[] bodyRenderers = null;
RaycastHit[] hitsBuffer = new RaycastHit[10];
RaycastHit[] validHits = new RaycastHit[10];
Vector3 characterPosition = default(Vector3);
float lerpedHeight;
public enum CameraMode
{
FirstPerson,
ThirdPerson,
}
public void ToggleCameraMode()
{
cameraMode = cameraMode == CameraMode.FirstPerson ? CameraMode.ThirdPerson : CameraMode.FirstPerson;
}
void OnValidate()
{
initialPitch = Mathf.Clamp(initialPitch, -minPitchAngle, maxPitchAngle);
}
void Awake()
{
Initialize(targetTransform);
}
public bool Initialize(Transform targetTransform)
{
if (targetTransform == null)
return false;
characterActor = targetTransform.GetComponentInBranch<CharacterActor>();
if (characterActor == null || !characterActor.isActiveAndEnabled)
{
Debug.Log("The character actor component is null, or it is not active/enabled.");
return false;
}
characterRigidbody = characterActor.GetComponent<Rigidbody>();
inputHandlerSettings.Initialize(gameObject);
GameObject referenceObject = new GameObject("Camera reference");
viewReference = referenceObject.transform;
if (bodyObject != null)
bodyRenderers = bodyObject.GetComponentsInChildren<Renderer>();
return true;
}
void OnEnable()
{
if (characterActor == null)
return;
characterActor.OnTeleport += OnTeleport;
}
void OnDisable()
{
if (characterActor == null)
return;
characterActor.OnTeleport -= OnTeleport;
}
void Start()
{
characterPosition = targetTransform.position;
previousLerpedCharacterUp = targetTransform.up;
lerpedCharacterUp = previousLerpedCharacterUp;
currentDistanceToTarget = distanceToTarget;
smoothedDistanceToTarget = currentDistanceToTarget;
viewReference.rotation = targetTransform.rotation;
viewReference.Rotate(Vector3.right, initialPitch);
lerpedHeight = characterActor.BodySize.y;
}
void Update()
{
if (targetTransform == null)
{
this.enabled = false;
return;
}
Vector2 cameraAxes = inputHandlerSettings.InputHandler.GetVector2(axes);
if (updatePitch)
deltaPitch = -cameraAxes.y;
if (updateYaw)
deltaYaw = cameraAxes.x;
if (updateZoom)
deltaZoom = -inputHandlerSettings.InputHandler.GetFloat(zoomAxis);
// An input axis value (e.g. mouse x) usually gets accumulated over time. So, the higher the frame rate the smaller the value returned.
// In order to prevent inconsistencies due to frame rate changes, the camera movement uses a fixed delta time, instead of the old regular
// delta time.
float dt = Time.fixedDeltaTime;
UpdateCamera(dt);
}
void OnTeleport(Vector3 position, Quaternion rotation)
{
viewReference.rotation = rotation;
transform.rotation = viewReference.rotation;
lerpedCharacterUp = characterActor.Up;
previousLerpedCharacterUp = lerpedCharacterUp;
}
Vector3 previousLerpedCharacterUp = Vector3.up;
void HandleBodyVisibility()
{
if (cameraMode == CameraMode.FirstPerson)
{
if (bodyRenderers != null)
for (int i = 0; i < bodyRenderers.Length; i++)
{
if (bodyRenderers[i].GetType().IsSubclassOf(typeof(SkinnedMeshRenderer)))
{
SkinnedMeshRenderer skinnedMeshRenderer = (SkinnedMeshRenderer)bodyRenderers[i];
if (skinnedMeshRenderer != null)
skinnedMeshRenderer.forceRenderingOff = hideBody;
}
else
{
bodyRenderers[i].enabled = !hideBody;
}
}
}
else
{
if (bodyRenderers != null)
for (int i = 0; i < bodyRenderers.Length; i++)
{
if (bodyRenderers[i] == null)
continue;
if (bodyRenderers[i].GetType().IsSubclassOf(typeof(SkinnedMeshRenderer)))
{
SkinnedMeshRenderer skinnedMeshRenderer = (SkinnedMeshRenderer)bodyRenderers[i];
if (skinnedMeshRenderer != null)
skinnedMeshRenderer.forceRenderingOff = false;
}
else
{
bodyRenderers[i].enabled = true;
}
}
}
}
void UpdateCamera(float dt)
{
// Body visibility ---------------------------------------------------------------------
HandleBodyVisibility();
// Rotation -----------------------------------------------------------------------------------------
lerpedCharacterUp = targetTransform.up;
// Rotate the reference based on the lerped character up vector
Quaternion deltaRotation = Quaternion.FromToRotation(previousLerpedCharacterUp, lerpedCharacterUp);
previousLerpedCharacterUp = lerpedCharacterUp;
viewReference.rotation = deltaRotation * viewReference.rotation;
// Yaw rotation -----------------------------------------------------------------------------------------
viewReference.Rotate(lerpedCharacterUp, deltaYaw * yawSpeed * dt, Space.World);
// Pitch rotation -----------------------------------------------------------------------------------------
float angleToUp = Vector3.Angle(viewReference.forward, lerpedCharacterUp);
float minPitch = -angleToUp + (90f - minPitchAngle);
float maxPitch = 180f - angleToUp - (90f - maxPitchAngle);
float pitchAngle = Mathf.Clamp(deltaPitch * pitchSpeed * dt, minPitch, maxPitch);
viewReference.Rotate(Vector3.right, pitchAngle);
// Roll rotation -----------------------------------------------------------------------------------------
if (updateRoll)
{
viewReference.up = lerpedCharacterUp;//Quaternion.FromToRotation( viewReference.up , lerpedCharacterUp ) * viewReference.up;
}
// Position of the target -----------------------------------------------------------------------
characterPosition = targetTransform.position;
lerpedHeight = Mathf.Lerp(lerpedHeight, characterActor.BodySize.y, heightLerpSpeed * dt);
Vector3 targetPosition = characterPosition + targetTransform.up * lerpedHeight + targetTransform.TransformDirection(offsetFromHead);
viewReference.position = targetPosition;
Vector3 finalPosition = viewReference.position;
// ------------------------------------------------------------------------------------------------------
if (cameraMode == CameraMode.ThirdPerson)
{
currentDistanceToTarget += deltaZoom * zoomInOutSpeed * dt;
currentDistanceToTarget = Mathf.Clamp(currentDistanceToTarget, minZoom, maxZoom);
smoothedDistanceToTarget = Mathf.Lerp(smoothedDistanceToTarget, currentDistanceToTarget, zoomInOutLerpSpeed * dt);
Vector3 displacement = -viewReference.forward * smoothedDistanceToTarget;
if (collisionDetection)
{
bool hit = DetectCollisions(ref displacement, targetPosition);
if (collisionAffectsZoom && hit)
{
currentDistanceToTarget = smoothedDistanceToTarget = displacement.magnitude;
}
}
finalPosition = targetPosition + displacement;
}
transform.position = finalPosition;
transform.rotation = viewReference.rotation;
}
bool DetectCollisions(ref Vector3 displacement, Vector3 lookAtPosition)
{
int hits = Physics.SphereCastNonAlloc(
lookAtPosition,
detectionRadius,
Vector3.Normalize(displacement),
hitsBuffer,
currentDistanceToTarget,
layerMask,
QueryTriggerInteraction.Ignore
);
// Order the results
int validHitsNumber = 0;
for (int i = 0; i < hits; i++)
{
RaycastHit hitBuffer = hitsBuffer[i];
Rigidbody detectedRigidbody = hitBuffer.collider.attachedRigidbody;
// Filter the results ---------------------------
if (hitBuffer.distance == 0)
continue;
if (detectedRigidbody != null)
{
if (considerKinematicRigidbodies && !detectedRigidbody.isKinematic)
continue;
if (considerDynamicRigidbodies && detectedRigidbody.isKinematic)
continue;
if (detectedRigidbody == characterRigidbody)
continue;
}
//----------------------------------------------
validHits[validHitsNumber] = hitBuffer;
validHitsNumber++;
}
if (validHitsNumber == 0)
return false;
float distance = Mathf.Infinity;
for (int i = 0; i < validHitsNumber; i++)
{
RaycastHit hitBuffer = validHits[i];
if (hitBuffer.distance < distance)
distance = hitBuffer.distance;
}
displacement = CustomUtilities.Multiply(Vector3.Normalize(displacement), distance);
return true;
}
}
}

View File

@@ -0,0 +1,34 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Lightbug.CharacterControllerPro.Demo
{
public class ChangeTargetFrameRate : MonoBehaviour
{
public void SetTargetFrameRate(int targetFrameRate)
{
if (targetFrameRate < 0 && targetFrameRate != -1)
return;
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = targetFrameRate;
}
public void UseFullVSync()
{
QualitySettings.vSyncCount = 1;
}
public void UseHalfVSync()
{
QualitySettings.vSyncCount = 2;
}
}
}

View File

@@ -0,0 +1,39 @@
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
public class JumpPad : CharacterDetector
{
public bool useLocalSpace = true;
public Vector3 direction = Vector3.up;
public float jumpPadVelocity = 10f;
protected override void ProcessEnterAction(CharacterActor characterActor)
{
if (characterActor.GroundObject != gameObject)
return;
characterActor.ForceNotGrounded();
Vector3 direction = useLocalSpace ? transform.TransformDirection(this.direction) : this.direction;
characterActor.Velocity += direction * jumpPadVelocity;
}
protected override void ProcessStayAction(CharacterActor characterActor)
{
ProcessEnterAction(characterActor);
}
private void OnDrawGizmos()
{
Vector3 direction = useLocalSpace ? transform.TransformDirection(this.direction) : this.direction;
//Gizmos.DrawRay(transform.position, direction * 2f);
CustomUtilities.DrawArrowGizmo(transform.position, transform.position + direction * 2f, Color.red);
}
}
}

View File

@@ -0,0 +1,131 @@
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
public class PositionAndRotationModifier : CharacterDetector
{
public enum CallbackType
{
Enter,
Exit
}
[Header("Callbacks")]
public CallbackType callbackType = CallbackType.Enter;
[Header("Position")]
public bool teleport = false;
[Condition("teleport", ConditionAttribute.ConditionType.IsTrue, ConditionAttribute.VisibilityType.NotEditable)]
public Transform teleportTarget = null;
[Header("Rotation")]
public bool rotate = false;
[Condition("rotate", ConditionAttribute.ConditionType.IsTrue, ConditionAttribute.VisibilityType.Hidden)]
public RotationMode rotationMode = RotationMode.ModifyUp;
[Condition("rotationMode", ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.VisibilityType.Hidden, (int)RotationMode.ModifyUp)]
[Tooltip("The target Transform.up vector to use.")]
public Transform referenceTransform = null;
[Condition(
new string[] { "rotationMode", "rotate" },
new ConditionAttribute.ConditionType[] { ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.ConditionType.IsTrue },
new float[] { (int)RotationMode.AlignWithObject, 0f },
ConditionAttribute.VisibilityType.Hidden)]
[Tooltip("The target transform to use as the reference.")]
public Transform verticalAlignmentReference = null;
[Condition(
new string[] { "rotationMode", "rotate" },
new ConditionAttribute.ConditionType[] { ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.ConditionType.IsTrue },
new float[] { (int)RotationMode.AlignWithObject, 0f },
ConditionAttribute.VisibilityType.Hidden)]
public VerticalAlignmentSettings.VerticalReferenceMode upDirectionReferenceMode = VerticalAlignmentSettings.VerticalReferenceMode.Away;
public enum RotationMode
{
/// <summary>
///
/// </summary>
ModifyUp,
/// <summary>
///
/// </summary>
AlignWithObject
}
void Teleport(CharacterActor characterActor)
{
if (!teleport)
return;
if (teleportTarget == null)
return;
Vector3 targetPosition = teleportTarget.position;
// If the character is 2D, don't change the position z component (Transform).
if (characterActor.Is2D)
targetPosition.z = characterActor.transform.position.z;
characterActor.Teleport(targetPosition);
}
void Rotate(CharacterActor characterActor)
{
if (!rotate)
return;
switch (rotationMode)
{
case RotationMode.ModifyUp:
if (referenceTransform != null)
characterActor.Up = referenceTransform.up;
if (characterActor.constraintRotation)
{
characterActor.upDirectionReference = null;
characterActor.constraintUpDirection = characterActor.Up;
}
break;
case RotationMode.AlignWithObject:
// Just in case the rotation constraint is active ...
characterActor.constraintRotation = true;
characterActor.upDirectionReference = verticalAlignmentReference;
characterActor.upDirectionReferenceMode = upDirectionReferenceMode;
characterActor.constraintUpDirection = characterActor.Up;
break;
}
}
protected override void ProcessEnterAction(CharacterActor characterActor)
{
if (callbackType != CallbackType.Enter)
return;
Teleport(characterActor);
Rotate(characterActor);
}
protected override void ProcessExitAction(CharacterActor characterActor)
{
if (callbackType != CallbackType.Exit)
return;
Teleport(characterActor);
Rotate(characterActor);
}
}
}

View File

@@ -0,0 +1,279 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
/// <summary>
/// This component handles all the particles effects associated with the character.
/// </summary>
[AddComponentMenu("Character Controller Pro/Demo/Character/Character Particles")]
public class CharacterParticles : MonoBehaviour
{
[HelpBox("This script contains a \"PlayFootstep\" method, which is intended to be used as an animation event function. " +
"Please make sure the animation clip does trigger this event (if you want to see the effect in action).", HelpBoxMessageType.Warning)]
[Tooltip("This prefab will be used by the grounded and the footsteps particles.")]
[SerializeField]
GameObject groundParticlesPrefab = null;
[Header("Grounded particles")]
[Tooltip("The character vertical speed at the moment of impact (on the horizontal axis) vs the particle system main module start speed (on the vertical axis).")]
[SerializeField]
AnimationCurve groundParticlesSpeed = AnimationCurve.Linear(0f, 0.5f, 10f, 3f);
[Header("Footsteps particles")]
[Tooltip("The character on ground speed (on the horizontal axis) vs the particle system main module start speed (on the vertical axis).")]
[SerializeField]
AnimationCurve footstepParticleSpeed = AnimationCurve.Linear(0f, 0.5f, 10f, 3f);
[Tooltip("The character on ground speed (on the horizontal axis) vs the particle system main module start size (on the vertical axis).")]
[SerializeField]
AnimationCurve footstepParticleSize = AnimationCurve.Linear(0f, 0.5f, 10f, 3f);
ParticleSystem[] groundParticlesArray = new ParticleSystem[10];
ParticleSystemPooler particlesPooler = null;
MaterialController materialController = null;
CharacterActor CharacterActor = null;
void Awake()
{
CharacterActor = this.GetComponentInBranch<CharacterActor>();
materialController = this.GetComponentInBranch<CharacterActor, MaterialController>();
if (materialController == null)
{
Debug.Log("CharacterMaterial component missing");
this.enabled = false;
return;
}
if (groundParticlesPrefab != null)
particlesPooler = new ParticleSystemPooler(groundParticlesPrefab, CharacterActor.transform.position, CharacterActor.transform.rotation, 10);
}
void OnEnable() => CharacterActor.OnGroundedStateEnter += OnGroundedStateEnter;
void OnDisable() => CharacterActor.OnGroundedStateEnter -= OnGroundedStateEnter;
void OnGroundedStateEnter(Vector3 localVelocity)
{
Vector3 particlePosition = CharacterActor.transform.position;
Quaternion particleRotation = Quaternion.LookRotation(CharacterActor.GroundContactNormal);
float fallingSpeed = Mathf.Abs(localVelocity.y);
float particlesStartSpeed = groundParticlesSpeed.Evaluate(fallingSpeed);
particlesPooler.Instantiate(
particlePosition,
particleRotation,
materialController.CurrentSurface.color,
particlesStartSpeed
);
}
/// <summary>
/// Public method used by the animation events to play the footsteps particles.
/// </summary>
public void PlayFootstep()
{
if (!enabled)
return;
Vector3 particlePosition = CharacterActor.transform.position;
Quaternion particleRotation = CharacterActor.GroundContactNormal != Vector3.zero ? Quaternion.LookRotation(CharacterActor.GroundContactNormal) : Quaternion.identity;
float groundedSpeed = CharacterActor.Velocity.magnitude;
particlesPooler.Instantiate(
particlePosition,
particleRotation,
materialController.CurrentSurface.color,
footstepParticleSpeed.Evaluate(groundedSpeed),
footstepParticleSize.Evaluate(groundedSpeed)
);
}
void Update()
{
particlesPooler.Update();
}
}
/// <summary>
/// This class implements a simple "Particle System Pooler". By using this system it is possible to reuse a fixed number of particles, avoiding runtime instantiation (thus improving performance).
/// </summary>
public class ParticleSystemPooler
{
List<ParticleSystem> activeList = new List<ParticleSystem>();
List<ParticleSystem> inactiveList = new List<ParticleSystem>();
public ParticleSystemPooler(GameObject particlePrefab, Vector3 position, Quaternion rotation, int bufferLength)
{
for (int i = 0; i < bufferLength; i++)
{
GameObject gameObject = GameObject.Instantiate<GameObject>(particlePrefab, position, rotation);
ParticleSystem particleSystem = gameObject.GetComponent<ParticleSystem>();
ParticleSystem.MainModule mainModule = particleSystem.main;
mainModule.playOnAwake = false;
particleSystem.Stop(true);
if (particleSystem != null)
inactiveList.Add(particleSystem);
}
}
ParticleSystem SelectParticle()
{
ParticleSystem selectedParticle = null;
if (inactiveList.Count == 0)
{
selectedParticle = activeList[0];
}
else
{
selectedParticle = inactiveList[0];
inactiveList.RemoveAt(0);
activeList.Add(selectedParticle);
}
return selectedParticle;
}
/// <summary>
/// Puts a particle from the pool into the scene.
/// </summary>
public void Instantiate(Vector3 position, Quaternion rotation)
{
ParticleSystem particleSystem = SelectParticle();
particleSystem.transform.position = position;
particleSystem.transform.rotation = rotation;
particleSystem.Play(true);
}
/// <summary>
/// Puts a particle from the pool into the scene.
/// </summary>
public void Instantiate(Vector3 position, Quaternion rotation, Color color)
{
ParticleSystem particleSystem = SelectParticle();
ParticleSystem.MainModule mainModule = particleSystem.main;
particleSystem.transform.position = position;
particleSystem.transform.rotation = rotation;
Color particleColor = mainModule.startColor.color;
particleColor.r = color.r;
particleColor.g = color.g;
particleColor.b = color.b;
mainModule.startColor = particleColor;
particleSystem.Play(true);
activeList.Add(particleSystem);
}
/// <summary>
/// Puts a particle from the pool into the scene.
/// </summary>
public void Instantiate(Vector3 position, Quaternion rotation, Color color, float startSpeed)
{
ParticleSystem particleSystem = SelectParticle();
ParticleSystem.MainModule mainModule = particleSystem.main;
particleSystem.transform.position = position;
particleSystem.transform.rotation = rotation;
Color particleColor = mainModule.startColor.color;
particleColor.r = color.r;
particleColor.g = color.g;
particleColor.b = color.b;
mainModule.startColor = particleColor;
mainModule.startSpeed = startSpeed;
particleSystem.Play(true);
activeList.Add(particleSystem);
}
/// <summary>
/// Puts a particle from the pool into the scene.
/// </summary>
public void Instantiate(Vector3 position, Quaternion rotation, Color color, float startSpeed, float startSize)
{
ParticleSystem particleSystem = SelectParticle();
ParticleSystem.MainModule mainModule = particleSystem.main;
particleSystem.transform.position = position;
particleSystem.transform.rotation = rotation;
Color particleColor = mainModule.startColor.color;
particleColor.r = color.r;
particleColor.g = color.g;
particleColor.b = color.b;
mainModule.startColor = particleColor;
mainModule.startSpeed = startSpeed;
mainModule.startSize = startSize;
particleSystem.Play(true);
activeList.Add(particleSystem);
}
/// <summary>
/// Updates the system.
/// </summary>
public void Update()
{
for (int i = activeList.Count - 1; i >= 0; i--)
{
ParticleSystem particleSystem = activeList[i];
if (!particleSystem.isPlaying)
{
activeList.RemoveAt(i);
inactiveList.Add(particleSystem);
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
namespace Lightbug.CharacterControllerPro.Demo
{
[System.Serializable]
public class CharacterReferenceObject
{
[Tooltip("This transform up direction will be used as the character up.")]
public Transform referenceTransform;
[Tooltip("This transform up direction will be used as the character up.")]
public Transform verticalAlignmentReference = null;
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Lightbug.CharacterControllerPro.Demo
{
[RequireComponent(typeof(Rigidbody))]
public class CustomGravity : MonoBehaviour
{
public Transform planet;
public float gravity = 10f;
new Rigidbody rigidbody;
private void Awake()
{
if (planet == null)
{
enabled = false;
return;
}
rigidbody = GetComponent<Rigidbody>();
rigidbody.useGravity = false;
}
void FixedUpdate()
{
Vector3 dir = (planet.position - transform.position).normalized;
rigidbody.velocity += dir * gravity * Time.deltaTime;
}
}
}

View File

@@ -0,0 +1,192 @@
using UnityEngine;
using Lightbug.Utilities;
using Lightbug.CharacterControllerPro.Core;
namespace Lightbug.CharacterControllerPro.Demo
{
public class DemoSceneManager : MonoBehaviour
{
[Header("Character")]
[SerializeField]
CharacterActor characterActor = null;
[Header("Scene references")]
[SerializeField]
CharacterReferenceObject[] references = null;
[Header("UI")]
[SerializeField]
Canvas infoCanvas = null;
[SerializeField]
bool hideAndConfineCursor = true;
[Header("Graphics")]
[SerializeField]
GameObject graphicsObject = null;
[Header("Camera")]
[SerializeField]
new Camera3D camera = null;
[UnityEngine.Serialization.FormerlySerializedAs("frameRateText")]
[SerializeField]
UnityEngine.UI.Text targetFrameRateText = null;
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Renderer[] graphicsRenderers = null;
Renderer[] capsuleRenderers = null;
NormalMovement normalMovement = null;
float GetRefreshRateValue()
{
#if UNITY_2023_1_OR_NEWER
return (float)Screen.currentResolution.refreshRateRatio.value;
#else
return Screen.currentResolution.refreshRate;
#endif
}
void Awake()
{
if (characterActor != null)
normalMovement = characterActor.GetComponentInChildren<NormalMovement>();
// Set the looking direction mode
if (normalMovement != null && camera != null)
{
if (camera.cameraMode == Camera3D.CameraMode.FirstPerson)
normalMovement.lookingDirectionParameters.lookingDirectionMode = LookingDirectionParameters.LookingDirectionMode.ExternalReference;
else
normalMovement.lookingDirectionParameters.lookingDirectionMode = LookingDirectionParameters.LookingDirectionMode.Movement;
}
if (graphicsObject != null)
graphicsRenderers = graphicsObject.GetComponentsInChildren<Renderer>(true);
Cursor.visible = !hideAndConfineCursor;
Cursor.lockState = hideAndConfineCursor ? CursorLockMode.Locked : CursorLockMode.None;
if (targetFrameRateText != null)
{
targetFrameRateText.fontSize = 15;
targetFrameRateText.rectTransform.sizeDelta = new Vector2(
300f,
40f
);
if (QualitySettings.vSyncCount == 1)
{
targetFrameRateText.text = "Target frame rate = " + (GetRefreshRateValue()) + " fps ( Full Vsync )";
}
else if (QualitySettings.vSyncCount == 2)
{
targetFrameRateText.text = "Target frame rate = " + (GetRefreshRateValue() / 2) + " fps ( Half Vsync )";
}
else if (QualitySettings.vSyncCount == 0)
{
if (Application.targetFrameRate == -1)
targetFrameRateText.text = $"Target frame rate = Unlimited";
else
targetFrameRateText.text = $"Target frame rate = { Application.targetFrameRate } fps";
}
}
}
void Update()
{
for (int index = 0; index < references.Length; index++)
{
if (references[index] == null)
break;
if (Input.GetKeyDown(KeyCode.Alpha1 + index) || Input.GetKeyDown(KeyCode.Keypad1 + index))
{
GoTo(references[index]);
break;
}
}
if (Input.GetKeyDown(KeyCode.Tab))
{
if (infoCanvas != null)
infoCanvas.enabled = !infoCanvas.enabled;
}
if (Input.GetKeyDown(KeyCode.V))
{
// If the Camera3D is present, change between First person and Third person mode.
if (camera != null)
{
camera.ToggleCameraMode();
if (normalMovement != null)
{
if (camera.cameraMode == Camera3D.CameraMode.FirstPerson)
normalMovement.lookingDirectionParameters.lookingDirectionMode = LookingDirectionParameters.LookingDirectionMode.ExternalReference;
else
normalMovement.lookingDirectionParameters.lookingDirectionMode = LookingDirectionParameters.LookingDirectionMode.Movement;
}
}
}
}
void HandleVisualObjects(bool showCapsule)
{
if (capsuleRenderers != null)
for (int i = 0; i < capsuleRenderers.Length; i++)
capsuleRenderers[i].enabled = showCapsule;
if (graphicsRenderers != null)
for (int i = 0; i < graphicsRenderers.Length; i++)
{
SkinnedMeshRenderer skinnedMeshRenderer = (SkinnedMeshRenderer)graphicsRenderers[i];
if (skinnedMeshRenderer != null)
skinnedMeshRenderer.forceRenderingOff = showCapsule;
else
graphicsRenderers[i].enabled = !showCapsule;
}
}
void GoTo(CharacterReferenceObject reference)
{
if (reference == null)
return;
if (characterActor == null)
return;
characterActor.constraintUpDirection = reference.referenceTransform.up;
characterActor.Teleport(reference.referenceTransform);
characterActor.upDirectionReference = reference.verticalAlignmentReference;
characterActor.upDirectionReferenceMode = VerticalAlignmentSettings.VerticalReferenceMode.Away;
}
}
}

View File

@@ -0,0 +1,94 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace Lightbug.Utilities
{
public class FpsCounter : MonoBehaviour
{
[SerializeField]
float refreshTime = 0.2f;
[SerializeField]
Text text = null;
[SerializeField]
bool limitToRefreshRate = true;
public float Fps => fps;
int samples = 0;
string output = "FPS : ";
float fps = 60f;
Dictionary<float, string> frames = new Dictionary<float, string>();
float GetRefreshRateValue()
{
#if UNITY_2023_1_OR_NEWER
return (float)Screen.currentResolution.refreshRateRatio.value;
#else
return Screen.currentResolution.refreshRate;
#endif
}
void Awake()
{
fps = GetRefreshRateValue();
// Max value = 1000.00
// Resolution = 0.01
for (int i = 0; i < 100000; i++)
{
float frameFloat = i / 100f;
frames.Add(i, frameFloat.ToString("F2"));
}
if (text != null)
StartCoroutine(UpdateFPS());
}
float time = 0f;
void Update()
{
time += Time.unscaledDeltaTime;
samples++;
if (time >= refreshTime)
{
fps = samples / time;
PrintData();
time -= refreshTime;
samples = 0;
}
}
IEnumerator UpdateFPS()
{
var waitInstruction = new WaitForSecondsRealtime(refreshTime);
while (true)
{
yield return waitInstruction;
PrintData();
}
}
private void PrintData()
{
if (limitToRefreshRate && QualitySettings.vSyncCount != 0)
fps = Mathf.Min(fps, GetRefreshRateValue());
else
fps = Mathf.Min(fps, 1000f);
output = frames[(int)(fps * 100)];
text.text = $"{output}\n time = {(1000f * time / samples)} ms";
}
}
}

View File

@@ -0,0 +1,141 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
public class Ladder : MonoBehaviour
{
[Header("Debug")]
[SerializeField]
bool showGizmos = true;
[Header("Exit points")]
[SerializeField]
Transform topReference = null;
[SerializeField]
Transform bottomReference = null;
[Header("Properties")]
[Min(0)]
[SerializeField]
int climbingAnimations = 1;
[SerializeField]
Vector3 bottomLocalPosition = Vector3.zero;
[SerializeField]
Direction facingDirection = Direction.Forward;
public int ClimbingAnimations
{
get
{
return climbingAnimations;
}
}
public Transform TopReference
{
get
{
return topReference;
}
}
public Transform BottomReference
{
get
{
return bottomReference;
}
}
public Vector3 FacingDirectionVector
{
get
{
Vector3 facingDirectionVector = transform.forward;
switch (facingDirection)
{
case Direction.Left:
facingDirectionVector = -transform.right;
break;
case Direction.Right:
facingDirectionVector = transform.right;
break;
case Direction.Up:
facingDirectionVector = transform.up;
break;
case Direction.Down:
facingDirectionVector = -transform.up;
break;
case Direction.Forward:
facingDirectionVector = transform.forward;
break;
case Direction.Back:
facingDirectionVector = -transform.forward;
break;
}
return facingDirectionVector;
}
}
void Awake()
{
}
void OnDrawGizmos()
{
if (!showGizmos)
return;
if (bottomReference != null)
{
Gizmos.color = new Color(0f, 0f, 1f, 0.2f);
Gizmos.DrawCube(bottomReference.position, Vector3.one * 0.5f);
}
if (topReference != null)
{
Gizmos.color = new Color(1f, 0f, 0f, 0.2f);
Gizmos.DrawCube(topReference.position, Vector3.one * 0.5f);
}
CustomUtilities.DrawArrowGizmo(transform.position, transform.position + FacingDirectionVector, Color.blue);
Gizmos.color = Color.white;
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Lightbug.CharacterControllerPro.Demo
{
[RequireComponent(typeof(LineRenderer))]
public class LineRendererUtility : MonoBehaviour
{
public Transform target = null;
LineRenderer lineRenderer = null;
void Awake()
{
lineRenderer = GetComponent<LineRenderer>();
}
void Update()
{
if (target == null)
return;
lineRenderer.positionCount = 2;
lineRenderer.SetPositions(
new Vector3[] {
transform.position,
target.position
}
);
}
}
}

View File

@@ -0,0 +1,42 @@
using UnityEngine;
namespace Lightbug.CharacterControllerPro.Demo
{
public class LookAtTarget : MonoBehaviour
{
[SerializeField]
Transform lookAtTarget = null;
[SerializeField]
Transform positionTarget = null;
[SerializeField]
bool invertForwardDirection = true;
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Vector3 initialPositionOffset = default(Vector3);
void Start()
{
if (positionTarget != null)
initialPositionOffset = positionTarget.position - transform.position;
}
void Update()
{
if (lookAtTarget != null)
{
transform.LookAt(lookAtTarget);
if (invertForwardDirection)
transform.Rotate(Vector3.up * 180f);
}
if (positionTarget != null)
transform.position = positionTarget.position + initialPositionOffset;
}
}
}

View File

@@ -0,0 +1,60 @@
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Lightbug.CharacterControllerPro.Demo
{
public class MainMenuManager : MonoBehaviour
{
string mainMenuName = "";
static MainMenuManager instance = null;
public static MainMenuManager Instance => instance;
void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
mainMenuName = SceneManager.GetActiveScene().name;
}
else
{
Destroy(gameObject);
}
}
public void QuitApplication()
{
Application.Quit();
}
public void GoToScene(string sceneName)
{
if (sceneName == mainMenuName)
Cursor.visible = true;
else
Cursor.visible = false;
SceneManager.LoadScene(sceneName, LoadSceneMode.Single);
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
if (SceneManager.GetActiveScene().name == mainMenuName)
Application.Quit();
else
GoToScene(mainMenuName);
}
}
}
}

View File

@@ -0,0 +1,195 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Material Controller")]
[DefaultExecutionOrder(-10)]
public class MaterialController : MonoBehaviour
{
[SerializeField]
MaterialsProperties materialsProperties = null;
CharacterActor characterActor = null;
/// <summary>
/// This event is called when the character enters a volume.
///
/// The volume is passed as an argument.
/// </summary>
public event System.Action<Volume> OnVolumeEnter;
/// <summary>
/// This event is called when the character exits a volume.
///
/// The volume is passed as an argument.
/// </summary>
public event System.Action<Volume> OnVolumeExit;
/// <summary>
/// This event is called when the character step on a surface.
///
/// The surface is passed as an argument.
/// </summary>
public event System.Action<Surface> OnSurfaceEnter;
/// <summary>
/// This event is called when the character step off a surface.
///
/// The surface is passed as an argument.
/// </summary>
public event System.Action<Surface> OnSurfaceExit;
// Environment ------------------------------------------------------
Volume currentVolume = null;
Surface currentSurface = null;
/// <summary>
/// Gets the surface the character is colliding with. If this returns null the surface will be considered as "default".
/// </summary>
public Surface CurrentSurface => currentSurface;
/// <summary>
/// Gets the volume the character is colliding with. If this returns null the volume will be considered as "default".
/// </summary>
public Volume CurrentVolume => currentVolume;
void GetSurfaceData()
{
if (!characterActor.IsGrounded)
{
SetCurrentSurface(materialsProperties.DefaultSurface);
}
else
{
var ground = characterActor.GroundObject;
if (ground != null)
{
bool validSurface = materialsProperties.GetSurface(ground, out Surface surface);
if (validSurface)
{
SetCurrentSurface(surface);
}
else
{
// Untagged ground
if (ground.CompareTag("Untagged"))
{
SetCurrentSurface(materialsProperties.DefaultSurface);
}
}
}
}
}
void SetCurrentSurface(Surface surface)
{
if (surface != currentSurface)
{
if (OnSurfaceExit != null)
OnSurfaceExit(currentSurface);
if (OnSurfaceEnter != null)
OnSurfaceEnter(surface);
}
currentSurface = surface;
}
void GetVolumeData()
{
var triggerObject = characterActor.CurrentTrigger.gameObject;
if (triggerObject == null)
{
if (currentVolume != materialsProperties.DefaultVolume)
{
if (OnVolumeExit != null)
OnVolumeExit(currentVolume);
SetCurrentVolume(materialsProperties.DefaultVolume);
}
}
else
{
bool validVolume = materialsProperties.GetVolume(triggerObject, out Volume volume);
if (validVolume)
{
SetCurrentVolume(volume);
}
else
{
// If the current trigger is not a valid volume, then search for one that is.
int currentTriggerIndex = characterActor.Triggers.Count - 1;
for (int i = currentTriggerIndex; i >= 0; i--)
{
validVolume = materialsProperties.GetVolume(characterActor.Triggers[i].gameObject, out volume);
if (validVolume)
{
SetCurrentVolume(volume);
}
}
if (!validVolume)
{
SetCurrentVolume(materialsProperties.DefaultVolume);
}
}
}
}
void SetCurrentVolume(Volume volume)
{
if (volume != currentVolume)
{
if (OnVolumeExit != null)
OnVolumeExit(currentVolume);
if (OnVolumeEnter != null)
OnVolumeEnter(volume);
}
currentVolume = volume;
}
void Awake()
{
characterActor = this.GetComponentInBranch<CharacterActor>();
if (characterActor == null)
{
this.enabled = false;
return;
}
SetCurrentSurface(materialsProperties.DefaultSurface);
SetCurrentVolume(materialsProperties.DefaultVolume);
}
void FixedUpdate()
{
GetSurfaceData();
GetVolumeData();
}
}
}

View File

@@ -0,0 +1,66 @@
using UnityEngine;
namespace Lightbug.CharacterControllerPro.Demo
{
/// <summary>
/// This ScriptableObject contains all the properties used by the volumes and the surfaces. Create many instances as you want to create different environments.
/// </summary>
[CreateAssetMenu(menuName = "Character Controller Pro/Demo/Materials/Material Properties")]
public class MaterialsProperties : ScriptableObject
{
[SerializeField]
Surface defaultSurface = new Surface();
[SerializeField]
Volume defaultVolume = new Volume();
[SerializeField]
Surface[] surfaces = null;
[SerializeField]
Volume[] volumes = null;
public Surface DefaultSurface => defaultSurface;
public Volume DefaultVolume => defaultVolume;
public Surface[] Surfaces => surfaces;
public Volume[] Volumes => volumes;
public bool GetSurface(GameObject gameObject, out Surface outputSurface)
{
outputSurface = null;
for (int i = 0; i < surfaces.Length; i++)
{
var surface = surfaces[i];
if (gameObject.CompareTag(surface.tagName))
{
outputSurface = surface;
return true;
}
}
return false;
}
public bool GetVolume(GameObject gameObject, out Volume outputVolume)
{
outputVolume = null;
for (int i = 0; i < volumes.Length; i++)
{
var volume = volumes[i];
if (gameObject.CompareTag(volume.tagName))
{
outputVolume = volume;
return true;
}
}
return false;
}
}
}

View File

@@ -0,0 +1,219 @@
using UnityEngine;
using Lightbug.Utilities;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
namespace Lightbug.CharacterControllerPro.Demo
{
[CustomEditor(typeof(MaterialsProperties))]
public class MaterialsPropertiesEditor : Editor
{
ReorderableList surfacesList = null;
ReorderableList volumesList = null;
SerializedProperty defaultSurface = null;
SerializedProperty surfaceAccelerationMultiplier = null;
SerializedProperty surfaceDecelerationMultiplier = null;
SerializedProperty surfaceSpeedMultiplier = null;
SerializedProperty defaultVolume = null;
SerializedProperty volumeAccelerationMultiplier = null;
SerializedProperty volumeDecelerationMultiplier = null;
SerializedProperty volumeSpeedMultiplier = null;
SerializedProperty volumeGravityPositiveMultiplier = null;
SerializedProperty volumeGravityNegativeMultiplier = null;
SerializedProperty surfaces = null;
SerializedProperty volumes = null;
void OnEnable()
{
defaultSurface = serializedObject.FindProperty("defaultSurface");
surfaceAccelerationMultiplier = defaultSurface.FindPropertyRelative("accelerationMultiplier");
surfaceDecelerationMultiplier = defaultSurface.FindPropertyRelative("decelerationMultiplier");
surfaceSpeedMultiplier = defaultSurface.FindPropertyRelative("speedMultiplier");
defaultVolume = serializedObject.FindProperty("defaultVolume");
volumeAccelerationMultiplier = defaultVolume.FindPropertyRelative("accelerationMultiplier");
volumeDecelerationMultiplier = defaultVolume.FindPropertyRelative("decelerationMultiplier");
volumeSpeedMultiplier = defaultVolume.FindPropertyRelative("speedMultiplier");
volumeGravityPositiveMultiplier = defaultVolume.FindPropertyRelative("gravityPositiveMultiplier");
volumeGravityNegativeMultiplier = defaultVolume.FindPropertyRelative("gravityNegativeMultiplier");
surfaces = serializedObject.FindProperty("surfaces");
volumes = serializedObject.FindProperty("volumes");
surfacesList = new ReorderableList(
serializedObject, surfaces,
true,
false,
true,
true
);
volumesList = new ReorderableList(
serializedObject, volumes,
true,
false,
true,
true
);
volumes.isExpanded = true;
volumesList.elementHeight = 10 * EditorGUIUtility.singleLineHeight;
volumesList.headerHeight = 0f;
surfaces.isExpanded = true;
surfacesList.elementHeight = 8 * EditorGUIUtility.singleLineHeight;
surfacesList.headerHeight = 0f;
volumesList.drawElementCallback += OnDrawElementVolumes;
surfacesList.drawElementCallback += OnDrawElementSurfaces;
}
void OnDisable()
{
volumesList.drawElementCallback -= OnDrawElementVolumes;
surfacesList.drawElementCallback -= OnDrawElementSurfaces;
}
void OnDrawElementVolumes(Rect rect, int index, bool isActive, bool isFocused)
{
Rect fieldRect = rect;
fieldRect.height = EditorGUIUtility.singleLineHeight;
SerializedProperty item = volumes.GetArrayElementAtIndex(index);
item.isExpanded = true;
SerializedProperty itr = item.Copy();
EditorGUI.LabelField(fieldRect, itr.FindPropertyRelative("tagName").stringValue);
itr.Next(true);
fieldRect.y += 1.5f * fieldRect.height;
//bool enterChildren = true;
EditorGUI.PropertyField(fieldRect, itr, false);
int children = item.CountInProperty() - 1;
for (int i = 0; i < children; i++)
{
EditorGUI.PropertyField(fieldRect, itr, false);
itr.Next(false);
fieldRect.y += fieldRect.height;
}
}
void OnDrawElementSurfaces(Rect rect, int index, bool isActive, bool isFocused)
{
Rect fieldRect = rect;
fieldRect.height = EditorGUIUtility.singleLineHeight;
SerializedProperty item = surfaces.GetArrayElementAtIndex(index);
item.isExpanded = true;
SerializedProperty itr = item.Copy();
EditorGUI.LabelField(fieldRect, itr.FindPropertyRelative("tagName").stringValue);
itr.Next(true);
fieldRect.y += 1.5f * fieldRect.height;
//bool enterChildren = true;
EditorGUI.PropertyField(fieldRect, itr, false);
int children = item.CountInProperty() - 1;
for (int i = 0; i < children; i++)
{
EditorGUI.PropertyField(fieldRect, itr, false);
itr.Next(false);
fieldRect.y += fieldRect.height;
}
}
public override void OnInspectorGUI()
{
serializedObject.Update();
CustomUtilities.DrawScriptableObjectField<MaterialsProperties>((MaterialsProperties)target);
CustomUtilities.DrawEditorLayoutHorizontalLine(Color.gray, 8);
EditorGUILayout.LabelField("Default material", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("A default material parameter corresponds to any ground or spatial volume without a specific \"material tag\". " +
"A Surface affects grounded movement, while a Volume affects not grounded movement.", MessageType.Info);
GUILayout.Space(10);
CustomUtilities.DrawEditorLayoutHorizontalLine(Color.gray);
EditorGUILayout.LabelField("Default surface", EditorStyles.boldLabel);
CustomUtilities.DrawArrayElement(defaultSurface, null, true);
CustomUtilities.DrawEditorLayoutHorizontalLine(Color.gray);
EditorGUILayout.LabelField("Default volume", EditorStyles.boldLabel);
CustomUtilities.DrawArrayElement(defaultVolume, null, true);
// --------------------------------------------------------------------------------------------------------
GUILayout.Space(10);
CustomUtilities.DrawEditorLayoutHorizontalLine(Color.gray);
EditorGUILayout.LabelField("Tagged materials", EditorStyles.boldLabel);
GUILayout.Space(10);
CustomUtilities.DrawEditorLayoutHorizontalLine(Color.gray, 8);
EditorGUILayout.LabelField("Surfaces", EditorStyles.boldLabel);
CustomUtilities.DrawArray(surfaces, "tagName");
CustomUtilities.DrawEditorLayoutHorizontalLine(Color.gray, 8);
EditorGUILayout.LabelField("Volumes", EditorStyles.boldLabel);
CustomUtilities.DrawArray(volumes, "tagName");
serializedObject.ApplyModifiedProperties();
}
}
}
#endif

View File

@@ -0,0 +1,27 @@
using UnityEngine;
namespace Lightbug.CharacterControllerPro.Demo
{
[System.Serializable]
public class Surface
{
public string tagName = "";
[Header("Movement")]
[Min(0.01f)]
public float accelerationMultiplier = 1f;
[Min(0.01f)]
public float decelerationMultiplier = 1f;
[Min(0.01f)]
public float speedMultiplier = 1f;
[Header("Particles")]
public Color color = Color.gray;
}
}

View File

@@ -0,0 +1,31 @@
using UnityEngine;
namespace Lightbug.CharacterControllerPro.Demo
{
[System.Serializable]
public class Volume
{
public string tagName = "";
[Header("Movement")]
[Min(0.01f)]
public float accelerationMultiplier = 1f;
[Min(0.01f)]
public float decelerationMultiplier = 1f;
[Min(0.01f)]
public float speedMultiplier = 1f;
[Range(0.05f, 50f)]
public float gravityAscendingMultiplier = 1f;
[Range(0.05f, 50f)]
public float gravityDescendingMultiplier = 1f;
}
}

View File

@@ -0,0 +1,62 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
namespace Lightbug.CharacterControllerPro.Demo
{
public class MenuButton : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler
{
[SerializeField]
string sceneName = "";
[SerializeField]
Color highlightColor = Color.green;
[SerializeField]
float lerpSpeed = 5f;
Color normalColor;
Image image = null;
bool enter = false;
void Awake()
{
image = GetComponent<Image>();
if (image == null)
{
enabled = false;
return;
}
normalColor = image.color;
}
void Update()
{
image.color = Color.Lerp(image.color, enter ? highlightColor : normalColor, lerpSpeed * Time.deltaTime);
}
public void OnPointerClick(PointerEventData eventData)
{
MainMenuManager.Instance.GoToScene(sceneName);
}
public void OnPointerEnter(PointerEventData eventData)
{
enter = true;
}
public void OnPointerExit(PointerEventData eventData)
{
enter = false;
}
}
}

View File

@@ -0,0 +1,34 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
public class PerformanceCharacterBehaviour : MonoBehaviour
{
public CharacterActor characterActor = null;
float sineAmplitude;
float sineAngularSpeed;
float sinePhase;
void Start()
{
sineAmplitude = Random.Range(8f, 15f);
sineAngularSpeed = Random.Range(0.5f, 2f);
sinePhase = Random.Range(0f, 90f);
}
void FixedUpdate()
{
characterActor.VerticalVelocity += Vector3.down * 15f * Time.deltaTime;
characterActor.PlanarVelocity = CustomUtilities.Multiply(Vector3.forward, sineAmplitude * Mathf.Sin(Time.time * sineAngularSpeed + sinePhase));
}
}
}

View File

@@ -0,0 +1,85 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Lightbug.CharacterControllerPro.Core;
namespace Lightbug.CharacterControllerPro.Demo
{
public class PerformanceDemoManager : MonoBehaviour
{
[SerializeField]
GameObject characterPrefab = null;
[SerializeField]
Transform prefabInstantiationReference = null;
[SerializeField]
Text textField = null;
[SerializeField]
float maxInstantiationDistance = 50f;
int numberOfCharacters = 0;
List<CharacterActor> characterActors = new List<CharacterActor>(50);
void Awake()
{
if (characterPrefab == null)
{
Debug.Log("Missing prefab! Destroying this component...");
Destroy(this);
}
}
public void AddCharacters(int charactersToAdd)
{
if (characterPrefab == null)
return;
for (int i = 0; i < charactersToAdd; i++)
{
GameObject newCharacter = Instantiate<GameObject>(
characterPrefab,
prefabInstantiationReference.position + Vector3.right * Random.Range(-maxInstantiationDistance, maxInstantiationDistance) + Vector3.forward * Random.Range(-maxInstantiationDistance, maxInstantiationDistance),
Quaternion.identity * Quaternion.Euler(0, Random.Range(0f, 180f), 0f));
characterActors.Add(newCharacter.GetComponent<CharacterActor>());
}
this.numberOfCharacters += charactersToAdd;
if (textField != null)
textField.text = this.numberOfCharacters.ToString();
}
public void RemoveCharacters(int charactersToEliminate)
{
if (this.numberOfCharacters < charactersToEliminate)
{
RemoveAllCharacters();
return;
}
for (int i = charactersToEliminate - 1; i >= 0; i--)
{
Destroy(characterActors[i].gameObject);
characterActors.RemoveAt(i);
}
this.numberOfCharacters -= charactersToEliminate;
if (textField != null)
textField.text = this.numberOfCharacters.ToString();
}
public void RemoveAllCharacters()
{
RemoveCharacters(this.numberOfCharacters);
}
}
}

View File

@@ -0,0 +1,38 @@
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
namespace Lightbug.CharacterControllerPro.Demo
{
/// <summary>
/// A "KinematicPlatform" implementation whose movement and rotation is defined by an action (movement and/or rotation).
/// </summary>
[AddComponentMenu("Character Controller Pro/Demo/Dynamic Platform/Action Based Platform")]
public class ActionBasedPlatform : Platform
{
[SerializeField]
protected MovementAction movementAction = new MovementAction();
[SerializeField]
protected RotationAction rotationAction = new RotationAction();
void Start()
{
movementAction.Initialize(transform);
rotationAction.Initialize(transform);
}
void FixedUpdate()
{
float dt = Time.deltaTime;
Vector3 position = RigidbodyComponent.Position;
Quaternion rotation = RigidbodyComponent.Rotation;
movementAction.Tick(dt, ref position);
rotationAction.Tick(dt, ref position, ref rotation);
RigidbodyComponent.MoveAndRotate(position, rotation);
}
}
}

View File

@@ -0,0 +1,48 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
namespace Lightbug.CharacterControllerPro.Demo
{
public class ConveyorBeltPlatform : Platform
{
[SerializeField]
protected MovementAction movementAction = new MovementAction();
void Start()
{
movementAction.Initialize(transform);
StartCoroutine(PostSimulationUpdate());
}
Vector3 preSimulationPosition = Vector3.zero;
void FixedUpdate()
{
float dt = Time.deltaTime;
preSimulationPosition = RigidbodyComponent.Position;
Vector3 position = preSimulationPosition;
movementAction.Tick(dt, ref position);
RigidbodyComponent.Move(position);
}
IEnumerator PostSimulationUpdate()
{
YieldInstruction waitForFixedUpdate = new WaitForFixedUpdate();
while (true)
{
yield return waitForFixedUpdate;
RigidbodyComponent.Position = preSimulationPosition;
}
}
}
}

View File

@@ -0,0 +1,231 @@
using UnityEngine;
using Lightbug.Utilities;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Lightbug.CharacterControllerPro.Demo
{
/// <summary>
/// This class represents a movement action, used by the ActionBasedPlatform component.
/// </summary>
[System.Serializable]
public class MovementAction
{
[SerializeField]
bool enabled = true;
[SerializeField]
bool useWorldCoordinates = true;
[SerializeField]
bool infiniteDuration = false;
[Min(0f)]
[SerializeField]
float cycleDuration = 2f;
[SerializeField]
bool waitAtTheEnd = true;
[Min(0f)]
[SerializeField]
float waitDuration = 1f;
[SerializeField]
Vector3 direction = Vector3.up;
[Min(0f)]
[SerializeField]
float speed = 2f;
Transform transform = null;
Vector3 initialLocalDirection;
public void Initialize(Transform transform)
{
this.transform = transform;
initialLocalDirection = transform.InverseTransformVectorUnscaled(direction);
}
Vector3 actionVector = Vector3.zero;
public Vector3 ActionVector
{
get
{
return actionVector;
}
}
public void Tick(float dt, ref Vector3 position)
{
if (!enabled)
return;
time += dt;
if (isWaiting)
{
if (time >= waitDuration)
{
time = 0f;
isWaiting = false;
}
actionVector = Vector3.zero;
}
else
{
if (!infiniteDuration && time >= cycleDuration)
{
time = 0;
if (useWorldCoordinates)
direction = -direction;
else
initialLocalDirection = -initialLocalDirection;
if (waitAtTheEnd)
isWaiting = true;
}
if (isWaiting)
actionVector = Vector3.zero;
else
actionVector = CustomUtilities.Multiply(
useWorldCoordinates ? direction : initialLocalDirection,
speed,
dt
);
}
position += actionVector;
}
public void ResetTimer()
{
time = 0f;
}
float time = 0f;
bool isWaiting = false;
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(MovementAction))]
public class MovementActionDrawer : PropertyDrawer
{
SerializedProperty enabled = null;
SerializedProperty useWorldCoordinates = null;
SerializedProperty infiniteDuration = null;
SerializedProperty cycleDuration = null;
SerializedProperty waitAtTheEnd = null;
SerializedProperty waitDuration = null;
SerializedProperty direction = null;
SerializedProperty speed = null;
float size = 0f;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
enabled = property.FindPropertyRelative("enabled");
useWorldCoordinates = property.FindPropertyRelative("useWorldCoordinates");
infiniteDuration = property.FindPropertyRelative("infiniteDuration");
cycleDuration = property.FindPropertyRelative("cycleDuration");
waitAtTheEnd = property.FindPropertyRelative("waitAtTheEnd");
waitDuration = property.FindPropertyRelative("waitDuration");
direction = property.FindPropertyRelative("direction");
speed = property.FindPropertyRelative("speed");
// ----------------------------------------------------------------------------------
Rect fieldRect = position;
fieldRect.height = EditorGUIUtility.singleLineHeight;
Rect backgroundRect = position;
GUI.Box(backgroundRect, "", EditorStyles.helpBox);
fieldRect.y += 0.25f * fieldRect.height;
fieldRect.x += 5f;
fieldRect.width = 20f;
EditorGUI.PropertyField(fieldRect, enabled, GUIContent.none);
fieldRect.x += 20f;
fieldRect.width = position.width;
EditorGUI.LabelField(fieldRect, "Movement", EditorStyles.boldLabel);
fieldRect.x = position.x + 20f;
fieldRect.y += 1.5f * fieldRect.height;
fieldRect.width = position.width - 25;
if (enabled.boolValue)
{
EditorGUI.PropertyField(fieldRect, useWorldCoordinates);
fieldRect.y += fieldRect.height;
EditorGUI.PropertyField(fieldRect, infiniteDuration);
fieldRect.y += fieldRect.height;
if (!infiniteDuration.boolValue)
{
EditorGUI.PropertyField(fieldRect, cycleDuration);
fieldRect.y += fieldRect.height;
}
fieldRect.y += fieldRect.height;
EditorGUI.PropertyField(fieldRect, waitAtTheEnd);
fieldRect.y += fieldRect.height;
if (waitAtTheEnd.boolValue)
{
EditorGUI.PropertyField(fieldRect, waitDuration);
fieldRect.y += fieldRect.height;
}
fieldRect.y += fieldRect.height;
EditorGUI.PropertyField(fieldRect, direction);
fieldRect.y += fieldRect.height;
EditorGUI.PropertyField(fieldRect, speed);
fieldRect.y += fieldRect.height;
fieldRect.y += 0.5f * fieldRect.height;
}
size = fieldRect.y - position.y;
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return size;
}
public override bool CanCacheInspectorGUI(SerializedProperty property)
{
return false;
}
}
#endif
}

View File

@@ -0,0 +1,310 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
/// <summary>
/// A "KinematicPlatform" implementation whose movement and rotation are purely based on nodes. Use this component to create platforms that moves and
/// rotate precisely following a predefined path.
/// </summary>
[AddComponentMenu("Character Controller Pro/Demo/Dynamic Platform/Node Based Platform")]
public class NodeBasedPlatform : Platform
{
public enum SequenceType
{
Rewind,
Loop,
OneWay
}
enum ActionState
{
Idle,
Ready,
Waiting,
Working,
Done
}
//[Header("Debug Options")]
[SerializeField] bool drawHandles = true;
public bool DrawHandles
{
get
{
return drawHandles;
}
}
//[Header("Actions")]
public bool move = true;
public bool rotate = false;
[SerializeField]
List<PlatformNode> actionsList = new List<PlatformNode>();
public List<PlatformNode> ActionsList
{
get
{
return actionsList;
}
}
public SequenceType sequenceType;
public bool positiveSequenceDirection = true;
[Range(0.1f, 50)]
[SerializeField]
float globalSpeedModifier = 1;
ActionState actionState;
Vector3 targetPosition;
Vector3 targetRotation;
Vector3 startingPosition;
Vector3 startingRotation;
bool updateInitialPosition = true;
public bool UpdateInitialPosition
{
get
{
return updateInitialPosition;
}
}
Vector3 initialPosition;
public Vector3 InitialPosition
{
get
{
return initialPosition;
}
}
float time = 0;
PlatformNode currentAction = null;
int currentActionIndex = 0;
public int CurrentActionIndex
{
get
{
return currentActionIndex;
}
}
protected override void Awake()
{
base.Awake();
updateInitialPosition = false;
initialPosition = transform.position;
actionState = ActionState.Ready;
currentActionIndex = 0;
currentAction = actionsList[0];
}
void FixedUpdate()
{
float dt = Time.deltaTime;
switch (actionState)
{
case ActionState.Idle:
break;
case ActionState.Ready:
SetTargets();
actionState = ActionState.Working;
break;
case ActionState.Working:
time += dt * globalSpeedModifier;
if (time >= currentAction.targetTime)
{
actionState = ActionState.Done;
time = 0;
}
else
{
if (move)
RigidbodyComponent.Move(CalculatePosition());
Quaternion rotation = RigidbodyComponent.Rotation;
if (rotate)
RigidbodyComponent.Rotate(CalculateRotation());
}
break;
case ActionState.Done:
time = 0;
if (positiveSequenceDirection)
{
if (currentActionIndex != (actionsList.Count - 1))
{
currentActionIndex++;
actionState = ActionState.Ready;
}
else
{
switch (sequenceType)
{
case SequenceType.Loop:
currentActionIndex = 0;
actionState = ActionState.Ready;
break;
case SequenceType.Rewind:
currentActionIndex--;
positiveSequenceDirection = false;
actionState = ActionState.Ready;
break;
case SequenceType.OneWay:
actionState = ActionState.Idle;
break;
}
}
}
else
{
if (currentActionIndex != 0)
{
currentActionIndex--;
actionState = ActionState.Ready;
}
else
{
switch (sequenceType)
{
case SequenceType.Loop:
currentActionIndex = actionsList.Count - 1;
actionState = ActionState.Ready;
break;
case SequenceType.Rewind:
currentActionIndex++;
positiveSequenceDirection = true;
actionState = ActionState.Ready;
break;
case SequenceType.OneWay:
actionState = ActionState.Idle;
break;
}
}
}
currentAction = actionsList[currentActionIndex];
break;
}
}
public override string ToString()
{
return "Current Index = " + currentActionIndex + '\n' +
"State = " + actionState;
}
void SetTargets()
{
startingPosition = transform.position;
startingRotation = transform.eulerAngles;
targetPosition = initialPosition + currentAction.position;
targetRotation = currentAction.eulerAngles;
}
Vector3 CalculatePosition()
{
float curveTime = time / currentAction.targetTime;
Vector3 position = Vector3.Lerp(
startingPosition,
this.targetPosition,
currentAction.movementCurve.Evaluate(curveTime)
);
return position;
}
Quaternion CalculateRotation()
{
float curveTime = time / currentAction.targetTime;
float curveResult = currentAction.rotationCurve.Evaluate(curveTime);
Vector3 finalAngle;
finalAngle.x = Mathf.LerpAngle(startingRotation.x, this.targetRotation.x, curveResult);
finalAngle.y = Mathf.LerpAngle(startingRotation.y, this.targetRotation.y, curveResult);
finalAngle.z = Mathf.LerpAngle(startingRotation.z, this.targetRotation.z, curveResult);
Quaternion rotation = Quaternion.Euler(finalAngle);
return rotation;
}
}
}

View File

@@ -0,0 +1,312 @@
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
[CustomEditor(typeof(NodeBasedPlatform), true)]
public class NodeBasedPlatformEditor : Editor
{
NodeBasedPlatform monoBehaviour;
Transform transform;
ReorderableList reorderableList = null;
#region Properties
SerializedProperty drawHandles = null;
SerializedProperty move = null;
SerializedProperty rotate = null;
SerializedProperty actionsList = null;
SerializedProperty sequenceType;
SerializedProperty positiveSequenceDirection = null;
SerializedProperty globalSpeedModifier = null;
#endregion
int deletedObjectIndex = -1;
GUIStyle style = new GUIStyle();
void OnEnable()
{
monoBehaviour = (NodeBasedPlatform)target;
transform = monoBehaviour.GetComponent<Transform>();
style.fontSize = 25;
style.normal.textColor = Color.white;
drawHandles = serializedObject.FindProperty("drawHandles");
move = serializedObject.FindProperty("move");
rotate = serializedObject.FindProperty("rotate");
actionsList = serializedObject.FindProperty("actionsList");
sequenceType = serializedObject.FindProperty("sequenceType");
positiveSequenceDirection = serializedObject.FindProperty("positiveSequenceDirection");
globalSpeedModifier = serializedObject.FindProperty("globalSpeedModifier");
reorderableList = new ReorderableList(
serializedObject,
actionsList,
true,
false,
false,
false
);
//reorderableList.headerHeight = 0f;
reorderableList.footerHeight = 0f;
reorderableList.drawElementCallback += OnDrawElement;
reorderableList.elementHeightCallback += OnElementHeight;
reorderableList.onAddCallback += OnAddElement;
reorderableList.drawHeaderCallback += OnDrawHeader;
}
void OnDisable()
{
reorderableList.drawElementCallback -= OnDrawElement;
reorderableList.elementHeightCallback -= OnElementHeight;
reorderableList.onAddCallback -= OnAddElement;
reorderableList.drawHeaderCallback -= OnDrawHeader;
}
void OnDrawHeader(Rect rect)
{
GUI.Label(rect, "Nodes");
}
void OnDrawElement(Rect rect, int index, bool isActive, bool isFocused)
{
GUI.Box(rect, "", EditorStyles.helpBox);
SerializedProperty element = actionsList.GetArrayElementAtIndex(index);
Rect fieldRect = rect;
fieldRect.x += 4;
fieldRect.width -= 8;
fieldRect.height = EditorGUI.GetPropertyHeight(element);
fieldRect.y += EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(fieldRect, element);
fieldRect.y += fieldRect.height + EditorGUIUtility.singleLineHeight;
Rect buttonRect = fieldRect;
buttonRect.height = EditorGUIUtility.singleLineHeight;
if (GUI.Button(buttonRect, "x"))
{
deletedObjectIndex = index;
}
}
float OnElementHeight(int index)
{
SerializedProperty element = actionsList.GetArrayElementAtIndex(index);
return EditorGUI.GetPropertyHeight(element) + 4 * EditorGUIUtility.singleLineHeight;
}
void OnAddElement(ReorderableList list)
{
actionsList.arraySize++;
SerializedProperty element = actionsList.GetArrayElementAtIndex(actionsList.arraySize - 1);
// PlatformNode node = monobehaviour.ActionsList[ monobehaviour.ActionsList.Count ];
// node = new PlatformNode();
// PlatformNode node = element.objectReferenceValue as PlatformNode;
// node = new PlatformNode();
// //node.Initialize();
// //actionsList.
}
void OnSceneGUI()
{
if (!monoBehaviour.enabled)
return;
if (transform == null)
transform = monoBehaviour.GetComponent<Transform>();
if (!monoBehaviour.DrawHandles)
return;
for (int i = 0; i < monoBehaviour.ActionsList.Count; i++)
{
Vector3 position = monoBehaviour.UpdateInitialPosition ? transform.position : monoBehaviour.InitialPosition;
DrawHandle(position, monoBehaviour.ActionsList[i]);
DrawText(position, monoBehaviour.ActionsList[i], i);
if (i > 0)
{
//Line between nodes
Handles.color = new Color(1, 1, 1, 0.2f);
Handles.DrawDottedLine(position + monoBehaviour.ActionsList[i].position,
position + monoBehaviour.ActionsList[i - 1].position, 2);
Handles.color = new Color(1, 1, 1, 0.8f);
//Middle
Vector3 middle = ((position + monoBehaviour.ActionsList[i].position) + (position + monoBehaviour.ActionsList[i - 1].position)) / 2;
Vector3 direction = ((position + monoBehaviour.ActionsList[i].position) - (position + monoBehaviour.ActionsList[i - 1].position));
direction.Normalize();
float distance = ((position + monoBehaviour.ActionsList[i].position) - (position + monoBehaviour.ActionsList[i - 1].position)).magnitude;
float arrowSize = distance / 4;
Vector3 arrowPosition = middle - direction * arrowSize / 2;
if (direction != Vector3.zero)
DrawArrowCap(i, arrowPosition, Quaternion.LookRotation(direction, -Vector3.forward), arrowSize, EventType.Repaint);
if (monoBehaviour.sequenceType == NodeBasedPlatform.SequenceType.Loop)
{
if (i == (monoBehaviour.ActionsList.Count - 1))
{
Handles.color = new Color(1, 1, 1, 0.2f);
Handles.DrawDottedLine(position + monoBehaviour.ActionsList[i].position,
position + monoBehaviour.ActionsList[0].position, 2);
}
}
}
}
}
void DrawText(Vector3 referencePosition, PlatformNode currentAction, int index)
{
Vector3 TextPosition = referencePosition + currentAction.position;
style.fontSize = 25;
style.normal.textColor = Color.white;
Handles.Label(TextPosition, index.ToString(), style);
}
void DrawHandle(Vector3 referencePosition, PlatformNode currentAction)
{
float radius = 0.5f;
Handles.color = Color.white;
Handles.DrawWireDisc(referencePosition + currentAction.position, -Vector3.forward, radius);
Vector3[] lines = new Vector3[]{
referencePosition + currentAction.position + Vector3.up * radius ,
referencePosition + currentAction.position - Vector3.up * radius ,
referencePosition + currentAction.position + Vector3.right * radius ,
referencePosition + currentAction.position - Vector3.right * radius
};
Handles.DrawLines(lines);
//Position Handle
currentAction.position = Handles.PositionHandle(
referencePosition + currentAction.position, Quaternion.identity) - referencePosition;
}
public override void OnInspectorGUI()
{
serializedObject.Update();
CustomUtilities.DrawMonoBehaviourField<NodeBasedPlatform>(monoBehaviour);
GUILayout.Space(10);
GUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("Debug Options", EditorStyles.boldLabel);
GUILayout.Space(5);
EditorGUILayout.PropertyField(drawHandles);
GUILayout.EndVertical();
GUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("Actions", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(move);
EditorGUILayout.PropertyField(rotate);
reorderableList.DoLayoutList();
if (deletedObjectIndex != -1)
{
actionsList.DeleteArrayElementAtIndex(deletedObjectIndex);
deletedObjectIndex = -1;
}
if (GUILayout.Button("Add Node", EditorStyles.miniButton))
{
monoBehaviour.ActionsList.Add(new PlatformNode());
}
GUILayout.EndVertical();
GUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("Properties", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(sequenceType);
EditorGUILayout.PropertyField(positiveSequenceDirection);
EditorGUILayout.PropertyField(globalSpeedModifier);
GUILayout.EndVertical();
serializedObject.ApplyModifiedProperties();
//SceneView.RepaintAll();
}
void DrawArrowCap(int controlID, Vector3 position, Quaternion rotation, float size, EventType eventType)
{
#if UNITY_5_6_OR_NEWER
Handles.ArrowHandleCap(controlID, position, rotation, size, eventType);
#else
Handles.ArrowCap( controlID , position , rotation , size );
#endif
}
}
}
#endif

View File

@@ -0,0 +1,34 @@
using UnityEngine;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
/// <summary>
/// This abstract class represents a basic platform.
/// </summary>
public abstract class Platform : MonoBehaviour
{
/// <summary>
/// Gets the RigidbodyComponent component associated to the character.
/// </summary>
public RigidbodyComponent RigidbodyComponent { get; protected set; }
protected virtual void Awake()
{
RigidbodyComponent = RigidbodyComponent.CreateInstance(gameObject);
if (RigidbodyComponent == null)
{
Debug.Log("(2D/3D)Rigidbody component not found! \nDynamic platforms must have a Rigidbody component associated.");
this.enabled = false;
}
}
}
}

View File

@@ -0,0 +1,91 @@
using UnityEngine;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
#if UNITY_EDITOR
using UnityEditor;
#endif
[System.Serializable]
public class PlatformNode
{
public Vector3 position = Vector3.zero;
public Vector3 eulerAngles = Vector3.zero;
public AnimationCurve movementCurve = AnimationCurve.Linear(0, 0, 1, 1);
public AnimationCurve rotationCurve = AnimationCurve.Linear(0, 0, 1, 1);
[Min(0f)]
public float targetTime = 1;
public void Initialize()
{
position = Vector3.zero;
eulerAngles = Vector3.zero;
movementCurve = AnimationCurve.Linear(0, 0, 1, 1);
rotationCurve = AnimationCurve.Linear(0, 0, 1, 1);
targetTime = 1;
}
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(PlatformNode))]
public class PlatformNodeDrawer : PropertyDrawer
{
SerializedProperty position = null;
SerializedProperty eulerAngles = null;
SerializedProperty movementCurve = null;
SerializedProperty rotationCurve = null;
SerializedProperty targetTime = null;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
this.position = property.FindPropertyRelative("position");
this.eulerAngles = property.FindPropertyRelative("eulerAngles");
this.movementCurve = property.FindPropertyRelative("movementCurve");
this.rotationCurve = property.FindPropertyRelative("rotationCurve");
this.targetTime = property.FindPropertyRelative("targetTime");
Rect fieldRect = position;
fieldRect.height = EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(fieldRect, this.position);
fieldRect.y += fieldRect.height;
EditorGUI.PropertyField(fieldRect, this.eulerAngles);
fieldRect.y += fieldRect.height;
EditorGUI.PropertyField(fieldRect, this.movementCurve);
fieldRect.y += fieldRect.height;
EditorGUI.PropertyField(fieldRect, this.rotationCurve);
fieldRect.y += fieldRect.height;
EditorGUI.PropertyField(fieldRect, this.targetTime);
fieldRect.y += fieldRect.height;
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return 5 * EditorGUIUtility.singleLineHeight;
}
}
#endif
}

View File

@@ -0,0 +1,234 @@
using UnityEngine;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
#if UNITY_EDITOR
using UnityEditor;
#endif
/// <summary>
/// This class represents a rotation action, used by the ActionBasedPlatform component.
/// </summary>
[System.Serializable]
public class RotationAction
{
[SerializeField]
bool enabled = true;
[SerializeField]
bool infiniteDuration = false;
[Min(0f)]
[SerializeField]
float cycleDuration = 2f;
[SerializeField]
bool waitAtTheEnd = true;
[Min(0f)]
[SerializeField]
float waitDuration = 1f;
[SerializeField]
Vector3 direction = Vector3.up;
[Min(0f)]
[SerializeField]
float speed = 2f;
[SerializeField]
Transform pivotObject = null;
Transform transform = null;
public void Initialize(Transform transform)
{
this.transform = transform;
}
Vector3 actionVector = Vector3.zero;
public Vector3 ActionVector
{
get
{
return actionVector;
}
}
public void Tick(float dt, ref Vector3 position, ref Quaternion rotation)
{
if (!enabled)
return;
time += dt;
if (isWaiting)
{
if (time > waitDuration)
{
time = 0f;
isWaiting = false;
}
actionVector = Vector3.zero;
}
else
{
if (!infiniteDuration && time > cycleDuration)
{
time = 0;
direction = -direction;
if (waitAtTheEnd)
isWaiting = true;
}
if (isWaiting)
actionVector = Vector3.zero;
else
actionVector = CustomUtilities.Multiply(direction, speed, dt);
}
if (pivotObject != null)
RotateAround(ref position, ref rotation, dt);
else
rotation *= Quaternion.AngleAxis(speed * dt, direction);
}
void RotateAround(ref Vector3 position, ref Quaternion rotation, float dt)
{
Vector3 delta = position - pivotObject.position;
Quaternion thisRotation = Quaternion.AngleAxis(speed * dt, direction);
delta = thisRotation * delta;
position = pivotObject.position + delta;
rotation = rotation * thisRotation;
}
public void ResetTimer()
{
time = 0f;
}
float time = 0f;
bool isWaiting = false;
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(RotationAction))]
public class RotationActionDrawer : PropertyDrawer
{
SerializedProperty enabled = null;
SerializedProperty infiniteDuration = null;
SerializedProperty cycleDuration = null;
SerializedProperty waitAtTheEnd = null;
SerializedProperty waitDuration = null;
SerializedProperty direction = null;
SerializedProperty speed = null;
SerializedProperty pivotObject = null;
float size = 0f;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
enabled = property.FindPropertyRelative("enabled");
infiniteDuration = property.FindPropertyRelative("infiniteDuration");
cycleDuration = property.FindPropertyRelative("cycleDuration");
waitAtTheEnd = property.FindPropertyRelative("waitAtTheEnd");
waitDuration = property.FindPropertyRelative("waitDuration");
direction = property.FindPropertyRelative("direction");
speed = property.FindPropertyRelative("speed");
pivotObject = property.FindPropertyRelative("pivotObject");
// ----------------------------------------------------------------------------------
Rect fieldRect = position;
fieldRect.height = EditorGUIUtility.singleLineHeight;
Rect backgroundRect = position;
GUI.Box(backgroundRect, "", EditorStyles.helpBox);
fieldRect.y += 0.25f * fieldRect.height;
fieldRect.x += 5f;
fieldRect.width = 20f;
EditorGUI.PropertyField(fieldRect, enabled, GUIContent.none);
fieldRect.x += 20f;
fieldRect.width = position.width;
EditorGUI.LabelField(fieldRect, "Rotation", EditorStyles.boldLabel);
fieldRect.x = position.x + 20f;
fieldRect.y += 1.5f * fieldRect.height;
fieldRect.width = position.width - 25;
if (enabled.boolValue)
{
EditorGUI.PropertyField(fieldRect, infiniteDuration);
fieldRect.y += fieldRect.height;
if (!infiniteDuration.boolValue)
{
EditorGUI.PropertyField(fieldRect, cycleDuration);
fieldRect.y += fieldRect.height;
fieldRect.y += fieldRect.height;
EditorGUI.PropertyField(fieldRect, waitAtTheEnd);
fieldRect.y += fieldRect.height;
if (waitAtTheEnd.boolValue)
{
EditorGUI.PropertyField(fieldRect, waitDuration);
fieldRect.y += fieldRect.height;
}
}
fieldRect.y += fieldRect.height;
EditorGUI.PropertyField(fieldRect, direction);
fieldRect.y += fieldRect.height;
EditorGUI.PropertyField(fieldRect, speed);
fieldRect.y += fieldRect.height;
EditorGUI.PropertyField(fieldRect, pivotObject);
fieldRect.y += fieldRect.height;
fieldRect.y += 0.5f * fieldRect.height;
}
size = fieldRect.y - position.y;
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return size;
}
}
#endif
}

View File

@@ -0,0 +1,92 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
public class RigidbodyModifier : MonoBehaviour
{
[SerializeField]
AddMode mode = AddMode.AddForce;
[SerializeField]
Vector3 localAddVector = Vector3.zero;
[Min(0.01f)]
[SerializeField]
float dragMultiplier = 1f;
[Min(0.01f)]
[SerializeField]
float massMultiplier = 1f;
enum AddMode
{
AddForce,
Accelerate,
AddVelocity
}
Vector3 worldAddVector = default(Vector3);
Dictionary<Transform, Rigidbody> rigidbodies = new Dictionary<Transform, Rigidbody>();
void OnTriggerEnter(Collider otherCollider)
{
Rigidbody rigidbody = rigidbodies.GetOrRegisterValue<Transform, Rigidbody>(otherCollider.transform);
if (rigidbody == null)
return;
rigidbody.mass *= massMultiplier;
rigidbody.drag *= dragMultiplier;
}
void OnTriggerExit(Collider otherCollider)
{
Rigidbody rigidbody;
rigidbodies.TryGetValue(otherCollider.transform, out rigidbody);
if (rigidbody == null)
return;
rigidbody.mass /= massMultiplier;
rigidbody.drag /= dragMultiplier;
rigidbodies.Remove(otherCollider.transform);
}
void Start()
{
worldAddVector = transform.TransformDirection(localAddVector);
}
void FixedUpdate()
{
foreach (var rigidbody in rigidbodies)
{
switch (mode)
{
case AddMode.AddForce:
rigidbody.Value.AddForce(worldAddVector);
break;
case AddMode.Accelerate:
rigidbody.Value.velocity += worldAddVector * Time.deltaTime;
break;
case AddMode.AddVelocity:
rigidbody.Value.velocity += worldAddVector;
break;
}
}
}
}
}

View File

@@ -0,0 +1,86 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
public class Rope : MonoBehaviour
{
[Header("Debug")]
[SerializeField]
bool showGizmos = true;
[Header("Properties")]
[SerializeField]
Vector3 topLocalPosition = Vector3.zero;
[SerializeField]
Vector3 bottomLocalPosition = Vector3.zero;
public Vector3 TopPosition
{
get
{
return transform.position + transform.TransformVectorUnscaled(topLocalPosition);
}
}
public Vector3 BottomPosition
{
get
{
return transform.position + transform.TransformVectorUnscaled(bottomLocalPosition);
}
}
public Vector3 BottomToTop
{
get
{
return TopPosition - BottomPosition;
}
}
public bool IsInRange(Vector3 referencePosition)
{
Vector3 bottomToReference = referencePosition - BottomPosition;
if (Vector3.Angle(BottomToTop, bottomToReference) > 90f)
return false;
Vector3 topToReference = referencePosition - TopPosition;
if (Vector3.Angle(BottomToTop, topToReference) < 90f)
return false;
return true;
}
void OnDrawGizmos()
{
if (!showGizmos)
return;
Gizmos.color = new Color(0f, 1f, 0f, 0.2f);
Gizmos.DrawSphere(TopPosition, 0.25f);
Gizmos.color = new Color(0f, 0f, 1f, 0.2f);
Gizmos.DrawSphere(BottomPosition, 0.25f);
}
}
}

View File

@@ -0,0 +1,199 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.Utilities;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.CharacterControllerPro.Core;
namespace Lightbug.CharacterControllerPro.Demo
{
public enum DashMode
{
FacingDirection,
InputDirection
}
[AddComponentMenu("Character Controller Pro/Demo/Character/States/Dash")]
public class Dash : CharacterState
{
[Min(0f)]
[SerializeField]
protected float initialVelocity = 12f;
[Min(0f)]
[SerializeField]
protected float duration = 0.4f;
[SerializeField]
protected AnimationCurve movementCurve = AnimationCurve.Linear(0, 1, 1, 0);
[Min(0f)]
[SerializeField]
protected int availableNotGroundedDashes = 1;
[SerializeField]
protected bool ignoreSpeedMultipliers = false;
[SerializeField]
protected bool forceNotGrounded = true;
[Tooltip("Should the dash stop when we hit an obstacle (wall collision)?")]
[SerializeField]
protected bool cancelOnContact = true;
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
protected MaterialController materialController = null;
protected int airDashesLeft;
protected float dashCursor = 0;
protected Vector3 dashDirection = Vector2.right;
protected bool isDone = true;
protected float currentSpeedMultiplier = 1f;
#region Events
/// <summary>
/// This event is called when the dash state is entered.
///
/// The direction of the dash is passed as an argument.
/// </summary>
public event System.Action<Vector3> OnDashStart;
/// <summary>
/// This event is called when the dash action has ended.
///
/// The direction of the dash is passed as an argument.
/// </summary>
public event System.Action<Vector3> OnDashEnd;
#endregion
void OnEnable() => CharacterActor.OnGroundedStateEnter += OnGroundedStateEnter;
void OnDisable() => CharacterActor.OnGroundedStateEnter -= OnGroundedStateEnter;
public override string GetInfo()
{
return "This state is entirely based on particular movement, the \"dash\". This movement is normally a fast impulse along " +
"the forward direction. In this case the movement can be defined by using an animation curve (time vs velocity)";
}
void OnGroundedStateEnter(Vector3 localVelocity) => airDashesLeft = availableNotGroundedDashes;
bool EvaluateCancelOnContact() => CharacterActor.WallContacts.Count != 0;
protected override void Awake()
{
base.Awake();
materialController = this.GetComponentInBranch<CharacterActor, MaterialController>();
airDashesLeft = availableNotGroundedDashes;
}
public override bool CheckEnterTransition(CharacterState fromState)
{
if (!CharacterActor.IsGrounded && airDashesLeft <= 0)
return false;
return true;
}
public override void CheckExitTransition()
{
if (isDone)
{
if (OnDashEnd != null)
OnDashEnd(dashDirection);
CharacterStateController.EnqueueTransition<NormalMovement>();
}
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
if (forceNotGrounded)
CharacterActor.alwaysNotGrounded = true;
CharacterActor.UseRootMotion = false;
if (CharacterActor.IsGrounded)
{
if (!ignoreSpeedMultipliers)
{
currentSpeedMultiplier = materialController != null ? materialController.CurrentSurface.speedMultiplier * materialController.CurrentVolume.speedMultiplier : 1f;
}
}
else
{
if (!ignoreSpeedMultipliers)
{
currentSpeedMultiplier = materialController != null ? materialController.CurrentVolume.speedMultiplier : 1f;
}
airDashesLeft--;
}
//Set the dash direction
dashDirection = CharacterActor.Forward;
ResetDash();
//Execute the event
OnDashStart?.Invoke(dashDirection);
}
public override void ExitBehaviour(float dt, CharacterState toState)
{
if (forceNotGrounded)
CharacterActor.alwaysNotGrounded = false;
}
public override void UpdateBehaviour(float dt)
{
Vector3 dashVelocity = initialVelocity * currentSpeedMultiplier * movementCurve.Evaluate(dashCursor) * dashDirection;
CharacterActor.Velocity = dashVelocity;
float animationDt = dt / duration;
dashCursor += animationDt;
if (dashCursor >= 1)
{
isDone = true;
dashCursor = 0;
}
}
public override void PostUpdateBehaviour(float dt)
{
if (cancelOnContact)
isDone |= EvaluateCancelOnContact();
}
public virtual void ResetDash()
{
CharacterActor.Velocity = Vector3.zero;
isDone = false;
dashCursor = 0;
}
}
}

View File

@@ -0,0 +1,33 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
public class InputBasedBehaviour : StateMachineBehaviour
{
CharacterBrain brain;
[SerializeField]
string trigger = "";
[SerializeField]
Vector2 movementValue = Vector2.zero;
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (brain == null)
brain = animator.GetComponentInBranch<CharacterActor, CharacterBrain>();
}
public override void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (brain.CharacterActions.movement.value == movementValue)
{
animator.SetTrigger(trigger);
}
}
}
}

View File

@@ -0,0 +1,82 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Implementation;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Character/States/Jet Pack")]
public class JetPack : CharacterState
{
[Header("Planar movement")]
[SerializeField]
protected float targetPlanarSpeed = 5f;
[Header("Planar movement")]
[SerializeField]
protected float targetVerticalSpeed = 5f;
[SerializeField]
protected float duration = 1f;
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
protected Vector3 smoothDampVelocity = Vector3.zero;
public override string GetInfo()
{
return "This state allows the character to imitate a \"JetPack\" type of movement. Basically the character can ascend towards the up direction, " +
"but also move in the local XZ plane.";
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
CharacterActor.alwaysNotGrounded = true;
CharacterActor.UseRootMotion = false;
smoothDampVelocity = CharacterActor.VerticalVelocity;
}
public override void ExitBehaviour(float dt, CharacterState toState)
{
CharacterActor.alwaysNotGrounded = false;
}
public override void UpdateBehaviour(float dt)
{
// Vertical movement
CharacterActor.VerticalVelocity = Vector3.SmoothDamp(CharacterActor.VerticalVelocity, targetVerticalSpeed * CharacterActor.Up, ref smoothDampVelocity, duration);
// Planar movement
CharacterActor.PlanarVelocity = Vector3.Lerp(CharacterActor.PlanarVelocity, targetPlanarSpeed * CharacterStateController.InputMovementReference, 7f * dt);
// Looking direction
CharacterActor.SetYaw(CharacterActor.PlanarVelocity);
}
public override void CheckExitTransition()
{
if (CharacterActor.Triggers.Count != 0)
{
if (CharacterActions.interact.Started)
CharacterStateController.EnqueueTransition<LadderClimbing>();
}
else if (!CharacterActions.jetPack.value)
{
CharacterStateController.EnqueueTransition<NormalMovement>();
}
}
}
}

View File

@@ -0,0 +1,338 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.Utilities;
using static UnityEngine.EventSystems.PointerEventData;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Character/States/Ladder Climbing")]
public class LadderClimbing : CharacterState
{
[Header("Offset")]
[SerializeField]
protected bool useIKOffsetValues = false;
[Condition("useIKOffsetValues", ConditionAttribute.ConditionType.IsTrue)]
[SerializeField]
protected Vector3 rightFootOffset = Vector3.zero;
[Condition("useIKOffsetValues", ConditionAttribute.ConditionType.IsTrue)]
[SerializeField]
protected Vector3 leftFootOffset = Vector3.zero;
[Condition("useIKOffsetValues", ConditionAttribute.ConditionType.IsTrue)]
[SerializeField]
protected Vector3 rightHandOffset = Vector3.zero;
[Condition("useIKOffsetValues", ConditionAttribute.ConditionType.IsTrue)]
[SerializeField]
protected Vector3 leftHandOffset = Vector3.zero;
[Header("Activation")]
[SerializeField]
protected bool useInteractAction = true;
[SerializeField]
protected bool filterByAngle = true;
[SerializeField]
protected float maxAngle = 70f;
[Header("Animation")]
[SerializeField]
protected string bottomDownParameter = "BottomDown";
[SerializeField]
protected string bottomUpParameter = "BottomUp";
[SerializeField]
protected string topDownParameter = "TopDown";
[SerializeField]
protected string topUpParameter = "TopUp";
[SerializeField]
protected string upParameter = "Up";
[SerializeField]
protected string downParameter = "Down";
[Space(10f)]
[SerializeField]
protected string entryStateName = "Entry";
[SerializeField]
protected string exitStateName = "Exit";
[SerializeField]
protected string idleStateName = "Idle";
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
protected Dictionary<Transform, Ladder> ladders = new Dictionary<Transform, Ladder>();
public enum LadderClimbState
{
Entering,
Exiting,
Idling,
Climbing,
}
protected LadderClimbState state;
protected Ladder currentLadder = null;
protected Vector3 targetPosition = Vector3.zero;
protected int currentClimbingAnimation = 0;
protected bool forceExit = false;
protected AnimatorStateInfo animatorStateInfo;
protected bool isBottom = false;
public override void CheckExitTransition()
{
if (forceExit)
CharacterStateController.EnqueueTransition<NormalMovement>();
}
public override bool CheckEnterTransition(CharacterState fromState)
{
if (!CharacterActor.IsGrounded)
return false;
if (useInteractAction && !CharacterActions.interact.Started)
return false;
for (int i = 0; i < CharacterActor.Triggers.Count; i++)
{
Trigger trigger = CharacterActor.Triggers[i];
Ladder ladder = ladders.GetOrRegisterValue(trigger.transform);
if (ladder != null)
{
if (!useInteractAction && CharacterActor.WasGrounded && !trigger.firstContact)
{
return false;
}
currentLadder = ladder;
// Check if the character is closer to the top of the ladder.
float distanceToTop = Vector3.Distance(CharacterActor.Position, currentLadder.TopReference.position);
float distanceToBottom = Vector3.Distance(CharacterActor.Position, currentLadder.BottomReference.position);
isBottom = distanceToBottom < distanceToTop;
if (filterByAngle)
{
Vector3 ladderToCharacter = CharacterActor.Position - currentLadder.transform.position;
ladderToCharacter = Vector3.ProjectOnPlane(ladderToCharacter, currentLadder.transform.up);
float angle = Vector3.Angle(currentLadder.FacingDirectionVector, ladderToCharacter);
if (isBottom)
{
if (angle >= maxAngle)
return true;
else
continue;
}
else
{
if (angle <= maxAngle)
return true;
else
continue;
}
}
else
{
return true;
}
}
}
return false;
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
CharacterActor.Velocity = Vector3.zero;
CharacterActor.IsKinematic = true;
CharacterActor.alwaysNotGrounded = true;
currentClimbingAnimation = isBottom ? 0 : currentLadder.ClimbingAnimations;
targetPosition = isBottom ? currentLadder.BottomReference.position : currentLadder.TopReference.position;
CharacterActor.SetYaw(currentLadder.FacingDirectionVector);
CharacterActor.Position = targetPosition;
// Root motion
CharacterActor.SetUpRootMotion(
true,
PhysicsActor.RootMotionVelocityType.SetVelocity,
false
);
CharacterActor.Animator.SetTrigger(isBottom ? bottomUpParameter : topDownParameter);
state = LadderClimbState.Entering;
}
public override void ExitBehaviour(float dt, CharacterState toState)
{
forceExit = false;
CharacterActor.IsKinematic = false;
CharacterActor.alwaysNotGrounded = false;
currentLadder = null;
CharacterStateController.ResetIKWeights();
CharacterActor.Velocity = Vector3.zero;
CharacterActor.ForceGrounded();
}
protected override void Awake()
{
base.Awake();
#if UNITY_2023_1_OR_NEWER
Ladder[] laddersArray = FindObjectsByType<Ladder>(FindObjectsSortMode.None);
#else
Ladder[] laddersArray = FindObjectsOfType<Ladder>();
#endif
for (int i = 0; i < laddersArray.Length; i++)
ladders.Add(laddersArray[i].transform, laddersArray[i]);
}
protected override void Start()
{
base.Start();
if (CharacterActor.Animator == null)
{
Debug.Log("The LadderClimbing state needs the character to have a reference to an Animator component. Destroying this state...");
Destroy(this);
}
}
public override void UpdateBehaviour(float dt)
{
animatorStateInfo = CharacterActor.Animator.GetCurrentAnimatorStateInfo(0);
switch (state)
{
case LadderClimbState.Entering:
// The LadderClimbing state has just begun, Make sure to wait for the "Idle" animation state.
// Important: Note that the animation clip from this state (Animator) is the same as the locomotion idle clip.
if (animatorStateInfo.IsName(idleStateName))
state = LadderClimbState.Idling;
break;
case LadderClimbState.Idling:
// This state is responsible for handling inputs and setting animation triggers.
if (CharacterActions.interact.Started)
{
if (useInteractAction)
forceExit = true;
}
else
{
if (CharacterActions.movement.Up)
{
if (currentClimbingAnimation == currentLadder.ClimbingAnimations)
{
CharacterActor.Animator.SetTrigger(topUpParameter);
state = LadderClimbState.Exiting;
}
else
{
CharacterActor.Animator.SetTrigger(upParameter);
currentClimbingAnimation++;
state = LadderClimbState.Climbing;
}
}
else if (CharacterActions.movement.Down)
{
if (currentClimbingAnimation == 0)
{
CharacterActor.Animator.SetTrigger(bottomDownParameter);
state = LadderClimbState.Exiting;
}
else
{
CharacterActor.Animator.SetTrigger(downParameter);
currentClimbingAnimation--;
state = LadderClimbState.Climbing;
}
}
}
break;
case LadderClimbState.Climbing:
// Do nothing and wait for the "Idle" animation state
if (animatorStateInfo.IsName(idleStateName))
state = LadderClimbState.Idling;
break;
case LadderClimbState.Exiting:
// Do nothing and wait for the "Exit" animation state
// Important: Note that the animation clip from this state (Animator) is the same as the locomotion idle clip.
if (animatorStateInfo.IsName(exitStateName))
{
forceExit = true;
CharacterActor.ForceGrounded();
}
break;
}
}
public override void UpdateIK(int layerIndex)
{
if (!useIKOffsetValues)
return;
UpdateIKElement(AvatarIKGoal.LeftFoot, leftFootOffset);
UpdateIKElement(AvatarIKGoal.RightFoot, rightFootOffset);
UpdateIKElement(AvatarIKGoal.LeftHand, leftHandOffset);
UpdateIKElement(AvatarIKGoal.RightHand, rightHandOffset);
}
void UpdateIKElement(AvatarIKGoal avatarIKGoal, Vector3 offset)
{
// Get the original (weight = 0) ik position.
CharacterActor.Animator.SetIKPositionWeight(avatarIKGoal, 0f);
Vector3 originalRightFootPosition = CharacterActor.Animator.GetIKPosition(avatarIKGoal);
// Affect the original ik position with the offset.
CharacterActor.Animator.SetIKPositionWeight(avatarIKGoal, 1f);
CharacterActor.Animator.SetIKPosition(avatarIKGoal, originalRightFootPosition + offset);
}
}
}

View File

@@ -0,0 +1,376 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.Utilities;
using Lightbug.CharacterControllerPro.Implementation;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Character/States/Ledge Hanging")]
public class LedgeHanging : CharacterState
{
[Header("Filter")]
[SerializeField]
protected LayerMask layerMask = 0;
[SerializeField]
protected bool filterByTag = false;
[SerializeField]
protected string tagName = "Untagged";
[SerializeField]
protected bool detectRigidbodies = false;
[Header("Detection")]
[SerializeField]
protected bool groundedDetection = false;
[Tooltip("How far the hands are from the character along the forward direction.")]
[Min(0f)]
[SerializeField]
protected float forwardDetectionOffset = 0.5f;
[Tooltip("How far the hands are from the character along the up direction.")]
[Min(0.05f)]
[SerializeField]
protected float upwardsDetectionOffset = 1.8f;
[Min(0.05f)]
[SerializeField]
protected float separationBetweenHands = 1f;
[Tooltip("The distance used by the raycast methods.")]
[Min(0.05f)]
[SerializeField]
protected float ledgeDetectionDistance = 0.05f;
[Header("Offset")]
[SerializeField]
protected float verticalOffset = 0f;
[SerializeField]
protected float forwardOffset = 0f;
[Header("Movement")]
public float ledgeJumpVelocity = 10f;
[SerializeField]
protected bool autoClimbUp = true;
[Tooltip("If the previous state (\"fromState\") is contained in this list the autoClimbUp flag will be triggered.")]
[SerializeField]
protected CharacterState[] forceAutoClimbUpStates = null;
[Header("Animation")]
[SerializeField]
protected string topUpParameter = "TopUp";
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
protected const float MaxLedgeVerticalAngle = 50f;
public enum LedgeHangingState
{
Idle,
TopUp
}
protected LedgeHangingState state;
protected bool forceExit = false;
protected bool forceAutoClimbUp = false;
protected override void Awake()
{
base.Awake();
}
protected override void Start()
{
base.Start();
if (CharacterActor.Animator == null)
{
Debug.Log("The LadderClimbing state needs the character to have a reference to an Animator component. Destroying this state...");
Destroy(this);
}
}
public override void CheckExitTransition()
{
if (forceExit)
CharacterStateController.EnqueueTransition<NormalMovement>();
}
HitInfo leftHitInfo = new HitInfo();
HitInfo rightHitInfo = new HitInfo();
public override bool CheckEnterTransition(CharacterState fromState)
{
if (!groundedDetection && CharacterActor.IsAscending)
return false;
if (!groundedDetection && CharacterActor.IsGrounded)
return false;
if (!IsValidLedge(CharacterActor.Position))
return false;
return true;
}
Vector3 initialPosition;
public override void EnterBehaviour(float dt, CharacterState fromState)
{
forceExit = false;
initialPosition = CharacterActor.Position;
CharacterActor.alwaysNotGrounded = true;
CharacterActor.Velocity = Vector3.zero;
CharacterActor.IsKinematic = true;
// Set the size as the default one (CharacterBody component)
CharacterActor.SetSize(CharacterActor.DefaultBodySize, CharacterActor.SizeReferenceType.Top);
// Look towards the wall
CharacterActor.SetYaw(Vector3.ProjectOnPlane(-CharacterActor.WallContact.normal, CharacterActor.Up));
Vector3 referencePosition = 0.5f * (leftHitInfo.point + rightHitInfo.point);
Vector3 headToReference = referencePosition - CharacterActor.Top;
Vector3 correction = Vector3.Project(headToReference, CharacterActor.Up) +
verticalOffset * CharacterActor.Up +
forwardOffset * CharacterActor.Forward;
CharacterActor.Position = CharacterActor.Position + correction;
state = LedgeHangingState.Idle;
// Determine if the character should skip the "hanging" state and go directly to the "climbing" state.
for (int i = 0; i < forceAutoClimbUpStates.Length; i++)
{
CharacterState state = forceAutoClimbUpStates[i];
if (fromState == state)
{
forceAutoClimbUp = true;
break;
}
}
}
public override void ExitBehaviour(float dt, CharacterState toState)
{
CharacterActor.IsKinematic = false;
CharacterActor.alwaysNotGrounded = false;
forceAutoClimbUp = false;
if (ledgeJumpFlag)
{
ledgeJumpFlag = false;
CharacterActor.Position = initialPosition;
CharacterActor.Velocity = CharacterActor.Up * ledgeJumpVelocity;
}
else
{
CharacterActor.Velocity = Vector3.zero;
}
}
bool CheckValidClimb()
{
HitInfoFilter ledgeHitInfoFilter = new HitInfoFilter(layerMask, false, true);
bool overlap = CharacterActor.CharacterCollisions.CheckOverlap(
(leftHitInfo.point + rightHitInfo.point) / 2f,
CharacterActor.StepOffset,
in ledgeHitInfoFilter
);
return !overlap;
}
bool ledgeJumpFlag = false;
public override void UpdateBehaviour(float dt)
{
switch (state)
{
case LedgeHangingState.Idle:
if (CharacterActions.jump.Started)
{
forceExit = true;
ledgeJumpFlag = true;
}
else if (CharacterActions.movement.Up || autoClimbUp || forceAutoClimbUp)
{
if (CheckValidClimb())
{
state = LedgeHangingState.TopUp;
// Root motion
CharacterActor.SetUpRootMotion(
true,
PhysicsActor.RootMotionVelocityType.SetVelocity,
false
);
CharacterActor.Animator.SetTrigger(topUpParameter);
}
}
else if (CharacterActions.movement.Down)
{
forceExit = true;
}
break;
case LedgeHangingState.TopUp:
if (CharacterActor.Animator.GetCurrentAnimatorStateInfo(0).IsName("Exit"))
{
forceExit = true;
CharacterActor.ForceGrounded();
}
break;
}
}
bool IsValidLedge(Vector3 characterPosition)
{
if (!CharacterActor.WallCollision)
return false;
DetectLedge(
characterPosition,
out leftHitInfo,
out rightHitInfo
);
if (!leftHitInfo.hit || !rightHitInfo.hit)
return false;
if (filterByTag)
if (!leftHitInfo.transform.CompareTag(tagName) || !rightHitInfo.transform.CompareTag(tagName))
return false;
Vector3 interpolatedNormal = Vector3.Normalize(leftHitInfo.normal + rightHitInfo.normal);
float ledgeAngle = Vector3.Angle(CharacterActor.Up, interpolatedNormal);
if (ledgeAngle > MaxLedgeVerticalAngle)
return false;
return true;
}
void DetectLedge(Vector3 position, out HitInfo leftHitInfo, out HitInfo rightHitInfo)
{
HitInfoFilter ledgeHitInfoFilter = new HitInfoFilter(layerMask, !detectRigidbodies, true);
leftHitInfo = new HitInfo();
rightHitInfo = new HitInfo();
Vector3 forwardDirection = CharacterActor.WallCollision ? -CharacterActor.WallContact.normal : CharacterActor.Forward;
Vector3 sideDirection = Vector3.Cross(CharacterActor.Up, forwardDirection);
// Check if there is an object above
Vector3 upDetection = position + CharacterActor.Up * (upwardsDetectionOffset);
CharacterActor.PhysicsComponent.Raycast(
out HitInfo auxHitInfo,
CharacterActor.Center,
upDetection - CharacterActor.Center,
in ledgeHitInfoFilter
);
if (auxHitInfo.hit)
return;
Vector3 middleOrigin = upDetection + forwardDirection * (forwardDetectionOffset);
Vector3 leftOrigin = middleOrigin - sideDirection * (separationBetweenHands / 2f);
Vector3 rightOrigin = middleOrigin + sideDirection * (separationBetweenHands / 2f);
CharacterActor.PhysicsComponent.Raycast(
out leftHitInfo,
leftOrigin,
-CharacterActor.Up * ledgeDetectionDistance,
in ledgeHitInfoFilter
);
CharacterActor.PhysicsComponent.Raycast(
out rightHitInfo,
rightOrigin,
-CharacterActor.Up * ledgeDetectionDistance,
in ledgeHitInfoFilter
);
}
#if UNITY_EDITOR
CharacterBody characterBody = null;
void OnValidate()
{
characterBody = this.GetComponentInBranch<CharacterBody>();
}
void OnDrawGizmos()
{
Vector3 forwardDirection = transform.forward;
if (characterBody != null)
if (characterBody.Is2D)
forwardDirection = transform.right;
Vector3 sideDirection = Vector3.Cross(transform.up, forwardDirection);
Vector3 middleOrigin = transform.position + transform.up * (upwardsDetectionOffset) + forwardDirection * (forwardDetectionOffset);
Vector3 leftOrigin = middleOrigin - sideDirection * (separationBetweenHands / 2f);
Vector3 rightOrigin = middleOrigin + sideDirection * (separationBetweenHands / 2f);
CustomUtilities.DrawArrowGizmo(leftOrigin, leftOrigin - transform.up * ledgeDetectionDistance, Color.red, 0.15f);
CustomUtilities.DrawArrowGizmo(rightOrigin, rightOrigin - transform.up * ledgeDetectionDistance, Color.red, 0.15f);
}
#endif
}
}

View File

@@ -0,0 +1,817 @@
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.Utilities;
using Lightbug.CharacterControllerPro.Implementation;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Character/States/Normal Movement")]
public class NormalMovement : CharacterState
{
[Space(10)]
public PlanarMovementParameters planarMovementParameters = new PlanarMovementParameters();
public VerticalMovementParameters verticalMovementParameters = new VerticalMovementParameters();
public CrouchParameters crouchParameters = new CrouchParameters();
public LookingDirectionParameters lookingDirectionParameters = new LookingDirectionParameters();
[Header("Animation")]
[SerializeField]
protected string groundedParameter = "Grounded";
[SerializeField]
protected string stableParameter = "Stable";
[SerializeField]
protected string verticalSpeedParameter = "VerticalSpeed";
[SerializeField]
protected string planarSpeedParameter = "PlanarSpeed";
[SerializeField]
protected string horizontalAxisParameter = "HorizontalAxis";
[SerializeField]
protected string verticalAxisParameter = "VerticalAxis";
[SerializeField]
protected string heightParameter = "Height";
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
#region Events
/// <summary>
/// Event triggered when the character jumps.
/// </summary>
public event System.Action OnJumpPerformed;
/// <summary>
/// Event triggered when the character jumps from the ground.
/// </summary>
public event System.Action<bool> OnGroundedJumpPerformed;
/// <summary>
/// Event triggered when the character jumps while.
/// </summary>
public event System.Action<int> OnNotGroundedJumpPerformed;
#endregion
protected MaterialController materialController = null;
protected int notGroundedJumpsLeft = 0;
protected bool isAllowedToCancelJump = false;
protected bool wantToRun = false;
protected float currentPlanarSpeedLimit = 0f;
protected bool groundedJumpAvailable = true;
protected Vector3 jumpDirection = default(Vector3);
protected Vector3 targetLookingDirection = default(Vector3);
protected float targetHeight = 1f;
protected bool wantToCrouch = false;
protected bool isCrouched = false;
protected PlanarMovementParameters.PlanarMovementProperties currentMotion = new PlanarMovementParameters.PlanarMovementProperties();
bool reducedAirControlFlag = false;
float reducedAirControlInitialTime = 0f;
float reductionDuration = 0.5f;
protected override void Awake()
{
base.Awake();
notGroundedJumpsLeft = verticalMovementParameters.availableNotGroundedJumps;
materialController = this.GetComponentInBranch<CharacterActor, MaterialController>();
}
protected virtual void OnValidate()
{
verticalMovementParameters.OnValidate();
}
protected override void Start()
{
base.Start();
targetHeight = CharacterActor.DefaultBodySize.y;
float minCrouchHeightRatio = CharacterActor.BodySize.x / CharacterActor.BodySize.y;
crouchParameters.heightRatio = Mathf.Max(minCrouchHeightRatio, crouchParameters.heightRatio);
}
protected virtual void OnEnable()
{
CharacterActor.OnTeleport += OnTeleport;
}
protected virtual void OnDisable()
{
CharacterActor.OnTeleport -= OnTeleport;
}
public override string GetInfo()
{
return "This state serves as a multi purpose movement based state. It is responsible for handling gravity and jump, walk and run, crouch, " +
"react to the different material properties, etc. Basically it covers all the common movements involved " +
"in a typical game, from a 3D platformer to a first person walking simulator.";
}
void OnTeleport(Vector3 position, Quaternion rotation)
{
targetLookingDirection = CharacterActor.Forward;
isAllowedToCancelJump = false;
}
/// <summary>
/// Gets/Sets the useGravity toggle. Use this property to enable/disable the effect of gravity on the character.
/// </summary>
/// <value></value>
public bool UseGravity
{
get => verticalMovementParameters.useGravity;
set => verticalMovementParameters.useGravity = value;
}
public override void CheckExitTransition()
{
if (CharacterActions.jetPack.value)
{
CharacterStateController.EnqueueTransition<JetPack>();
}
else if (CharacterActions.dash.Started)
{
CharacterStateController.EnqueueTransition<Dash>();
}
else if (CharacterActor.Triggers.Count != 0)
{
CharacterStateController.EnqueueTransition<LadderClimbing>();
CharacterStateController.EnqueueTransition<RopeClimbing>();
}
else if (!CharacterActor.IsGrounded)
{
if (!CharacterActions.crouch.value)
CharacterStateController.EnqueueTransition<WallSlide>();
CharacterStateController.EnqueueTransition<LedgeHanging>();
}
}
public override void ExitBehaviour(float dt, CharacterState toState)
{
reducedAirControlFlag = false;
}
/// <summary>
/// Reduces the amount of acceleration and deceleration (not grounded state) until the character reaches the apex of the jump
/// (vertical velocity close to zero). This can be useful to prevent the character from accelerating/decelerating too quickly (e.g. right after performing a wall jump).
/// </summary>
public void ReduceAirControl(float reductionDuration = 0.5f)
{
reducedAirControlFlag = true;
reducedAirControlInitialTime = Time.time;
this.reductionDuration = reductionDuration;
}
void SetMotionValues(Vector3 targetPlanarVelocity)
{
float angleCurrentTargetVelocity = Vector3.Angle(CharacterActor.PlanarVelocity, targetPlanarVelocity);
switch (CharacterActor.CurrentState)
{
case CharacterActorState.StableGrounded:
currentMotion.acceleration = planarMovementParameters.stableGroundedAcceleration;
currentMotion.deceleration = planarMovementParameters.stableGroundedDeceleration;
currentMotion.angleAccelerationMultiplier = planarMovementParameters.stableGroundedAngleAccelerationBoost.Evaluate(angleCurrentTargetVelocity);
break;
case CharacterActorState.UnstableGrounded:
currentMotion.acceleration = planarMovementParameters.unstableGroundedAcceleration;
currentMotion.deceleration = planarMovementParameters.unstableGroundedDeceleration;
currentMotion.angleAccelerationMultiplier = planarMovementParameters.unstableGroundedAngleAccelerationBoost.Evaluate(angleCurrentTargetVelocity);
break;
case CharacterActorState.NotGrounded:
if (reducedAirControlFlag)
{
float time = Time.time - reducedAirControlInitialTime;
if (time <= reductionDuration)
{
currentMotion.acceleration = (planarMovementParameters.notGroundedAcceleration / reductionDuration) * time;
currentMotion.deceleration = (planarMovementParameters.notGroundedDeceleration / reductionDuration) * time;
}
else
{
reducedAirControlFlag = false;
currentMotion.acceleration = planarMovementParameters.notGroundedAcceleration;
currentMotion.deceleration = planarMovementParameters.notGroundedDeceleration;
}
}
else
{
currentMotion.acceleration = planarMovementParameters.notGroundedAcceleration;
currentMotion.deceleration = planarMovementParameters.notGroundedDeceleration;
}
currentMotion.angleAccelerationMultiplier = planarMovementParameters.notGroundedAngleAccelerationBoost.Evaluate(angleCurrentTargetVelocity);
break;
}
// Material values
if (materialController != null)
{
if (CharacterActor.IsGrounded)
{
currentMotion.acceleration *= materialController.CurrentSurface.accelerationMultiplier * materialController.CurrentVolume.accelerationMultiplier;
currentMotion.deceleration *= materialController.CurrentSurface.decelerationMultiplier * materialController.CurrentVolume.decelerationMultiplier;
}
else
{
currentMotion.acceleration *= materialController.CurrentVolume.accelerationMultiplier;
currentMotion.deceleration *= materialController.CurrentVolume.decelerationMultiplier;
}
}
}
/// <summary>
/// Processes the lateral movement of the character (stable and unstable state), that is, walk, run, crouch, etc.
/// This movement is tied directly to the "movement" character action.
/// </summary>
protected virtual void ProcessPlanarMovement(float dt)
{
//SetMotionValues();
float speedMultiplier = materialController != null ?
materialController.CurrentSurface.speedMultiplier * materialController.CurrentVolume.speedMultiplier : 1f;
bool needToAccelerate = CustomUtilities.Multiply(CharacterStateController.InputMovementReference, currentPlanarSpeedLimit).sqrMagnitude >= CharacterActor.PlanarVelocity.sqrMagnitude;
Vector3 targetPlanarVelocity = default;
switch (CharacterActor.CurrentState)
{
case CharacterActorState.NotGrounded:
if (CharacterActor.WasGrounded)
currentPlanarSpeedLimit = Mathf.Max(CharacterActor.PlanarVelocity.magnitude, planarMovementParameters.baseSpeedLimit);
//needToAccelerate = CustomUtilities.Multiply(CharacterStateController.InputMovementReference, currentPlanarSpeedLimit).sqrMagnitude >= CharacterActor.PlanarVelocity.sqrMagnitude;
targetPlanarVelocity = CustomUtilities.Multiply(CharacterStateController.InputMovementReference, speedMultiplier, currentPlanarSpeedLimit);
//GetAccelerationBoost(targetPlanarVelocity)
break;
case CharacterActorState.StableGrounded:
// Run ------------------------------------------------------------
if (planarMovementParameters.runInputMode == InputMode.Toggle)
{
if (CharacterActions.run.Started)
wantToRun = !wantToRun;
}
else
{
wantToRun = CharacterActions.run.value;
}
if (wantToCrouch || !planarMovementParameters.canRun)
wantToRun = false;
if (isCrouched)
{
currentPlanarSpeedLimit = planarMovementParameters.baseSpeedLimit * crouchParameters.speedMultiplier;
}
else
{
currentPlanarSpeedLimit = wantToRun ? planarMovementParameters.boostSpeedLimit : planarMovementParameters.baseSpeedLimit;
}
targetPlanarVelocity = CustomUtilities.Multiply(CharacterStateController.InputMovementReference, speedMultiplier, currentPlanarSpeedLimit);
//needToAccelerate = CharacterStateController.InputMovementReference != Vector3.zero;
break;
case CharacterActorState.UnstableGrounded:
currentPlanarSpeedLimit = planarMovementParameters.baseSpeedLimit;
//needToAccelerate = CustomUtilities.Multiply(CharacterStateController.InputMovementReference, currentPlanarSpeedLimit).sqrMagnitude >= CharacterActor.PlanarVelocity.sqrMagnitude;
targetPlanarVelocity = CustomUtilities.Multiply(CharacterStateController.InputMovementReference, speedMultiplier, currentPlanarSpeedLimit);
break;
}
SetMotionValues(targetPlanarVelocity);
float acceleration = currentMotion.acceleration;
if (needToAccelerate)
{
acceleration *= currentMotion.angleAccelerationMultiplier;
// Affect acceleration based on the angle between target velocity and current velocity
//float angleCurrentTargetVelocity = Vector3.Angle(CharacterActor.PlanarVelocity, targetPlanarVelocity);
//float accelerationBoost = 20f * (angleCurrentTargetVelocity / 180f);
//acceleration += accelerationBoost;
}
else
{
acceleration = currentMotion.deceleration;
}
CharacterActor.PlanarVelocity = Vector3.MoveTowards(
CharacterActor.PlanarVelocity,
targetPlanarVelocity,
acceleration * dt
);
}
protected virtual void ProcessGravity(float dt)
{
if (!verticalMovementParameters.useGravity)
return;
verticalMovementParameters.UpdateParameters();
float gravityMultiplier = 1f;
if (materialController != null)
gravityMultiplier = CharacterActor.LocalVelocity.y >= 0 ?
materialController.CurrentVolume.gravityAscendingMultiplier :
materialController.CurrentVolume.gravityDescendingMultiplier;
float gravity = gravityMultiplier * verticalMovementParameters.gravity;
if (!CharacterActor.IsStable)
CharacterActor.VerticalVelocity += CustomUtilities.Multiply(-CharacterActor.Up, gravity, dt);
}
protected bool UnstableGroundedJumpAvailable => !verticalMovementParameters.canJumpOnUnstableGround && CharacterActor.CurrentState == CharacterActorState.UnstableGrounded;
public enum JumpResult
{
Invalid,
Grounded,
NotGrounded
}
JumpResult CanJump()
{
JumpResult jumpResult = JumpResult.Invalid;
if (!verticalMovementParameters.canJump)
return jumpResult;
if (isCrouched)
return jumpResult;
switch (CharacterActor.CurrentState)
{
case CharacterActorState.StableGrounded:
if (CharacterActions.jump.StartedElapsedTime <= verticalMovementParameters.preGroundedJumpTime && groundedJumpAvailable)
jumpResult = JumpResult.Grounded;
break;
case CharacterActorState.NotGrounded:
if (CharacterActions.jump.Started)
{
// First check if the "grounded jump" is available. If so, execute a "coyote jump".
if (CharacterActor.NotGroundedTime <= verticalMovementParameters.postGroundedJumpTime && groundedJumpAvailable)
{
jumpResult = JumpResult.Grounded;
}
else if (notGroundedJumpsLeft != 0) // Do a not grounded jump
{
jumpResult = JumpResult.NotGrounded;
}
}
break;
case CharacterActorState.UnstableGrounded:
if (CharacterActions.jump.StartedElapsedTime <= verticalMovementParameters.preGroundedJumpTime && verticalMovementParameters.canJumpOnUnstableGround)
jumpResult = JumpResult.Grounded;
break;
}
return jumpResult;
}
protected virtual void ProcessJump(float dt)
{
ProcessRegularJump(dt);
ProcessJumpDown(dt);
}
#region JumpDown
protected virtual bool ProcessJumpDown(float dt)
{
if (!verticalMovementParameters.canJumpDown)
return false;
if (!CharacterActor.IsStable)
return false;
if (!CharacterActor.IsGroundAOneWayPlatform)
return false;
if (verticalMovementParameters.filterByTag)
{
if (!CharacterActor.GroundObject.CompareTag(verticalMovementParameters.jumpDownTag))
return false;
}
if (!ProcessJumpDownAction())
return false;
JumpDown(dt);
return true;
}
protected virtual bool ProcessJumpDownAction()
{
return isCrouched && CharacterActions.jump.Started;
}
protected virtual void JumpDown(float dt)
{
float groundDisplacementExtraDistance = 0f;
Vector3 groundDisplacement = CustomUtilities.Multiply(CharacterActor.GroundVelocity, dt);
// bool CharacterActor.transform.InverseTransformVectorUnscaled( Vector3.Project( groundDisplacement , CharacterActor.Up ) ).y
if (!CharacterActor.IsGroundAscending)
groundDisplacementExtraDistance = groundDisplacement.magnitude;
CharacterActor.ForceNotGrounded();
CharacterActor.Position -=
CustomUtilities.Multiply(
CharacterActor.Up,
CharacterConstants.ColliderMinBottomOffset + verticalMovementParameters.jumpDownDistance + groundDisplacementExtraDistance
);
CharacterActor.VerticalVelocity -= CustomUtilities.Multiply(CharacterActor.Up, verticalMovementParameters.jumpDownVerticalVelocity);
}
#endregion
#region Jump
protected virtual void ProcessRegularJump(float dt)
{
if (CharacterActor.IsGrounded)
{
notGroundedJumpsLeft = verticalMovementParameters.availableNotGroundedJumps;
groundedJumpAvailable = true;
}
if (isAllowedToCancelJump)
{
if (verticalMovementParameters.cancelJumpOnRelease)
{
if (CharacterActions.jump.StartedElapsedTime >= verticalMovementParameters.cancelJumpMaxTime || CharacterActor.IsFalling)
{
isAllowedToCancelJump = false;
}
else if (!CharacterActions.jump.value && CharacterActions.jump.StartedElapsedTime >= verticalMovementParameters.cancelJumpMinTime)
{
// Get the velocity mapped onto the current jump direction
Vector3 projectedJumpVelocity = Vector3.Project(CharacterActor.Velocity, jumpDirection);
CharacterActor.Velocity -= CustomUtilities.Multiply(projectedJumpVelocity, 1f - verticalMovementParameters.cancelJumpMultiplier);
isAllowedToCancelJump = false;
}
}
}
else
{
JumpResult jumpResult = CanJump();
switch (jumpResult)
{
case JumpResult.Grounded:
groundedJumpAvailable = false;
break;
case JumpResult.NotGrounded:
notGroundedJumpsLeft--;
break;
case JumpResult.Invalid:
return;
}
// Events ---------------------------------------------------
if (CharacterActor.IsGrounded)
{
if (OnGroundedJumpPerformed != null)
OnGroundedJumpPerformed(true);
}
else
{
if (OnNotGroundedJumpPerformed != null)
OnNotGroundedJumpPerformed(notGroundedJumpsLeft);
}
if (OnJumpPerformed != null)
OnJumpPerformed();
// Define the jump direction ---------------------------------------------------
jumpDirection = SetJumpDirection();
// Force "not grounded" state.
if (CharacterActor.IsGrounded)
CharacterActor.ForceNotGrounded();
// First remove any velocity associated with the jump direction.
CharacterActor.Velocity -= Vector3.Project(CharacterActor.Velocity, jumpDirection);
CharacterActor.Velocity += CustomUtilities.Multiply(jumpDirection, verticalMovementParameters.jumpSpeed);
if (verticalMovementParameters.cancelJumpOnRelease)
isAllowedToCancelJump = true;
}
}
/// <summary>
/// Returns the jump direction vector whenever the jump action is started.
/// </summary>
protected virtual Vector3 SetJumpDirection()
{
return CharacterActor.Up;
}
#endregion
void ProcessVerticalMovement(float dt)
{
ProcessGravity(dt);
ProcessJump(dt);
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
CharacterActor.alwaysNotGrounded = false;
targetLookingDirection = CharacterActor.Forward;
if (fromState == CharacterStateController.GetState<WallSlide>())
{
// "availableNotGroundedJumps + 1" because the update code will consume one jump!
notGroundedJumpsLeft = verticalMovementParameters.availableNotGroundedJumps + 1;
// Reduce the amount of air control (acceleration and deceleration) for 0.5 seconds.
ReduceAirControl(0.5f);
}
currentPlanarSpeedLimit = Mathf.Max(CharacterActor.PlanarVelocity.magnitude, planarMovementParameters.baseSpeedLimit);
CharacterActor.UseRootMotion = false;
}
protected virtual void HandleRotation(float dt)
{
HandleLookingDirection(dt);
}
void HandleLookingDirection(float dt)
{
if (!lookingDirectionParameters.changeLookingDirection)
return;
switch (lookingDirectionParameters.lookingDirectionMode)
{
case LookingDirectionParameters.LookingDirectionMode.Movement:
switch (CharacterActor.CurrentState)
{
case CharacterActorState.NotGrounded:
SetTargetLookingDirection(lookingDirectionParameters.notGroundedLookingDirectionMode);
break;
case CharacterActorState.StableGrounded:
SetTargetLookingDirection(lookingDirectionParameters.stableGroundedLookingDirectionMode);
break;
case CharacterActorState.UnstableGrounded:
SetTargetLookingDirection(lookingDirectionParameters.unstableGroundedLookingDirectionMode);
break;
}
break;
case LookingDirectionParameters.LookingDirectionMode.ExternalReference:
if (!CharacterActor.CharacterBody.Is2D)
targetLookingDirection = CharacterStateController.MovementReferenceForward;
break;
case LookingDirectionParameters.LookingDirectionMode.Target:
targetLookingDirection = (lookingDirectionParameters.target.position - CharacterActor.Position);
targetLookingDirection.Normalize();
break;
}
Quaternion targetDeltaRotation = Quaternion.FromToRotation(CharacterActor.Forward, targetLookingDirection);
Quaternion currentDeltaRotation = Quaternion.Slerp(Quaternion.identity, targetDeltaRotation, lookingDirectionParameters.speed * dt);
if (CharacterActor.CharacterBody.Is2D)
CharacterActor.SetYaw(targetLookingDirection);
else
CharacterActor.SetYaw(currentDeltaRotation * CharacterActor.Forward);
}
void SetTargetLookingDirection(LookingDirectionParameters.LookingDirectionMovementSource lookingDirectionMode)
{
if (lookingDirectionMode == LookingDirectionParameters.LookingDirectionMovementSource.Input)
{
if (CharacterStateController.InputMovementReference != Vector3.zero)
targetLookingDirection = CharacterStateController.InputMovementReference;
else
targetLookingDirection = CharacterActor.Forward;
}
else
{
if (CharacterActor.PlanarVelocity != Vector3.zero)
targetLookingDirection = Vector3.ProjectOnPlane(CharacterActor.PlanarVelocity, CharacterActor.Up);
else
targetLookingDirection = CharacterActor.Forward;
}
}
public override void UpdateBehaviour(float dt)
{
HandleSize(dt);
HandleVelocity(dt);
HandleRotation(dt);
}
public override void PreCharacterSimulation(float dt)
{
// Pre/PostCharacterSimulation methods are useful to update all the Animator parameters.
// Why? Because the CharacterActor component will end up modifying the velocity of the actor.
if (!CharacterActor.IsAnimatorValid())
return;
CharacterStateController.Animator.SetBool(groundedParameter, CharacterActor.IsGrounded);
CharacterStateController.Animator.SetBool(stableParameter, CharacterActor.IsStable);
CharacterStateController.Animator.SetFloat(horizontalAxisParameter, CharacterActions.movement.value.x);
CharacterStateController.Animator.SetFloat(verticalAxisParameter, CharacterActions.movement.value.y);
CharacterStateController.Animator.SetFloat(heightParameter, CharacterActor.BodySize.y);
}
public override void PostCharacterSimulation(float dt)
{
// Pre/PostCharacterSimulation methods are useful to update all the Animator parameters.
// Why? Because the CharacterActor component will end up modifying the velocity of the actor.
if (!CharacterActor.IsAnimatorValid())
return;
// Parameters associated with velocity are sent after the simulation.
// The PostSimulationUpdate (CharacterActor) might update velocity once more (e.g. if a "bad step" has been detected).
CharacterStateController.Animator.SetFloat(verticalSpeedParameter, CharacterActor.LocalVelocity.y);
CharacterStateController.Animator.SetFloat(planarSpeedParameter, CharacterActor.PlanarVelocity.magnitude);
}
protected virtual void HandleSize(float dt)
{
// Get the crouch input state
if (crouchParameters.enableCrouch)
{
if (crouchParameters.inputMode == InputMode.Toggle)
{
if (CharacterActions.crouch.Started)
wantToCrouch = !wantToCrouch;
}
else
{
wantToCrouch = CharacterActions.crouch.value;
}
if (!crouchParameters.notGroundedCrouch && !CharacterActor.IsGrounded)
wantToCrouch = false;
if (CharacterActor.IsGrounded && wantToRun)
wantToCrouch = false;
}
else
{
wantToCrouch = false;
}
if (wantToCrouch)
Crouch(dt);
else
StandUp(dt);
}
void Crouch(float dt)
{
CharacterActor.SizeReferenceType sizeReferenceType = CharacterActor.IsGrounded ?
CharacterActor.SizeReferenceType.Bottom : crouchParameters.notGroundedReference;
bool validSize = CharacterActor.CheckAndInterpolateHeight(
CharacterActor.DefaultBodySize.y * crouchParameters.heightRatio,
crouchParameters.sizeLerpSpeed * dt, sizeReferenceType);
if (validSize)
isCrouched = true;
}
void StandUp(float dt)
{
CharacterActor.SizeReferenceType sizeReferenceType = CharacterActor.IsGrounded ?
CharacterActor.SizeReferenceType.Bottom : crouchParameters.notGroundedReference;
bool validSize = CharacterActor.CheckAndInterpolateHeight(
CharacterActor.DefaultBodySize.y,
crouchParameters.sizeLerpSpeed * dt, sizeReferenceType);
if (validSize)
isCrouched = false;
}
protected virtual void HandleVelocity(float dt)
{
ProcessVerticalMovement(dt);
ProcessPlanarMovement(dt);
}
}
}

View File

@@ -0,0 +1,280 @@
using UnityEngine;
using Lightbug.Utilities;
using Lightbug.CharacterControllerPro.Core;
namespace Lightbug.CharacterControllerPro.Demo
{
[System.Serializable]
public class PlanarMovementParameters
{
[Min(0f)]
public float baseSpeedLimit = 6f;
[Header("Run (boost)")]
public bool canRun = true;
[Tooltip("\"Toggle\" will activate/deactivate the action when the input is \"pressed\". On the other hand, \"Hold\" will activate the action when the input is pressed, " +
"and deactivate it when the input is \"released\".")]
public InputMode runInputMode = InputMode.Hold;
[Min(0f)]
public float boostSpeedLimit = 10f;
[Header("Stable grounded parameters")]
public float stableGroundedAcceleration = 50f;
public float stableGroundedDeceleration = 40f;
public AnimationCurve stableGroundedAngleAccelerationBoost = AnimationCurve.EaseInOut(0f, 1f, 180f, 2f);
[Header("Unstable grounded parameters")]
public float unstableGroundedAcceleration = 10f;
public float unstableGroundedDeceleration = 2f;
public AnimationCurve unstableGroundedAngleAccelerationBoost = AnimationCurve.EaseInOut(0f, 1f, 180f, 1f);
[Header("Not grounded parameters")]
public float notGroundedAcceleration = 20f;
public float notGroundedDeceleration = 5f;
public AnimationCurve notGroundedAngleAccelerationBoost = AnimationCurve.EaseInOut(0f, 1f, 180f, 1f);
[System.Serializable]
public struct PlanarMovementProperties
{
[Tooltip("How fast the character increses its current velocity.")]
public float acceleration;
[Tooltip("How fast the character reduces its current velocity.")]
public float deceleration;
[Tooltip("How fast the character reduces its current velocity.")]
public float angleAccelerationMultiplier;
public PlanarMovementProperties(float acceleration, float deceleration, float angleAccelerationBoost)
{
this.acceleration = acceleration;
this.deceleration = deceleration;
this.angleAccelerationMultiplier = angleAccelerationBoost;
}
}
}
[System.Serializable]
public class VerticalMovementParameters
{
public enum UnstableJumpMode
{
Vertical,
GroundNormal
}
[Header("Gravity")]
[Tooltip("It enables/disables gravity. The gravity value is calculated based on the jump apex height and duration.")]
public bool useGravity = true;
[Header("Jump")]
public bool canJump = true;
[Space(10f)]
[Tooltip("The gravity magnitude and the jump speed will be automatically calculated based on the jump apex height and duration. Set this to false if you want to manually " +
"set those values.")]
public bool autoCalculate = true;
[Condition("autoCalculate", ConditionAttribute.ConditionType.IsTrue, ConditionAttribute.VisibilityType.NotEditable)]
[Tooltip("The height reached at the apex of the jump. The maximum height will depend on the \"jumpCancellationMode\".")]
[Min(0f)]
public float jumpApexHeight = 2.25f;
[Condition("autoCalculate", ConditionAttribute.ConditionType.IsTrue, ConditionAttribute.VisibilityType.NotEditable)]
[Tooltip("The amount of time to reach the \"base height\" (apex).")]
[Min(0f)]
public float jumpApexDuration = 0.5f;
[Condition("autoCalculate", ConditionAttribute.ConditionType.IsFalse, ConditionAttribute.VisibilityType.NotEditable)]
public float jumpSpeed = 10f;
[Condition("autoCalculate", ConditionAttribute.ConditionType.IsFalse, ConditionAttribute.VisibilityType.NotEditable)]
public float gravity = 10f;
[Space(10f)]
[Tooltip("Reduces the vertical velocity when the jump action is canceled.")]
public bool cancelJumpOnRelease = true;
[Tooltip("How much the vertical velocity is reduced when canceling the jump (0 = no effect , 1 = zero velocity).")]
[Range(0f, 1f)]
public float cancelJumpMultiplier = 0.5f;
[Tooltip("When canceling the jump (releasing the action), if the jump time is less than this value nothing is going to happen. Only when the timer is greater than this \"min time\" the jump will be affected.")]
public float cancelJumpMinTime = 0.1f;
[Tooltip("When canceling the jump (releasing the action), if the jump time is less than this value (and greater than the \"min time\") the velocity will be affected.")]
public float cancelJumpMaxTime = 0.3f;
[Space(10f)]
[Tooltip("This will help to perform the jump action after the actual input has been started. This value determines the maximum time between input and ground detection.")]
[Min(0f)]
public float preGroundedJumpTime = 0.2f;
[Tooltip("If the character is not grounded, and the \"not grounded time\" is less or equal than this value, the jump action will be performed as a grounded jump. This is also known as \"coyote time\".")]
[Min(0f)]
public float postGroundedJumpTime = 0.1f;
[Space(10f)]
[Min(0)]
[Tooltip("Number of jumps available for the character in the air.")]
public int availableNotGroundedJumps = 1;
[Space(10f)]
public bool canJumpOnUnstableGround = false;
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// public float GravityMagnitude { get; private set; } = 9.8f;
public void UpdateParameters()
{
if (autoCalculate)
{
gravity = (2 * jumpApexHeight) / Mathf.Pow(jumpApexDuration, 2);
jumpSpeed = gravity * jumpApexDuration;
}
}
public void OnValidate()
{
if (autoCalculate)
{
gravity = (2 * jumpApexHeight) / Mathf.Pow(jumpApexDuration, 2);
jumpSpeed = gravity * jumpApexDuration;
}
else
{
jumpApexDuration = jumpSpeed / gravity;
jumpApexHeight = gravity * Mathf.Pow(jumpApexDuration, 2) / 2f;
}
}
[Header("Jump Down (One Way Platforms)")]
public bool canJumpDown = true;
[Space(10f)]
public bool filterByTag = false;
public string jumpDownTag = "JumpDown";
[Space(10f)]
[Min(0f)]
public float jumpDownDistance = 0.05f;
[Min(0f)]
public float jumpDownVerticalVelocity = 0.5f;
}
[System.Serializable]
public class CrouchParameters
{
public bool enableCrouch = true;
public bool notGroundedCrouch = false;
[Tooltip("This multiplier represents the crouch ratio relative to the default height.")]
[Condition("enableCrouch", ConditionAttribute.ConditionType.IsTrue, ConditionAttribute.VisibilityType.NotEditable)]
[Min(0f)]
public float heightRatio = 0.75f;
[Tooltip("How much the crouch action affects the movement speed?.")]
[Condition("enableCrouch", ConditionAttribute.ConditionType.IsTrue, ConditionAttribute.VisibilityType.NotEditable)]
[Min(0f)]
public float speedMultiplier = 0.3f;
[Tooltip("\"Toggle\" will activate/deactivate the action when the input is \"pressed\". On the other hand, \"Hold\" will activate the action when the input is pressed, " +
"and deactivate it when the input is \"released\".")]
[Condition("enableCrouch", ConditionAttribute.ConditionType.IsTrue, ConditionAttribute.VisibilityType.NotEditable)]
public InputMode inputMode = InputMode.Hold;
[Tooltip("This field determines an anchor point in space (top, center or bottom) that can be used as a reference during size changes. " +
"For instance, by using \"top\" as a reference, the character will shrink/grow my moving only the bottom part of the body.")]
public CharacterActor.SizeReferenceType notGroundedReference = CharacterActor.SizeReferenceType.Top;
[Min(0f)]
public float sizeLerpSpeed = 8f;
}
[System.Serializable]
public class LookingDirectionParameters
{
public bool changeLookingDirection = true;
[Header("Lerp properties")]
public float speed = 10f;
[Header("Target Direction")]
public LookingDirectionMode lookingDirectionMode = LookingDirectionMode.Movement;
[Condition("lookingDirectionMode", ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.VisibilityType.Hidden, (int)LookingDirectionMode.Target)]
[Space(5f)]
public Transform target = null;
[Space(5f)]
[Condition("lookingDirectionMode", ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.VisibilityType.Hidden, (int)LookingDirectionMode.Movement)]
public LookingDirectionMovementSource stableGroundedLookingDirectionMode = LookingDirectionMovementSource.Input;
[Condition("lookingDirectionMode", ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.VisibilityType.Hidden, (int)LookingDirectionMode.Movement)]
public LookingDirectionMovementSource unstableGroundedLookingDirectionMode = LookingDirectionMovementSource.Velocity;
[Condition("lookingDirectionMode", ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.VisibilityType.Hidden, (int)LookingDirectionMode.Movement)]
public LookingDirectionMovementSource notGroundedLookingDirectionMode = LookingDirectionMovementSource.Input;
public enum LookingDirectionMode
{
Movement,
Target,
ExternalReference
}
public enum LookingDirectionMovementSource
{
Velocity,
Input
}
}
public enum InputMode
{
Toggle,
Hold
}
}

View File

@@ -0,0 +1,189 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
public class RopeClimbing : CharacterState
{
[Header("Movement")]
[SerializeField]
protected float climbSpeed = 3f;
[SerializeField]
protected float angularSpeed = 120f;
[SerializeField]
protected float jumpVelocity = 10f;
[SerializeField]
protected float verticalAcceleration = 10f;
[SerializeField]
protected float angularAcceleration = 10f;
[Header("Offset")]
[SerializeField]
protected float forwardOffset = -0.25f;
[Header("Animation")]
[SerializeField]
protected string verticalVelocityParameter = "VerticalVelocity";
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
protected Rope currentRope = null;
protected Dictionary<Transform, Rope> ropes = new Dictionary<Transform, Rope>();
protected Vector3 verticalVelocity;
protected Vector3 angularVelocity;
Vector3 ReferencePosition => CharacterActor.Top;
Vector3 ClosestVectorToRope
{
get
{
Vector3 characterToTop = currentRope.TopPosition - CharacterActor.Position;
return Vector3.ProjectOnPlane(characterToTop, currentRope.BottomToTop);
}
}
protected override void Awake()
{
base.Awake();
#if UNITY_2023_1_OR_NEWER
Rope[] ropesArray = FindObjectsByType<Rope>(FindObjectsSortMode.None);
#else
Rope[] ropesArray = FindObjectsOfType<Rope>();
#endif
for (int i = 0; i < ropesArray.Length; i++)
ropes.Add(ropesArray[i].transform, ropesArray[i]);
}
public override void CheckExitTransition()
{
if (!currentRope.IsInRange(ReferencePosition) || CharacterActions.jump.Started)
CharacterStateController.EnqueueTransition<NormalMovement>();
}
public override bool CheckEnterTransition(CharacterState fromState)
{
for (int i = 0; i < CharacterActor.Triggers.Count; i++)
{
Trigger trigger = CharacterActor.Triggers[i];
if (!trigger.firstContact)
continue;
Rope rope = ropes.GetOrRegisterValue(trigger.transform);
if (rope != null)
{
if (!rope.IsInRange(ReferencePosition))
return false;
currentRope = rope;
return true;
}
}
return false;
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
CharacterActor.IsKinematic = false;
CharacterActor.alwaysNotGrounded = true;
CharacterActor.UseRootMotion = false;
CharacterActor.Velocity = Vector3.zero;
Vector3 closestVectorToRope = ClosestVectorToRope;
CharacterActor.SetYaw(closestVectorToRope);
CharacterActor.Position = CharacterActor.Position + closestVectorToRope + CharacterActor.Forward * forwardOffset;
}
public override void ExitBehaviour(float dt, CharacterState toState)
{
CharacterActor.alwaysNotGrounded = false;
currentRope = null;
if (CharacterActions.jump.Started)
{
if (CharacterActions.movement.Detected)
CharacterActor.Velocity = CharacterStateController.InputMovementReference * jumpVelocity;
else
CharacterActor.Velocity = CharacterStateController.MovementReferenceForward * jumpVelocity;
CharacterActor.SetYaw(Vector3.Normalize(CharacterActor.Velocity));
}
else
{
CharacterActor.Velocity = Vector3.zero;
}
}
public override void UpdateBehaviour(float dt)
{
Vector3 characterPosition = CharacterActor.Position;
float targetAngularSpeed = CharacterActions.movement.value.x * angularSpeed;
characterPosition = CustomUtilities.RotatePointAround(
characterPosition,
characterPosition + ClosestVectorToRope,
targetAngularSpeed * dt,
Vector3.Normalize(currentRope.BottomToTop)
);
Vector3 targetAngularVelocity = (characterPosition - CharacterActor.Position) / dt;
angularVelocity = Vector3.MoveTowards(angularVelocity, targetAngularVelocity, angularAcceleration * dt);
Vector3 targetVerticalVelocity = CharacterActions.movement.value.y * climbSpeed * CharacterActor.Up;
verticalVelocity = Vector3.MoveTowards(verticalVelocity, targetVerticalVelocity, verticalAcceleration * dt);
CharacterActor.Velocity = verticalVelocity + angularVelocity;
}
public override void PostUpdateBehaviour(float dt)
{
// Always look towards the rope.
CharacterActor.SetYaw(ClosestVectorToRope);
}
public override void PostCharacterSimulation(float dt)
{
if (!CharacterActor.IsAnimatorValid())
return;
CharacterActor.Animator.SetFloat(verticalVelocityParameter, CharacterActor.LocalVelocity.y);
}
}
}

View File

@@ -0,0 +1,233 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Character/States/Wall Slide")]
public class WallSlide : CharacterState
{
[Header("Filter")]
[SerializeField]
protected bool filterByTag = true;
[Condition("filterByTag", ConditionAttribute.ConditionType.IsTrue)]
[SerializeField]
protected string wallTag = "WallSlide";
[Header("Slide")]
[SerializeField]
protected float slideAcceleration = 10f;
[Range(0f, 1f)]
[SerializeField]
protected float initialIntertia = 0.4f;
[Header("Grab")]
public bool enableGrab = true;
public bool enableClimb = true;
[Condition("enableClimb", ConditionAttribute.ConditionType.IsTrue)]
public float wallClimbHorizontalSpeed = 1f;
[Condition("enableClimb", ConditionAttribute.ConditionType.IsTrue)]
public float wallClimbVerticalSpeed = 3f;
[Condition("enableClimb", ConditionAttribute.ConditionType.IsTrue)]
public float wallClimbAcceleration = 10f;
[Header("Size")]
[SerializeField]
protected bool modifySize = true;
[Condition("modifySize", ConditionAttribute.ConditionType.IsTrue)]
[SerializeField]
protected float height = 1.5f;
[Header("Jump")]
[SerializeField]
protected float jumpNormalVelocity = 5f;
[SerializeField]
protected float jumpVerticalVelocity = 10f;
[Header("Animation")]
[SerializeField]
protected string horizontalVelocityParameter = "HorizontalVelocity";
[SerializeField]
protected string verticalVelocityParameter = "VerticalVelocity";
[SerializeField]
protected string grabParameter = "Grab";
[SerializeField]
protected string movementDetectedParameter = "MovementDetected";
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
protected bool wallJump = false;
protected Vector2 initialSize = Vector2.zero;
public override void CheckExitTransition()
{
if (CharacterActions.crouch.value || CharacterActor.IsGrounded || !CharacterActor.WallCollision || !CheckCenterRay())
{
CharacterStateController.EnqueueTransition<NormalMovement>();
}
else if (CharacterActions.jump.Started)
{
wallJump = true;
CharacterStateController.EnqueueTransition<NormalMovement>();
}
else
{
CharacterStateController.EnqueueTransition<LedgeHanging>();
}
}
public override bool CheckEnterTransition(CharacterState fromState)
{
if (CharacterActor.IsAscending)
return false;
if (!CharacterActor.WallCollision)
return false;
if (filterByTag)
if (!CharacterActor.WallContact.gameObject.CompareTag(wallTag))
return false;
if (!CheckCenterRay())
return false;
return true;
}
protected virtual bool CheckCenterRay()
{
HitInfoFilter filter = new HitInfoFilter(
CharacterActor.PhysicsComponent.CollisionLayerMask,
true,
true
);
CharacterActor.PhysicsComponent.Raycast(
out HitInfo centerRayHitInfo,
CharacterActor.Center,
-CharacterActor.WallContact.normal * 1.2f * CharacterActor.BodySize.x,
in filter
);
return centerRayHitInfo.hit && centerRayHitInfo.transform.gameObject == CharacterActor.WallContact.gameObject;
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
CharacterActor.UseRootMotion = false;
CharacterActor.Velocity *= initialIntertia;
CharacterActor.SetYaw(-CharacterActor.WallContact.normal);
if (modifySize)
{
initialSize = CharacterActor.BodySize;
CharacterActor.SetSize(new Vector2(initialSize.x, height), CharacterActor.SizeReferenceType.Center);
}
}
public override void ExitBehaviour(float dt, CharacterState toState)
{
if (wallJump)
{
wallJump = false;
// Do a 180 degrees turn.
CharacterActor.TurnAround();
// Apply the wall jump velocity.
CharacterActor.Velocity = jumpVerticalVelocity * CharacterActor.Up + jumpNormalVelocity * CharacterActor.WallContact.normal;
}
if (modifySize)
{
CharacterActor.SizeReferenceType sizeReferenceType = CharacterActor.IsGrounded ?
CharacterActor.SizeReferenceType.Bottom : CharacterActor.SizeReferenceType.Top;
CharacterActor.SetSize(initialSize, sizeReferenceType);
}
}
protected bool IsGrabbing => CharacterActions.run.value && enableGrab;
public override void UpdateBehaviour(float dt)
{
if (IsGrabbing)
{
Vector3 rightDirection = Vector3.ProjectOnPlane(CharacterStateController.MovementReferenceRight, CharacterActor.WallContact.normal);
rightDirection.Normalize();
Vector3 upDirection = CharacterActor.Up;
Vector3 targetVelocity = enableClimb ? CharacterActions.movement.value.x * rightDirection * wallClimbHorizontalSpeed +
CharacterActions.movement.value.y * upDirection * wallClimbVerticalSpeed : Vector3.zero;
CharacterActor.Velocity = Vector3.MoveTowards(CharacterActor.Velocity, targetVelocity, wallClimbAcceleration * dt);
}
else
{
CharacterActor.VerticalVelocity += -CharacterActor.Up * slideAcceleration * dt;
}
}
public override void PostUpdateBehaviour(float dt)
{
if (!CharacterActor.IsAnimatorValid())
return;
CharacterActor.Animator.SetFloat(horizontalVelocityParameter, CharacterActor.LocalVelocity.x);
CharacterActor.Animator.SetFloat(verticalVelocityParameter, CharacterActor.LocalVelocity.y);
CharacterActor.Animator.SetBool(grabParameter, IsGrabbing);
CharacterActor.Animator.SetBool(movementDetectedParameter, CharacterActions.movement.Detected);
}
public override void UpdateIK(int layerIndex)
{
if (!CharacterActor.IsAnimatorValid())
return;
if (IsGrabbing && CharacterActions.movement.Detected)
{
CharacterActor.Animator.SetLookAtWeight(Mathf.Clamp01(CharacterActor.Velocity.magnitude), 0f, 0.2f);
CharacterActor.Animator.SetLookAtPosition(CharacterActor.Position + CharacterActor.Velocity);
}
else
{
CharacterActor.Animator.SetLookAtWeight(0f);
}
}
}
}

View File

@@ -0,0 +1,87 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Implementation;
namespace Lightbug.CharacterControllerPro.Demo
{
public class ZeroGravity : CharacterState
{
[Header("Movement")]
public float baseSpeed = 10f;
public float acceleration = 20f;
public float deceleration = 20f;
[Header("Pitch")]
public bool invertPitch = false;
public float pitchAngularSpeed = 180f;
[Min(0f)]
public float pitchLerpAcceleration = 5f;
[Header("Roll")]
public bool invertRoll = false;
public float rollAngularSpeed = 180f;
[Min(0f)]
public float rollLerpAcceleration = 5f;
float pitchModifier = 1f;
float rollModifier = 1f;
Vector3 targetVerticalVelocity;
float pitchValue;
float rollValue;
protected override void Awake()
{
base.Awake();
pitchModifier = -(invertPitch ? 1f : -1f);
rollModifier = invertRoll ? 1f : -1f;
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
CharacterActor.alwaysNotGrounded = true;
CharacterActor.UseRootMotion = false;
CharacterActor.constraintRotation = false;
targetVerticalVelocity = CharacterActor.VerticalVelocity;
}
public override void UpdateBehaviour(float dt)
{
ProcessRotation(dt);
ProcessVelocity(dt);
}
private void ProcessRotation(float dt)
{
pitchValue = Mathf.Lerp(pitchValue, pitchModifier * CharacterActions.pitch.value * pitchAngularSpeed * dt, pitchLerpAcceleration * dt);
rollValue = Mathf.Lerp(rollValue, rollModifier * CharacterActions.roll.value * rollAngularSpeed * dt, rollLerpAcceleration * dt);
CharacterActor.RotatePitch(pitchValue, CharacterActor.Center);
CharacterActor.RotateRoll(rollValue, CharacterActor.Center);
Vector3 forward = Vector3.Lerp(CharacterActor.Forward, Vector3.ProjectOnPlane(CharacterStateController.ExternalReference.forward, CharacterActor.Up), 5f * dt);
CharacterActor.SetYaw(forward);
}
private void ProcessVelocity(float dt)
{
Vector3 targetVelocity = CharacterStateController.InputMovementReference * baseSpeed;
CharacterActor.Velocity = Vector3.MoveTowards(CharacterActor.Velocity, targetVelocity, (CharacterActions.movement.Detected ? acceleration : deceleration) * dt);
if (CharacterActions.jump.value)
{
targetVerticalVelocity = CharacterActor.Up * baseSpeed;
CharacterActor.VerticalVelocity = Vector3.MoveTowards(CharacterActor.VerticalVelocity, targetVerticalVelocity, acceleration * dt);
}
else if (CharacterActions.crouch.value)
{
targetVerticalVelocity = -CharacterActor.Up * baseSpeed;
CharacterActor.VerticalVelocity = Vector3.MoveTowards(CharacterActor.VerticalVelocity, targetVerticalVelocity, acceleration * dt);
}
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Lightbug.CharacterControllerPro.Demo
{
public class Teleporter : MonoBehaviour
{
public Transform target;
void OnTriggerEnter(Collider collider)
{
collider.transform.position = target.position;
if (collider.attachedRigidbody != null)
collider.attachedRigidbody.position = target.position;
}
}
}

View File

@@ -0,0 +1,99 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.Utilities;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Lightbug.CharacterControllerPro.Demo
{
public abstract class VerticalDirectionModifier : MonoBehaviour
{
#if UNITY_EDITOR
[HelpBox("The trigger will only start the transition. The character will be teleported using the reference transform information (position and rotation).", HelpBoxMessageType.Warning)]
#endif
[SerializeField]
protected CharacterReferenceObject reference = new CharacterReferenceObject();
[Tooltip("This will change the up direction constraint settings from the CharacterActor component bsaed on this object")]
[SerializeField]
bool modifyUpDirection = true;
[Tooltip("The duration this modifier will be inactive once it is activated. " +
"Use this to prevent the character from re-activating the effect over and over again (the default value of 1 second should be enough.)")]
[SerializeField]
float waitTime = 1f;
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
protected bool isReady = true;
float time = 0f;
protected Dictionary<Transform, CharacterActor> characters = new Dictionary<Transform, CharacterActor>();
void Update()
{
if (isReady)
return;
time += Time.deltaTime;
if (time >= waitTime)
{
time = 0f;
isReady = true;
}
}
protected void HandleUpDirection(CharacterActor character)
{
if (reference == null)
return;
if (!modifyUpDirection)
return;
if (reference.verticalAlignmentReference != null)
{
character.upDirectionReference = reference.verticalAlignmentReference;
}
else
{
character.upDirectionReference = null;
character.constraintUpDirection = reference.referenceTransform.up;
}
isReady = false;
}
protected CharacterActor GetCharacter(Transform objectTransform)
{
CharacterActor characterActor;
bool found = characters.TryGetValue(objectTransform, out characterActor);
if (!found)
{
characterActor = objectTransform.GetComponent<CharacterActor>();
if (characterActor != null)
characters.Add(objectTransform, characterActor);
}
return characterActor;
}
}
}

View File

@@ -0,0 +1,27 @@
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
namespace Lightbug.CharacterControllerPro.Demo
{
public class VerticalDirectionModifier2D : VerticalDirectionModifier
{
void OnTriggerEnter2D(Collider2D other)
{
if (!isReady)
return;
CharacterActor characterActor = GetCharacter(other.transform);
if (characterActor != null)
{
HandleUpDirection(characterActor);
characterActor.Up = reference.referenceTransform.up;
}
}
}
}

View File

@@ -0,0 +1,26 @@
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
namespace Lightbug.CharacterControllerPro.Demo
{
public class VerticalDirectionModifier3D : VerticalDirectionModifier
{
void OnTriggerEnter(Collider other)
{
if (!isReady)
return;
CharacterActor characterActor = GetCharacter(other.transform);
if (characterActor != null)
{
HandleUpDirection(characterActor);
characterActor.Teleport(reference.referenceTransform);
}
}
}
}