224 lines
8.8 KiB
C#
224 lines
8.8 KiB
C#
#if !NEOFPS_FORCE_QUALITY && (UNITY_ANDROID || UNITY_IOS || UNITY_TIZEN || (UNITY_WSA && NETFX_CORE) || NEOFPS_FORCE_LIGHTWEIGHT)
|
|
#define NEOFPS_LIGHTWEIGHT
|
|
#endif
|
|
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using NeoFPS.CharacterMotion.Parameters;
|
|
using NeoCC;
|
|
using NeoFPS.CharacterMotion.MotionData;
|
|
|
|
namespace NeoFPS.CharacterMotion.Conditions
|
|
{
|
|
[MotionGraphElement("Physics/Climbable")]
|
|
public class ClimbableCondition : MotionGraphCondition
|
|
{
|
|
[SerializeField, Tooltip("The vector parameter to store the wall normal in once the check is complete")]
|
|
private VectorParameter m_OutputWallNormal = null;
|
|
[SerializeField, Tooltip("The direction to check in. YawForward casts directly forward in the direction the character is facing. InverseWallNormal takes the value already set in OutputWallNormal and casts back along it.")]
|
|
private CheckDirection m_CheckDirection = CheckDirection.YawForward;
|
|
[SerializeField, Range(0.05f, 0.5f), Tooltip("The distance to perform the initial wall check.")]
|
|
private FloatDataReference m_CheckDistance = new FloatDataReference(0.25f);
|
|
[SerializeField, Tooltip("The collision mask to use when checking the wall normal")]
|
|
private LayerMask m_WallCollisionMask = PhysicsFilter.Masks.StaticCharacterBlockers;
|
|
[SerializeField, Tooltip("The maximum height the character can move up to climb over the ledge")]
|
|
private float m_MaxClimbHeight = 1.5f;
|
|
[SerializeField, Tooltip("The distance the character should be able to move forwards past the edge of the climbable surface")]
|
|
private float m_ClimbForward = 0.25f;
|
|
[SerializeField, Tooltip("An optional parameter to store the climb height")]
|
|
private FloatParameter m_OutputClimbHeight = null;
|
|
|
|
public enum CheckDirection
|
|
{
|
|
YawForward,
|
|
InverseWallNormal
|
|
}
|
|
|
|
public override bool CheckCondition(MotionGraphConnectable connectable)
|
|
{
|
|
// Check wall vector
|
|
if (m_OutputWallNormal == null)
|
|
return false;
|
|
|
|
var ncc = controller.characterController;
|
|
float radius = ncc.radius;
|
|
float height = ncc.height;
|
|
RaycastHit hit;
|
|
|
|
// Get the check direction
|
|
Vector3 checkForward = Vector3.zero;
|
|
if (m_CheckDirection == CheckDirection.YawForward)
|
|
{
|
|
// Check collision flags
|
|
//if ((ncc.collisionFlags & (NeoCharacterCollisionFlags.MaskFront | NeoCharacterCollisionFlags.Below)) == NeoCharacterCollisionFlags.None)
|
|
// return false;
|
|
|
|
// Use the character forward
|
|
checkForward = ncc.forward;
|
|
}
|
|
else
|
|
{
|
|
// Only works if the stored wall normal has length
|
|
float mag = m_OutputWallNormal.value.magnitude;
|
|
if (mag < 0.5f)
|
|
return false;
|
|
|
|
// Use the inverse of the normal
|
|
checkForward = m_OutputWallNormal.value / -mag;
|
|
}
|
|
|
|
#if NEOFPS_LIGHTWEIGHT
|
|
|
|
// Cast and get the initial wall contact
|
|
if (ncc.CapsuleCast(checkForward * m_CheckDistance.value, Space.World, out hit, m_WallCollisionMask, QueryTriggerInteraction.Ignore))
|
|
{
|
|
// Record the wall normal
|
|
m_OutputWallNormal.value = hit.normal;
|
|
|
|
// Get head point at wall contact
|
|
Vector3 top = controller.localTransform.position;
|
|
top.y += height - radius;
|
|
top += checkForward * hit.distance;
|
|
|
|
// Sphere cast up the wall from the head point
|
|
if (PhysicsExtensions.SphereCastNonAllocSingle(
|
|
new Ray(top, Vector3.up),
|
|
radius - ncc.skinWidth,
|
|
out hit,
|
|
m_MaxClimbHeight,
|
|
PhysicsFilter.Masks.CharacterBlockers,
|
|
controller.localTransform,
|
|
QueryTriggerInteraction.Ignore
|
|
))
|
|
{
|
|
// Hit a ceiling
|
|
top = hit.point + hit.normal * radius;
|
|
}
|
|
else
|
|
{
|
|
// No obstruction overhead
|
|
top.y += m_MaxClimbHeight;
|
|
}
|
|
|
|
// Get forward direction
|
|
checkForward = -m_OutputWallNormal.value;
|
|
checkForward.y = 0f;
|
|
checkForward.Normalize();
|
|
|
|
// Cast forward
|
|
if (PhysicsExtensions.CapsuleCastNonAllocSingle(
|
|
top,
|
|
top - ncc.up * (height - radius * 2f),
|
|
radius - ncc.skinWidth,
|
|
checkForward,
|
|
out hit,
|
|
radius + m_ClimbForward,
|
|
PhysicsFilter.Masks.CharacterBlockers,
|
|
controller.localTransform,
|
|
QueryTriggerInteraction.Ignore
|
|
))
|
|
{
|
|
// No ledge to step onto, or ledge obstructed
|
|
return false;
|
|
}
|
|
|
|
// Route clear
|
|
return true;
|
|
}
|
|
|
|
#else
|
|
|
|
// Cast and get the initial wall contact
|
|
if (ncc.CapsuleCast(checkForward * m_CheckDistance.value, Space.World, out hit, m_WallCollisionMask, QueryTriggerInteraction.Ignore))
|
|
{
|
|
// Record the wall normal
|
|
m_OutputWallNormal.value = hit.normal;
|
|
|
|
// Get wall up vector
|
|
Vector3 projectedUp = Vector3.ProjectOnPlane(ncc.up, hit.normal).normalized;
|
|
|
|
// Get head point at wall contact
|
|
Vector3 top = controller.localTransform.position + ncc.up * (height - radius);
|
|
top += checkForward * hit.distance;
|
|
|
|
float verticalTravel = 0f;
|
|
|
|
// Sphere cast up the wall from the head point
|
|
if (PhysicsExtensions.SphereCastNonAllocSingle(
|
|
new Ray(top, projectedUp),
|
|
radius - ncc.skinWidth,
|
|
out hit,
|
|
m_MaxClimbHeight,
|
|
PhysicsFilter.Masks.CharacterBlockers,
|
|
controller.localTransform,
|
|
QueryTriggerInteraction.Ignore
|
|
))
|
|
{
|
|
// Hit a ceiling
|
|
top = hit.point + hit.normal * radius;
|
|
verticalTravel += hit.distance;
|
|
}
|
|
else
|
|
{
|
|
// No obstruction overhead
|
|
top = top + projectedUp * m_MaxClimbHeight;
|
|
verticalTravel += m_MaxClimbHeight;
|
|
}
|
|
|
|
// Get forward direction
|
|
checkForward = Vector3.ProjectOnPlane(-m_OutputWallNormal.value, ncc.up).normalized;
|
|
|
|
// Cast forward
|
|
var bottom = top - ncc.up * (height - radius * 2f);
|
|
if (PhysicsExtensions.CapsuleCastNonAllocSingle(
|
|
top,
|
|
bottom,
|
|
radius - ncc.skinWidth,
|
|
checkForward,
|
|
out hit,
|
|
radius + m_ClimbForward,
|
|
PhysicsFilter.Masks.CharacterBlockers,
|
|
controller.localTransform,
|
|
QueryTriggerInteraction.Ignore
|
|
))
|
|
{
|
|
// No ledge to step onto, or ledge obstructed
|
|
return false;
|
|
}
|
|
|
|
// If required, get the climb height
|
|
if (m_OutputClimbHeight != null)
|
|
{
|
|
// Raycast down to get a rough approximation of the height to climb
|
|
bottom += checkForward;
|
|
if (PhysicsExtensions.SphereCastNonAllocSingle(
|
|
new Ray(bottom, -ncc.up),
|
|
radius - ncc.skinWidth,
|
|
out hit,
|
|
verticalTravel,
|
|
PhysicsFilter.Masks.CharacterBlockers,
|
|
controller.localTransform,
|
|
QueryTriggerInteraction.Ignore
|
|
))
|
|
{
|
|
m_OutputClimbHeight.value = verticalTravel - hit.distance;
|
|
}
|
|
}
|
|
|
|
// Route clear
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
// No wall to climb
|
|
return false;
|
|
}
|
|
|
|
public override void CheckReferences(IMotionGraphMap map)
|
|
{
|
|
m_OutputWallNormal = map.Swap(m_OutputWallNormal);
|
|
m_OutputClimbHeight = map.Swap(m_OutputClimbHeight);
|
|
}
|
|
}
|
|
} |