You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
427 lines
17 KiB
C#
427 lines
17 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace Pegasus
|
|
{
|
|
/// <summary>
|
|
/// This will cause the targeted object to be followed by this object and has local avoidance as well.
|
|
/// </summary>
|
|
public class PegasusFollow : MonoBehaviour
|
|
{
|
|
//The thing we are chasing
|
|
public Transform m_target;
|
|
|
|
//Attachment & rotation to terrain
|
|
[Header("Local Overrides")]
|
|
[Tooltip("Always show gizmo's, even when not selected")]
|
|
public bool m_alwaysShowGizmos = false;
|
|
[Tooltip("Ground character to terrain")]
|
|
public bool m_groundToTerrain = true;
|
|
[Tooltip("Rotate character to terrain. Best for quadrapeds.")]
|
|
public bool m_rotateToTerrain = false;
|
|
[Tooltip("Avoid collisions with terrain trees.")]
|
|
public bool m_avoidTreeCollisions = false;
|
|
[Tooltip("Avoid collisions with objects on the object collision layer.")]
|
|
public bool m_avoidObjectCollisions = false;
|
|
[Tooltip("Distance to check for collisions. Bigger values will slow your system down.")]
|
|
[Range(1f, 10f)]
|
|
public float m_collisionRange = 2f;
|
|
[Tooltip("Layers to check for collisions on. Ensure that your terrain is NOT included in these layers.")]
|
|
public LayerMask m_objectCollisionLayer;
|
|
|
|
private int m_currentCollisionCount = 0;
|
|
private RaycastHit[] m_currentCollisionHitArray;
|
|
private int m_currentTreeCollisionCount = 0;
|
|
private List<TreeManager.TreeStruct> m_currentTreeCollisionHitArray = new List<TreeManager.TreeStruct>();
|
|
private TreeManager m_terrainTreeManager;
|
|
private Dictionary<int, Collider> m_myColliders = new Dictionary<int, Collider>();
|
|
|
|
//Target walk & run speeds
|
|
[Header("Character Speeds")]
|
|
[Tooltip("Walk speed")]
|
|
public float m_walkSpeed = 2f;
|
|
[Tooltip("Run speed")]
|
|
public float m_runSpeed = 7f;
|
|
|
|
//Determine when we can stop
|
|
[Header("Distance Thresholds")]
|
|
[Tooltip("Minimum distance from target that character can stop.")]
|
|
public float m_stopDistanceMin = 0.05f;
|
|
[Tooltip("Maximum distance from target that character can stop.")]
|
|
public float m_stopDistanceMax = 0.05f;
|
|
private float m_currentStopDistance = 0.05f;
|
|
private bool m_updateStopDistance = false;
|
|
|
|
//Speed beypond which we should run
|
|
[Tooltip("Character will run if further away from target than this distance, otherwise will walk or is stopped.")]
|
|
public float m_runIfFurtherThan = 15f;
|
|
|
|
[Header("Change Rates")]
|
|
[Range(0.001f, 3f), Tooltip("Turn rate. Lower values produce smoother turns, larger values produce more accurate turns.")]
|
|
public float m_turnChange = 1.5f;
|
|
[Range(0.001f, 3f), Tooltip("Movement rate. Lower values produce smoother movement, larger values produce more accurate movement.")]
|
|
public float m_movementChange = 1.5f;
|
|
|
|
[Header("Path Randomisation")]
|
|
[Range(0.0f, 2f), Tooltip("Deviation rate. Smaller values produce slower more subtle deviations, larger values produce more obvious and rapid deviations.")]
|
|
public float m_deviationRate = 0f;
|
|
[Tooltip("Deviation range in X plane")]
|
|
public float m_maxDeviationX = 0f;
|
|
[Tooltip("Deviation range in Y plane")]
|
|
public float m_maxDeviationY = 0f;
|
|
[Tooltip("Deviation range in Z plane")]
|
|
public float m_maxDeviationZ = 0f;
|
|
|
|
//Internal
|
|
private Vector3 m_targetPosition = Vector3.zero;
|
|
private float m_distanceToTarget = 0f;
|
|
private float m_currentMovementDistance = 0f;
|
|
private Vector3 m_currentVelocity = Vector3.zero;
|
|
|
|
// Use this for initialization
|
|
void Start()
|
|
{
|
|
if (m_target == null)
|
|
{
|
|
return;
|
|
}
|
|
m_updateStopDistance = false;
|
|
m_targetPosition = m_target.position;
|
|
m_distanceToTarget = (m_targetPosition - transform.position).magnitude;
|
|
m_currentStopDistance = UnityEngine.Random.Range(m_stopDistanceMin, m_stopDistanceMax);
|
|
m_currentCollisionCount = 0;
|
|
m_currentCollisionHitArray = new RaycastHit[20]; //Change the size here if you want more collisions to be considered
|
|
|
|
//Load up terrain trees if we are avoiding collisions
|
|
if (m_avoidTreeCollisions)
|
|
{
|
|
m_terrainTreeManager = new TreeManager();
|
|
m_terrainTreeManager.LoadTreesFromTerrain();
|
|
}
|
|
|
|
//Load up our own colliders - we will filter them out when checking for hits
|
|
Collider[] colliders = GetComponentsInChildren<Collider>();
|
|
foreach (Collider collider in colliders)
|
|
{
|
|
m_myColliders.Add(collider.GetInstanceID(), collider);
|
|
}
|
|
}
|
|
|
|
// Update is called once per frame
|
|
void Update()
|
|
{
|
|
//No target then exit
|
|
if (m_target == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//Get target position plus noise influence
|
|
Vector3 targetPosition = GetTargetPositionWithNoise(m_target.position);
|
|
|
|
//Get target position plus collision influence
|
|
targetPosition = GetTargetPositionWithCollisions(targetPosition);
|
|
|
|
//If nothing to do then exit
|
|
if (m_target.position == m_targetPosition && m_distanceToTarget <= m_currentStopDistance)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//And save it
|
|
m_targetPosition = targetPosition;
|
|
|
|
//Determine direction of target
|
|
Vector3 targetOffset = m_targetPosition - transform.position;
|
|
m_distanceToTarget = targetOffset.magnitude;
|
|
|
|
//Move to target
|
|
if (m_distanceToTarget > m_currentStopDistance)
|
|
{
|
|
//Signal that we need a new stop distance then next time we stop
|
|
m_updateStopDistance = true;
|
|
|
|
//Rotate to target
|
|
Vector3 targetRotation = new Vector3(targetOffset.x, 0f, targetOffset.z);
|
|
if (m_deviationRate > 0f && m_maxDeviationY > 0f)
|
|
{
|
|
targetRotation = new Vector3(targetOffset.x, targetOffset.y, targetOffset.z);
|
|
}
|
|
if (targetRotation == Vector3.zero)
|
|
{
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.identity, m_turnChange * Time.deltaTime);
|
|
}
|
|
else
|
|
{
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(targetRotation), m_turnChange * Time.deltaTime);
|
|
}
|
|
|
|
//Move
|
|
float distanceToMove = Time.deltaTime * m_walkSpeed;
|
|
if (m_distanceToTarget > m_runIfFurtherThan)
|
|
{
|
|
distanceToMove = Time.deltaTime * m_runSpeed;
|
|
}
|
|
m_currentMovementDistance = Mathf.Lerp(m_currentMovementDistance, distanceToMove, m_movementChange * Time.deltaTime);
|
|
m_currentVelocity = transform.forward * m_currentMovementDistance;
|
|
transform.position += m_currentVelocity;
|
|
m_currentVelocity *= (1f / Time.deltaTime); //Determine velocity per second
|
|
}
|
|
else
|
|
{
|
|
m_currentMovementDistance = 0f;
|
|
m_currentVelocity = Vector3.zero;
|
|
if (m_updateStopDistance)
|
|
{
|
|
m_updateStopDistance = false;
|
|
m_currentStopDistance = UnityEngine.Random.Range(m_stopDistanceMin, m_stopDistanceMax);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Fix up grounding and ground based rotation
|
|
/// </summary>
|
|
void LateUpdate()
|
|
{
|
|
if (m_target == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_groundToTerrain || m_rotateToTerrain)
|
|
{
|
|
Vector3 position = transform.position;
|
|
Terrain terrain = GetTerrain(position);
|
|
if (terrain == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
position.y = terrain.SampleHeight(position);
|
|
|
|
if (m_groundToTerrain)
|
|
{
|
|
transform.position = position;
|
|
}
|
|
|
|
if (m_rotateToTerrain)
|
|
{
|
|
Vector3 terrainLocalPos = terrain.transform.InverseTransformPoint(position);
|
|
Vector3 normalizedPos = new Vector3(Mathf.InverseLerp(0.0f, terrain.terrainData.size.x, terrainLocalPos.x),
|
|
Mathf.InverseLerp(0.0f, terrain.terrainData.size.y, terrainLocalPos.y),
|
|
Mathf.InverseLerp(0.0f, terrain.terrainData.size.z, terrainLocalPos.z));
|
|
Vector3 normal = terrain.terrainData.GetInterpolatedNormal(normalizedPos.x, normalizedPos.z);
|
|
//float steepness = terrain.terrainData.GetSteepness(normalizedPos.x, normalizedPos.z);
|
|
transform.rotation = Quaternion.FromToRotation(transform.up, normal) * transform.rotation;
|
|
}
|
|
}
|
|
|
|
//Update collisions if seleted
|
|
if (m_avoidObjectCollisions)
|
|
{
|
|
//Do sphere cast
|
|
m_currentCollisionCount = Physics.SphereCastNonAlloc(transform.position, m_collisionRange, transform.forward, m_currentCollisionHitArray, 0f, m_objectCollisionLayer, QueryTriggerInteraction.UseGlobal);
|
|
}
|
|
|
|
if (m_avoidTreeCollisions)
|
|
{
|
|
//Load trees if not loaded
|
|
if (m_terrainTreeManager == null)
|
|
{
|
|
m_terrainTreeManager = new TreeManager();
|
|
m_terrainTreeManager.LoadTreesFromTerrain();
|
|
}
|
|
//Do tree check
|
|
m_currentTreeCollisionCount = m_terrainTreeManager.GetTrees(transform.position, m_collisionRange + (m_collisionRange / 2f), ref m_currentTreeCollisionHitArray);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get target position influenced by noise
|
|
/// </summary>
|
|
/// <returns>Target position as influenced by noise</returns>
|
|
Vector3 GetTargetPositionWithNoise(Vector3 targetPosition)
|
|
{
|
|
//Offset target position by noise
|
|
if (m_deviationRate > 0f)
|
|
{
|
|
float noise = (-0.5f + Mathf.PerlinNoise(targetPosition.x * m_deviationRate, targetPosition.z * m_deviationRate)) * 2f;
|
|
Vector3 offset = new Vector3(noise * m_maxDeviationX, noise * m_maxDeviationY, noise * m_maxDeviationZ);
|
|
targetPosition += offset;
|
|
}
|
|
//And retrun
|
|
return targetPosition;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get target position inflenced by collisions
|
|
/// </summary>
|
|
/// <returns>Traget position influenced by avoiding collisions</returns>
|
|
Vector3 GetTargetPositionWithCollisions(Vector3 targetPosition)
|
|
{
|
|
//Collisions are updated in late update - exit quick if we can
|
|
if (m_currentCollisionCount == 0 && m_currentTreeCollisionCount == 0)
|
|
{
|
|
return targetPosition;
|
|
}
|
|
|
|
//Now process the collisions and update the target accordingly
|
|
RaycastHit hit;
|
|
float repulsion = 0f;
|
|
float distanceSqr = 0f;
|
|
float scale = 0f;
|
|
float sqrCollisionRange = m_collisionRange * m_collisionRange;
|
|
Vector3 desiredVelocity;
|
|
Vector3 targetOffset = Vector3.zero;
|
|
Vector3 position = transform.position;
|
|
|
|
//Project our position 0.5 second into future based on current velocity
|
|
//Debug.DrawRay(position, m_currentVelocity * m_predictFuture, Color.magenta);
|
|
position += (m_currentVelocity * 0.5f);
|
|
|
|
//Process general hits
|
|
for (int i = 0; i < m_currentCollisionCount; i++)
|
|
{
|
|
//Only check the collider if its not us
|
|
if (!m_myColliders.ContainsKey(m_currentCollisionHitArray[i].collider.GetInstanceID()))
|
|
{
|
|
Vector3 checkVector = m_currentCollisionHitArray[i].transform.position - position;
|
|
if (Physics.Raycast(position, checkVector, out hit, m_collisionRange, m_objectCollisionLayer))
|
|
{
|
|
desiredVelocity = position - hit.point;
|
|
distanceSqr = desiredVelocity.sqrMagnitude;
|
|
repulsion = Mathf.Abs(1f - (distanceSqr / sqrCollisionRange));
|
|
if (repulsion > 0f)
|
|
{
|
|
scale = repulsion * (sqrCollisionRange / distanceSqr);
|
|
desiredVelocity *= scale;
|
|
//Debug.DrawRay(position, desiredVelocity, Color.cyan);
|
|
targetOffset += desiredVelocity;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Process tree hits
|
|
for (int i = 0; i < m_currentTreeCollisionCount; i++)
|
|
{
|
|
desiredVelocity = position - m_currentTreeCollisionHitArray[i].position;
|
|
distanceSqr = desiredVelocity.sqrMagnitude - 0.0625f; //Allow for trunk width
|
|
repulsion = Mathf.Abs(1f - (distanceSqr / sqrCollisionRange));
|
|
if (repulsion > 0f)
|
|
{
|
|
scale = repulsion * (sqrCollisionRange / distanceSqr);
|
|
desiredVelocity *= scale;
|
|
//Debug.DrawRay(position, desiredVelocity, Color.cyan);
|
|
targetOffset += desiredVelocity;
|
|
}
|
|
}
|
|
|
|
//Make it an average of overall hits
|
|
//targetOffset /= (float)(m_currentCollisionCount + m_currentTreeCollisionCount);
|
|
//Debug.DrawRay(position, targetOffset, Color.magenta);
|
|
|
|
return targetPosition + targetOffset;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the terrain that matches this location, otherwise return null
|
|
/// </summary>
|
|
/// <param name="locationWU">Location to check in world units</param>
|
|
/// <returns>Terrain here or null</returns>
|
|
private Terrain GetTerrain(Vector3 locationWU)
|
|
{
|
|
Terrain terrain;
|
|
Vector3 terrainMin = new Vector3();
|
|
Vector3 terrainMax = new Vector3();
|
|
|
|
//First check active terrain - most likely already selected
|
|
terrain = Terrain.activeTerrain;
|
|
if (terrain != null)
|
|
{
|
|
terrainMin = terrain.GetPosition();
|
|
terrainMax = terrainMin + terrain.terrainData.size;
|
|
if (locationWU.x >= terrainMin.x && locationWU.x <= terrainMax.x)
|
|
{
|
|
if (locationWU.z >= terrainMin.z && locationWU.z <= terrainMax.z)
|
|
{
|
|
return terrain;
|
|
}
|
|
}
|
|
}
|
|
|
|
//Then check rest of terrains
|
|
for (int idx = 0; idx < Terrain.activeTerrains.Length; idx++)
|
|
{
|
|
terrain = Terrain.activeTerrains[idx];
|
|
terrainMin = terrain.GetPosition();
|
|
terrainMax = terrainMin + terrain.terrainData.size;
|
|
if (locationWU.x >= terrainMin.x && locationWU.x <= terrainMax.x)
|
|
{
|
|
if (locationWU.z >= terrainMin.z && locationWU.z <= terrainMax.z)
|
|
{
|
|
return terrain;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw gizmos when not selected
|
|
/// </summary>
|
|
private void OnDrawGizmos()
|
|
{
|
|
DrawGizmos(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw gizmos when selected
|
|
/// </summary>
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
DrawGizmos(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw the gizmos
|
|
/// </summary>
|
|
/// <param name="isSelected"></param>
|
|
private void DrawGizmos(bool isSelected)
|
|
{
|
|
//Determine whether to drop out
|
|
if (!isSelected && !m_alwaysShowGizmos)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Color gCol = Gizmos.color;
|
|
|
|
//Draw what we are following
|
|
Gizmos.color = Color.yellow;
|
|
Gizmos.DrawLine(transform.position, m_targetPosition);
|
|
|
|
//Draw collisions
|
|
if (m_avoidObjectCollisions || m_avoidTreeCollisions)
|
|
{
|
|
Gizmos.color = Color.cyan;
|
|
Gizmos.DrawWireSphere(transform.position, m_collisionRange);
|
|
|
|
Gizmos.color = Color.red;
|
|
for (int i = 0; i < m_currentCollisionCount; i++)
|
|
{
|
|
Gizmos.DrawLine(transform.position, m_currentCollisionHitArray[i].transform.position);
|
|
}
|
|
for (int i = 0; i < m_currentTreeCollisionCount; i++)
|
|
{
|
|
Gizmos.DrawLine(transform.position, m_currentTreeCollisionHitArray[i].position);
|
|
}
|
|
}
|
|
Gizmos.color = gCol;
|
|
}
|
|
}
|
|
}
|
|
|