254 lines
12 KiB
C#
254 lines
12 KiB
C#
/// ---------------------------------------------
|
|
/// Ultimate Character Controller
|
|
/// Copyright (c) Opsive. All Rights Reserved.
|
|
/// https://www.opsive.com
|
|
/// ---------------------------------------------
|
|
|
|
namespace Opsive.UltimateCharacterController.FirstPersonController.Character.Abilities
|
|
{
|
|
using Opsive.Shared.Events;
|
|
using Opsive.Shared.Game;
|
|
using Opsive.Shared.Utility;
|
|
using Opsive.UltimateCharacterController.Character;
|
|
using Opsive.UltimateCharacterController.Character.Abilities;
|
|
using Opsive.UltimateCharacterController.Input;
|
|
using Opsive.UltimateCharacterController.Utility;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// The Lean ability allows the character to lean the camera to the left or the right of the character. This allows the character to peak
|
|
/// without exposing their body. An optional collider can be used as a hitpoint and to detect any collisions.
|
|
/// </summary>
|
|
[DefaultStartType(AbilityStartType.Axis)]
|
|
[DefaultStopType(AbilityStopType.Axis)]
|
|
[DefaultInputName("Lean")]
|
|
public class Lean : Ability
|
|
{
|
|
private const string c_LeanEventName = "OnCharacterLean";
|
|
|
|
[Tooltip("The distance that the camera should lean.")]
|
|
[SerializeField] protected float m_Distance = 0.7f;
|
|
[Tooltip("The amount of tilt to apply with the lean (in degress).")]
|
|
[SerializeField] protected float m_Tilt = 7;
|
|
[Tooltip("A tilt multiplier applied to the items.")]
|
|
[SerializeField] protected float m_ItemTiltMultiplier = 2;
|
|
[Tooltip("An optional collider that can be used for collision detection and hit points.")]
|
|
[SerializeField] protected Collider m_Collider;
|
|
[Tooltip("Optionally modify the distance that the collider leans.")]
|
|
[Range(0, 1)] [SerializeField] protected float m_ColliderOffsetMultiplier = 0.75f;
|
|
[Tooltip("The maximum number of collisions that can be detected by the collider.")]
|
|
[SerializeField] protected int m_MaxCollisionCount = 5;
|
|
|
|
public float Distance { get { return m_Distance; } set { m_Distance = value; } }
|
|
public float Tilt { get { return m_Tilt; } set { m_Tilt = value; } }
|
|
public float ItemTiltMultiplier { get { return m_ItemTiltMultiplier; } set { m_ItemTiltMultiplier = value; } }
|
|
[NonSerialized] public Collider Collider { get { return m_Collider; } set { m_Collider = value; } }
|
|
public float ColliderOffsetMultiplier { get { return m_ColliderOffsetMultiplier; } set { m_ColliderOffsetMultiplier = value; } }
|
|
|
|
private UltimateCharacterLocomotionHandler m_Handler;
|
|
private ActiveInputEvent m_LeanInput;
|
|
|
|
private GameObject m_ColliderGameObject;
|
|
private Transform m_ColliderTransform;
|
|
private Collider[] m_OverlapColliders;
|
|
private float m_HitDistance;
|
|
|
|
private float m_AxisValue;
|
|
|
|
public override bool IsConcurrent { get { return true; } }
|
|
|
|
/// <summary>
|
|
/// Initialize the default values.
|
|
/// </summary>
|
|
public override void Awake()
|
|
{
|
|
base.Awake();
|
|
|
|
m_Handler = m_GameObject.GetCachedComponent<UltimateCharacterLocomotionHandler>();
|
|
if (m_Collider != null) {
|
|
if (!(m_Collider is CapsuleCollider) && !(m_Collider is SphereCollider)) {
|
|
Debug.LogError("Error: Only Capsule and Sphere Colliders are supported by the Lean ability.");
|
|
m_Collider = null;
|
|
return;
|
|
}
|
|
|
|
m_OverlapColliders = new Collider[m_MaxCollisionCount];
|
|
m_ColliderGameObject = m_Collider.gameObject;
|
|
m_ColliderTransform = m_Collider.transform;
|
|
m_ColliderGameObject.SetActive(false);
|
|
}
|
|
EventHandler.RegisterEvent<bool>(m_GameObject, "OnCharacterChangePerspectives", OnChangePerspectives);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The ability has started.
|
|
/// </summary>
|
|
protected override void AbilityStarted()
|
|
{
|
|
// If a handler exists then the ability is interested in updates when the axis value changes. This for example allows the lean to switch
|
|
// between the left and right lean without having to stop and start again.
|
|
if (m_Handler != null) {
|
|
m_LeanInput = GenericObjectPool.Get<ActiveInputEvent>();
|
|
m_LeanInput.Initialize(ActiveInputEvent.Type.Axis, InputNames[InputIndex], "OnLeanInputUpdate");
|
|
m_Handler.RegisterInputEvent(m_LeanInput);
|
|
}
|
|
EventHandler.RegisterEvent<float>(m_GameObject, "OnLeanInputUpdate", OnInputUpdate);
|
|
|
|
base.AbilityStarted();
|
|
|
|
// The collider should be activated when the ability starts. The collider detects when the character would be clipping with a wall
|
|
// and also allows the character to be shot at while leaning.
|
|
if (m_ColliderGameObject != null) {
|
|
m_ColliderGameObject.SetActive(true);
|
|
}
|
|
|
|
// Start leaning.
|
|
m_AxisValue = InputAxisValue;
|
|
UpdateLean(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// As the character is moving the lean should update to ensure the collider doesn't clip with any objects.
|
|
/// </summary>
|
|
public override void Update()
|
|
{
|
|
UpdateLean(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the lean value. Will first ensure the collider doesn't clip with any other objects.
|
|
/// </summary>
|
|
/// <param name="forceUpdate">Should the lean values be forced to update?</param>
|
|
private void UpdateLean(bool forceUpdate)
|
|
{
|
|
var update = forceUpdate;
|
|
// If a collider exists then the lean should not clip any walls. Note that the collider doesn't actually move - it stays at the maximum lean
|
|
// distance so ComputePenetration can detect how much to retract the lean in order to prevent any clipping.
|
|
if (m_Collider != null) {
|
|
var collisionLayerEnabled = m_CharacterLocomotion.CollisionLayerEnabled;
|
|
m_CharacterLocomotion.EnableColliderCollisionLayer(false);
|
|
int hitCount;
|
|
if (m_Collider is CapsuleCollider) {
|
|
Vector3 startEndCap, endEndCap;
|
|
var capsuleCollider = m_Collider as CapsuleCollider;
|
|
MathUtility.CapsuleColliderEndCaps(capsuleCollider, m_ColliderTransform.TransformPoint(capsuleCollider.center), m_ColliderTransform.rotation, out startEndCap, out endEndCap);
|
|
hitCount = Physics.OverlapCapsuleNonAlloc(startEndCap, endEndCap, capsuleCollider.radius * MathUtility.ColliderRadiusMultiplier(capsuleCollider),
|
|
m_OverlapColliders, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore);
|
|
} else { // SphereCollider.
|
|
var sphereCollider = m_Collider as SphereCollider;
|
|
hitCount = Physics.OverlapSphereNonAlloc(m_ColliderTransform.TransformPoint(sphereCollider.center), sphereCollider.radius *
|
|
MathUtility.ColliderRadiusMultiplier(sphereCollider),
|
|
m_OverlapColliders, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore);
|
|
}
|
|
|
|
if (hitCount > 0) {
|
|
Vector3 direction;
|
|
float distance;
|
|
var offset = Vector3.zero;
|
|
// Determine the offset required to resolve the collision. Note that for multiple hit colliders this will not always be resolved on the first iteration
|
|
// but it doesn't need to be perfect for a lean.
|
|
for (int i = 0; i < hitCount; ++i) {
|
|
if (Physics.ComputePenetration(m_Collider, m_ColliderTransform.position, m_ColliderTransform.rotation,
|
|
m_OverlapColliders[i], m_OverlapColliders[i].transform.position, m_OverlapColliders[i].transform.rotation, out direction, out distance)) {
|
|
offset += direction.normalized * (distance + m_CharacterLocomotion.ColliderSpacing);
|
|
}
|
|
}
|
|
// Determing if there is any horizontal collision. If a collision exists then the lean should be updated to prevent any clipping.
|
|
var hitDistance = m_Transform.InverseTransformDirection(offset).x;
|
|
if (m_HitDistance != hitDistance) {
|
|
m_HitDistance = hitDistance;
|
|
update = true;
|
|
}
|
|
} else if (m_HitDistance > 0) {
|
|
// The collider was previously overlapping an object but it is not anymore. Update lean.
|
|
m_HitDistance = 0;
|
|
update = true;
|
|
}
|
|
m_CharacterLocomotion.EnableColliderCollisionLayer(collisionLayerEnabled);
|
|
}
|
|
|
|
// Update the lean if the ability is just starting or stopping, there is an axis value change, or there is a collision.
|
|
if (update) {
|
|
float distance, tilt;
|
|
if (m_AxisValue == 0) {
|
|
distance = tilt = 0;
|
|
} else {
|
|
distance = m_Distance * -Mathf.Sign(m_AxisValue);
|
|
tilt = m_Tilt * Mathf.Sign(m_AxisValue);
|
|
}
|
|
|
|
// The collider should always be at the maximum value to allow for a stable ComputePenetration value.
|
|
if (m_ColliderTransform != null) {
|
|
var localPosition = m_ColliderTransform.localPosition;
|
|
localPosition.x = distance * m_ColliderOffsetMultiplier;
|
|
m_ColliderTransform.localPosition = localPosition;
|
|
}
|
|
|
|
// Prevent any clipping.
|
|
if (Mathf.Abs(m_HitDistance) > 0) {
|
|
var percent = 1 - Mathf.Abs(m_HitDistance) / m_Distance;
|
|
distance *= percent;
|
|
tilt *= percent;
|
|
}
|
|
// Notify those interested of the distance and tilt value.
|
|
EventHandler.ExecuteEvent(m_GameObject, c_LeanEventName, distance, tilt, m_ItemTiltMultiplier);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The AbilityInputEvent has updated the axis value.
|
|
/// </summary>
|
|
/// <param name="value">The updated axis value.</param>
|
|
private void OnInputUpdate(float value)
|
|
{
|
|
if (m_AxisValue != value) {
|
|
m_AxisValue = value;
|
|
UpdateLean(true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The ability has stopped running.
|
|
/// </summary>
|
|
/// <param name="force">Was the ability force stopped?</param>
|
|
protected override void AbilityStopped(bool force)
|
|
{
|
|
base.AbilityStopped(force);
|
|
|
|
// Update one last time with an axis value of 0 to return to the starting position.
|
|
m_AxisValue = 0;
|
|
UpdateLean(true);
|
|
|
|
// The collider is no longer needed.
|
|
if (m_ColliderGameObject != null) {
|
|
m_ColliderGameObject.SetActive(false);
|
|
}
|
|
|
|
if (m_Handler != null) {
|
|
m_Handler.UnregisterInputEvent(m_LeanInput);
|
|
GenericObjectPool.Return(m_LeanInput);
|
|
}
|
|
EventHandler.UnregisterEvent<float>(m_GameObject, "OnLeanInputUpdate", OnInputUpdate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The character perspective between first and third person has changed.
|
|
/// </summary>
|
|
/// <param name="firstPersonPerspective">Is the character in a first person perspective?</param>
|
|
private void OnChangePerspectives(bool firstPersonPerspective)
|
|
{
|
|
// Lean does not work in third person mode.
|
|
Enabled = firstPersonPerspective;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The character has been destroyed.
|
|
/// </summary>
|
|
public override void OnDestroy()
|
|
{
|
|
base.OnDestroy();
|
|
|
|
EventHandler.UnregisterEvent<bool>(m_GameObject, "OnLeanInputUpdate", OnChangePerspectives);
|
|
}
|
|
}
|
|
} |