/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Character.Abilities
{
using Opsive.Shared.Game;
using Opsive.UltimateCharacterController.Objects;
using Opsive.UltimateCharacterController.Utility;
using UnityEngine;
///
/// Abstract class which determines if the ground object is a valid object.
///
public abstract class DetectGroundAbilityBase : Ability
{
[Tooltip("The unique ID value of the Object Identifier component. A value of -1 indicates that this ID should not be used.")]
[SerializeField] protected int m_ObjectID = -1;
[Tooltip("The layer mask of the ground object.")]
[SerializeField] protected LayerMask m_LayerMask = -1;
[Tooltip("The character is no longer over the ground if the dot product between the character's up direction and the ground normal is less than the sensitivity.")]
[Range(0, 1)] [SerializeField] protected float m_GroundNormalSensitivity = 0.5f;
[Tooltip("The maximum angle that the character can be relative to the forward direction of the object.")]
[Range(0, 180)] [SerializeField] protected float m_AngleThreshold = 180;
public int ObjectID { get { return m_ObjectID; } set { m_ObjectID = value; } }
public LayerMask LayerMask { get { return m_LayerMask; } set { m_LayerMask = value; } }
public float NormalSensitivity { get { return m_GroundNormalSensitivity; } set { m_GroundNormalSensitivity = value; } }
public float AngleThreshold { get { return m_AngleThreshold; } set { m_AngleThreshold = value; } }
protected Collider m_GroundCollider;
protected Transform m_GroundTransform;
///
/// Can the ability be started?
///
/// True if the ability can be started.
public override bool CanStartAbility()
{
// An attribute may prevent the ability from starting.
if (!base.CanStartAbility()) {
return false;
}
return IsOverValidObject();
}
///
/// Is the character over a valid ground object?
///
/// True if the character is over a valid ground object.
protected bool IsOverValidObject()
{
if (!m_CharacterLocomotion.Grounded) {
return false;
}
// The ability may require the character to be directly on top of the ground.
if (Vector3.Dot(m_CharacterLocomotion.Up, m_CharacterLocomotion.GroundRaycastHit.normal) < m_GroundNormalSensitivity) {
return false;
}
var angle = Quaternion.Angle(Quaternion.LookRotation(m_Transform.forward, m_CharacterLocomotion.Up),
Quaternion.LookRotation(m_CharacterLocomotion.GroundRaycastHit.transform.forward, m_CharacterLocomotion.Up));
var objectFaces = m_CharacterLocomotion.GroundRaycastHit.transform.gameObject.GetCachedParentComponent();
// If an object has multiple faces then the ability can start from multiple directions.
if (objectFaces != null) {
var roundedAngle = 180 / objectFaces.ForwardFaceCount;
angle = Mathf.Abs(MathUtility.ClampInnerAngle(angle - (roundedAngle * Mathf.RoundToInt(angle / roundedAngle))));
}
if (angle > m_AngleThreshold) {
return false;
}
// Determine if the ground object is a valid ground object. This check only needs to be run when the grounded object changes.
if (m_GroundCollider != m_CharacterLocomotion.GroundRaycastHit.collider) {
// The ground object can be detected by using the ObjectIdentifier component.
if (m_ObjectID != -1) {
var objIdentifiers = m_CharacterLocomotion.GroundRaycastHit.collider.gameObject.GetCachedComponents();
if (objIdentifiers == null || objIdentifiers.Length == 0) {
return false;
}
var idMatch = false;
for (int i = 0; i < objIdentifiers.Length; ++i) {
if (objIdentifiers[i].ID == m_ObjectID) {
idMatch = true;
break;
}
}
if (!idMatch) {
return false;
}
}
// The ground object can be detected by using the layer mask.
if (!MathUtility.InLayerMask(m_CharacterLocomotion.GroundRaycastHit.collider.gameObject.layer, m_LayerMask)) {
return false;
}
// The ground object is valid.
m_GroundCollider = m_CharacterLocomotion.GroundRaycastHit.collider;
m_GroundTransform = m_GroundCollider.transform;
}
return true;
}
///
/// Stops the ability if the character is no longer over a valid object.
///
public override void Update()
{
base.Update();
if (!IsOverValidObject()) {
StopAbility();
}
}
///
/// The ability has stopped running.
///
/// Was the ability force stopped?
protected override void AbilityStopped(bool force)
{
base.AbilityStopped(force);
m_GroundCollider = null;
m_GroundTransform = null;
}
}
}