/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Objects
{
using Opsive.Shared.Game;
using Opsive.Shared.Utility;
using Opsive.UltimateCharacterController.Character;
using Opsive.UltimateCharacterController.Game;
using Opsive.UltimateCharacterController.StateSystem;
using Opsive.UltimateCharacterController.Traits;
using Opsive.UltimateCharacterController.Utility;
using UnityEngine;
///
/// The MovingPlatform component will move an object from one point to another. GameObjects with the Moving Platform component should be on the MovingPlatform layer.
///
[RequireComponent(typeof(Rigidbody))]
public class MovingPlatform : StateBehavior, IKinematicObject, IInteractableTarget
{
///
/// Represets a point that the platform can traverse.
///
[System.Serializable]
public struct Waypoint
{
[Tooltip("The transform the waypoint that the platform should traverse.")]
[SerializeField] private Transform m_Transform;
[Tooltip("The amount of time that the platform should stay at the current waypoint before moving to the next waypoint")]
[SerializeField] private float m_Delay;
[Tooltip("The state that should be triggered when the platform is moving towards it.")]
[SerializeField] private string m_State;
private int m_StateHash;
public Transform Transform { get { return m_Transform; } set { m_Transform = value; } }
public float Delay { get { return m_Delay; } set { m_Delay = value; } }
public string State { get { return m_State; } set { m_State = value; } }
public int StateHash { get { return m_StateHash; } }
///
/// Initializes the state hash.
///
public void Initialize()
{
m_StateHash = Serialization.StringHash(m_State);
}
}
///
/// Specifies the direction that the platform should traverse.
///
public enum PathDirection
{
Forward, // Move waypoints from least to greatest.
Backwards // Move waypoints from greatest to least.
}
///
/// Specifies how the platform traverses through waypoints.
///
public enum PathMovementType
{
PingPong, // Moves to the last waypoint and then back the way it came from.
Loop, // Moves to the last waypoint and then directly to the first waypoint.
Target // Moves to the specified waypoint index.
}
///
/// Specifies how the platform should interpolate the movement speed.
///
public enum MovementInterpolationMode
{
EaseInOut, // Gently moves into full movement and then gently moves out of it at each waypoint.
EaseIn, // Gently moves into full movement.
EaseOut, // Moves into full movement immediately and gently moves out of full movement at each waypoint.
EaseOut2, // Moves into full movement immediately and moves out of full movement according to the movement speed.
Slerp, // Uses Vector3.Slerp to move in and out of movement according to the movement speed.
Lerp // Uses Vector3.Lerp to move in and out of movement according to the movement speed.
}
///
/// Specifies how the platform should interpolate the rotation speed.
///
public enum RotateInterpolationMode
{
SyncToMovement, // Rotates according to the movement speed.
EaseOut, // Uses Quaternion.Lerp to lerp the rotation based on a linear curve.
CustomEaseOut, // Uses Quaternion.Lerp to lerp the rotation based on the RotationEaseAmount.
CustomRotate // Rotates according to the rotation speed.
}
[Tooltip("Specifies the location that the object should be updated.")]
[SerializeField] protected KinematicObjectManager.UpdateLocation m_UpdateLocation = KinematicObjectManager.UpdateLocation.FixedUpdate;
[Tooltip("The waypoints to traverse.")]
[SerializeField] protected Waypoint[] m_Waypoints;
[Tooltip("Specifies the direction that the platform should traverse.")]
[SerializeField] protected PathDirection m_Direction;
[Tooltip("Specifies how the platform traverses through waypoints.")]
[SerializeField] protected PathMovementType m_MovementType;
[Tooltip("If using the Target PathMovementType, specifies the waypoint index to move towards.")]
[SerializeField] protected int m_TargetWaypoint;
[Tooltip("The speed at which the platform should move.")]
[SerializeField] protected float m_MovementSpeed = 0.1f;
[Tooltip("Specifies how the platform should interpolate the movement speed.")]
[SerializeField] protected MovementInterpolationMode m_MovementInterpolation = MovementInterpolationMode.EaseInOut;
[Tooltip("Specifies how the platform should interpolate the rotation speed.")]
[SerializeField] protected RotateInterpolationMode m_RotationInterpolation;
[Tooltip("If using the CustomEaseOut RotationInterpolationMode, specifies the amount to ease into the target rotation.")]
[SerializeField] protected float m_RotationEaseAmount = 0.1f;
[Tooltip("If using the CustomRotate RotationInterpolationMode, specifies the rotation speed.")]
[SerializeField] protected Vector3 m_CustomRotationSpeed;
[Tooltip("The maximum angle that the platform can rotate. Set to -1 to have no max angle.")]
[SerializeField] protected float m_MaxRotationDeltaAngle = -1;
[Tooltip("The state name that should activate when the character enters the platform trigger.")]
[SerializeField] protected string m_CharacterTriggerState;
[Tooltip("Should the platform be enabled when interacted with?")]
[SerializeField] protected bool m_EnableOnInteract;
[Tooltip("Should the directions be changed if the character interacts with the platform while it is moving?")]
[SerializeField] protected bool m_ChangeDirectionsOnInteract = false;
#if UNITY_EDITOR
[Tooltip("The color to draw the editor gizmo in (editor only).")]
[SerializeField] protected Color m_GizmoColor = new Color(0, 0, 1, 0.3f);
[Tooltip("Should the delay and distance labels be drawh t0 tye scene view (editor only)?")]
[SerializeField] protected bool m_DrawDebugLabels;
#endif
public KinematicObjectManager.UpdateLocation UpdateLocation { get { return m_UpdateLocation; } }
[NonSerialized] public Waypoint[] Waypoints { get { return m_Waypoints; } set { m_Waypoints = value; } }
[NonSerialized] public PathDirection Direction { get { return m_Direction; } set { m_Direction = value; } }
public PathMovementType MovementType { get { return m_MovementType; } set { m_MovementType = value; } }
public int TargetWaypoint { get { return m_TargetWaypoint; } set { m_TargetWaypoint = value; } }
public float MovementSpeed { get { return m_MovementSpeed; } set { m_MovementSpeed = value; } }
public MovementInterpolationMode MovementInterpolation { get { return m_MovementInterpolation; } set { m_MovementInterpolation = value; } }
public RotateInterpolationMode RotationInterpolation { get { return m_RotationInterpolation; } set { m_RotationInterpolation = value; } }
public float RotationEaseAmount { get { return m_RotationEaseAmount; } set { m_RotationEaseAmount = value; } }
public Vector3 CustomRotationSpeed { get { return m_CustomRotationSpeed; } set { m_CustomRotationSpeed = value; } }
public string CharacterTriggerState { get { return m_CharacterTriggerState; } set { m_CharacterTriggerState = value; } }
public bool EnableOnInteract { get { return m_EnableOnInteract; } set { m_EnableOnInteract = value; } }
public bool ChangeDirectionsOnInteract { get { return m_ChangeDirectionsOnInteract; } set { m_ChangeDirectionsOnInteract = value; } }
#if UNITY_EDITOR
[NonSerialized] public Color GizmoColor { get { return m_GizmoColor; } set { m_GizmoColor = value; } }
[NonSerialized] public bool DrawDebugLabels { get { return m_DrawDebugLabels; } set { m_DrawDebugLabels = value; } }
#endif
protected GameObject m_GameObject;
protected Transform m_Transform;
private Rigidbody m_Rigidbody;
private int m_KinematicObjectIndex = -1;
protected int m_NextWaypoint;
protected int m_PreviousWaypoint;
protected float m_NextWaypointDistance;
protected Quaternion m_OriginalRotation;
protected float m_MoveTime;
protected Vector3 m_TargetPosition;
protected Quaternion m_TargetRotation;
private Vector3 m_MovePosition;
private Quaternion m_MoveRotation;
private AnimationCurve m_EaseInOutCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
private AnimationCurve m_LinearCurve = AnimationCurve.Linear(0, 0, 1, 1);
protected ScheduledEventBase m_NextWaypointEvent;
protected int m_ActiveCharacterCount;
public int NextWaypoint { get { return m_NextWaypoint; } }
public int KinematicObjectIndex { get { return m_KinematicObjectIndex; } set { m_KinematicObjectIndex = value; } }
///
/// Cache the component references and initialize the default values.
///
protected override void Awake()
{
base.Awake();
#if UNITY_EDITOR
// Sanity check in the editor:
for (int i = 0; i < m_Waypoints.Length; ++i) {
if (m_Waypoints[i].Transform == null) {
Debug.LogError("Error: Moving Platform " + gameObject.name + " has a null waypoint. This platform will be disabled.");
enabled = false;
return;
}
}
#endif
m_GameObject = gameObject;
m_Transform = transform;
m_MovePosition = m_Transform.position;
m_MoveRotation = m_Transform.rotation;
// The Rigidbody is only used to notify Unity that the character isn't static and for collision events. The Rigidbody doesn't control any movement.
m_Rigidbody = GetComponent();
m_Rigidbody.collisionDetectionMode = CollisionDetectionMode.Discrete;
m_Rigidbody.isKinematic = true;
m_Rigidbody.constraints = RigidbodyConstraints.FreezeAll;
// The GameObject must be on the MovingPlatform layer.
if (m_GameObject.layer != LayerManager.MovingPlatform) {
Debug.LogWarning("Warning: " + m_GameObject.name + " is a moving platform not using the MovingPlatform layer. Please change this layer.");
m_GameObject.layer = LayerManager.MovingPlatform;
}
// The platform can rotate without any waypoints.
if (m_Waypoints.Length > 0) {
for (int i = 0; i < m_Waypoints.Length; ++i) {
m_Waypoints[i].Initialize();
}
m_TargetRotation = m_OriginalRotation = m_Waypoints[m_NextWaypoint].Transform.rotation;
m_TargetPosition = m_Waypoints[m_NextWaypoint].Transform.position;
if (!string.IsNullOrEmpty(m_Waypoints[m_NextWaypoint].State)) {
StateManager.SetState(m_GameObject, m_Waypoints[m_NextWaypoint].State, true);
}
m_PreviousWaypoint = m_NextWaypoint;
}
// Start disabled until the platform is interacted with.
if (m_EnableOnInteract) {
enabled = false;
}
}
///
/// Registers the object with the KinematicObjectManager.
///
protected virtual void OnEnable()
{
m_KinematicObjectIndex = KinematicObjectManager.RegisterKinematicObject(this);
}
///
/// Update the platform movement and rotation.
///
public void Move()
{
// Updates the path to the next waypoint if necessary.
UpdatePath();
// Rotate along the path.
UpdateRotation();
// Applies the rotation.
ApplyRotation();
// No more updates are necessary if the platform is waiting at the current waypoint.
if (m_NextWaypointEvent != null || m_Waypoints.Length == 0) {
return;
}
// Progress towards the waypoint.
UpdatePosition();
// Applies the position.
ApplyPosition();
}
///
/// If the platform has arrived at the current waypoint then the next waypoint should be determined.
///
private void UpdatePath()
{
if (GetRemainingDistance() < 0.01f && m_NextWaypointEvent == null && (m_MovementType != PathMovementType.Target || m_NextWaypoint != m_TargetWaypoint)) {
m_NextWaypointEvent = Scheduler.ScheduleFixed(m_Waypoints[m_NextWaypoint].Delay, UpdateWaypoint);
}
}
///
/// Updates the moving platform to move to the next waypoint.
///
protected void UpdateWaypoint()
{
// The state should always reflect the state of the next waypoint. If moving in reverse then the state has to be updated before the index changes.
if (m_Direction == PathDirection.Backwards) {
UpdateState();
}
m_PreviousWaypoint = m_NextWaypoint;
switch (m_MovementType) {
case PathMovementType.Target:
if (m_NextWaypoint != m_TargetWaypoint) {
GoToNextWaypoint();
}
break;
case PathMovementType.Loop:
GoToNextWaypoint();
break;
case PathMovementType.PingPong:
if (m_Direction == PathDirection.Backwards) {
if (m_NextWaypoint == 0) {
m_Direction = PathDirection.Forward;
}
} else {
if (m_NextWaypoint == (m_Waypoints.Length - 1)) {
m_Direction = PathDirection.Backwards;
}
}
GoToNextWaypoint();
break;
}
// The state should always reflect the state of the next waypoint. If moving in reverse then the state has to be updated before the index changes.
if (m_Direction == PathDirection.Forward) {
UpdateState();
}
m_NextWaypointEvent = null;
}
///
/// Disables the state at the old index and enables the state at the new index.
///
private void UpdateState()
{
if (m_Waypoints[m_PreviousWaypoint].StateHash != m_Waypoints[m_NextWaypoint].StateHash) {
// The previous state should be disabled.
if (m_Waypoints[m_PreviousWaypoint].StateHash != 0) {
StateManager.SetState(m_GameObject, m_Waypoints[m_PreviousWaypoint].State, false);
}
if (m_Waypoints[m_NextWaypoint].StateHash != 0) {
StateManager.SetState(m_GameObject, m_Waypoints[m_NextWaypoint].State, true);
}
}
}
///
/// Returns the distance to the next waypoint.
///
/// The distance to the next waypoint.
protected float GetRemainingDistance()
{
if (m_Waypoints.Length == 0) {
return float.MaxValue;
}
return Vector3.Distance(m_Transform.position, m_Waypoints[m_NextWaypoint].Transform.position);
}
///
/// Determines the next waypoint.
///
private void GoToNextWaypoint()
{
// The next waypoint is based on the path direction.
switch (m_Direction) {
case PathDirection.Forward:
m_NextWaypoint = GetNextWaypoint(true);
break;
case PathDirection.Backwards:
m_NextWaypoint = GetNextWaypoint(false);
break;
}
// Update the path related variables.
m_MoveTime = 0;
m_OriginalRotation = m_TargetRotation;
m_TargetPosition = m_Waypoints[m_NextWaypoint].Transform.position;
m_TargetRotation = m_Waypoints[m_NextWaypoint].Transform.rotation;
m_NextWaypointDistance = GetRemainingDistance();
}
///
/// Returns the next waypoint index.
///
/// Should the waypoint index be inceased? If false it'll be decreased.
/// The next waypoint index.
private int GetNextWaypoint(bool increase)
{
m_NextWaypoint = (m_NextWaypoint + (increase ? 1 : -1)) % m_Waypoints.Length;
if (m_NextWaypoint < 0) {
m_NextWaypoint = 0;
}
return m_NextWaypoint;
}
///
/// Updates platform angle according to the current rotation interpolation mode.
///
private void UpdateRotation()
{
switch (m_RotationInterpolation) {
case RotateInterpolationMode.SyncToMovement:
if (m_NextWaypointEvent == null) {
m_MoveRotation = Quaternion.Lerp(m_OriginalRotation, m_TargetRotation, 1.0f - (GetRemainingDistance() / m_NextWaypointDistance));
}
break;
case RotateInterpolationMode.EaseOut:
m_MoveRotation = Quaternion.Lerp(m_Transform.rotation, m_TargetRotation, m_LinearCurve.Evaluate(m_MoveTime));
break;
case RotateInterpolationMode.CustomEaseOut:
m_MoveRotation = Quaternion.Lerp(m_Transform.rotation, m_TargetRotation, m_RotationEaseAmount);
break;
case RotateInterpolationMode.CustomRotate:
m_MoveRotation = m_Transform.rotation * Quaternion.Euler(m_CustomRotationSpeed);
break;
}
if (m_MaxRotationDeltaAngle != -1) {
m_MoveRotation = Quaternion.RotateTowards(m_Transform.rotation, m_MoveRotation, m_MaxRotationDeltaAngle);
}
}
///
/// Applies the rotational movement to the Transform.
///
private void ApplyRotation()
{
m_Transform.rotation = m_MoveRotation;
}
///
/// Updates platform position according to the current movement interpolation mode.
///
private void UpdatePosition()
{
switch (m_MovementInterpolation)
{
case MovementInterpolationMode.EaseInOut:
m_MovePosition = Vector3.Lerp(m_Transform.position, m_TargetPosition, m_EaseInOutCurve.Evaluate(m_MoveTime));
break;
case MovementInterpolationMode.EaseIn:
m_MovePosition = Vector3.MoveTowards(m_Transform.position, m_TargetPosition, m_MoveTime);
break;
case MovementInterpolationMode.EaseOut:
m_MovePosition = Vector3.Lerp(m_Transform.position, m_TargetPosition, m_LinearCurve.Evaluate(m_MoveTime));
break;
case MovementInterpolationMode.EaseOut2:
m_MovePosition = Vector3.Lerp(m_Transform.position, m_TargetPosition, m_MovementSpeed * 0.25f);
break;
case MovementInterpolationMode.Slerp:
m_MovePosition = Vector3.Slerp(m_Transform.position, m_TargetPosition, m_LinearCurve.Evaluate(m_MoveTime));
break;
case MovementInterpolationMode.Lerp:
m_MovePosition = Vector3.MoveTowards(m_Transform.position, m_TargetPosition, m_MovementSpeed);
break;
}
}
///
/// Applies the positional movement to the Transform.
///
private void ApplyPosition()
{
m_Transform.position = m_MovePosition;
// Progress the move time and also store the updated metrics.
m_MoveTime += m_MovementSpeed * 0.01f * Time.deltaTime;
}
///
/// Can the target be interacted with?
///
/// The character that wants to interactact with the target.
/// True if the target can be interacted with.
public bool CanInteract(GameObject character)
{
return true;
}
///
/// Interact with the target.
///
/// The character that wants to interactact with the target.
public void Interact(GameObject character)
{
if (m_EnableOnInteract) {
enabled = true;
} else if (m_ChangeDirectionsOnInteract) {
// If the platform is already moving and is interacted with then it should change directions.
m_Direction = m_Direction == PathDirection.Forward ? PathDirection.Backwards : PathDirection.Forward;
}
}
///
/// An object has entered the trigger.
///
/// The object that entered the trigger.
private void OnTriggerEnter(Collider other)
{
// Characters will have a CharacterLayerManager.
var layerManager = other.gameObject.GetCachedParentComponent();
if (layerManager == null) {
return;
}
if (!MathUtility.InLayerMask(other.gameObject.layer, layerManager.CharacterLayer)) {
return;
}
m_ActiveCharacterCount++;
// The platform can activate a state based on the character trigger state. Only the first character should activate the state
// if multiple characters land on the platform.
if (m_ActiveCharacterCount == 1 && !string.IsNullOrEmpty(m_CharacterTriggerState)) {
StateManager.SetState(m_GameObject, m_CharacterTriggerState, true);
}
}
///
/// An object has exited the trigger.
///
/// The collider that exited the trigger.
private void OnTriggerExit(Collider other)
{
// No further checks need to be done if a character isn't on the platform.
if (m_ActiveCharacterCount == 0) {
return;
}
// Characters will have a CharacterLayerManager.
var layerManager = other.gameObject.GetCachedParentComponent();
if (layerManager == null) {
return;
}
if (!MathUtility.InLayerMask(other.gameObject.layer, layerManager.CharacterLayer)) {
return;
}
m_ActiveCharacterCount--;
if (m_ActiveCharacterCount == 0 && !string.IsNullOrEmpty(m_CharacterTriggerState)) {
StateManager.SetState(m_GameObject, m_CharacterTriggerState, false);
}
}
///
/// Unregisters the object with the KinematicObjectManager.
///
protected virtual void OnDisable()
{
KinematicObjectManager.UnregisterKinematicObject(m_KinematicObjectIndex);
}
}
}