540 lines
21 KiB
C#
540 lines
21 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Sirenix.Utilities;
|
|
using Unity.AI.Navigation;
|
|
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
using VOID.Generic.Dict;
|
|
using VOID.Generic.Navigation;
|
|
using VOID.Generic.Objects.Abstract;
|
|
using VOID.Generic.Objects.Unit.Animations;
|
|
using VOID.Generic.Objects.Unit.Events;
|
|
using VOID.Generic.Settings;
|
|
using VOID.Generic.Util;
|
|
using VOID.ScriptableObjects.Animations;
|
|
|
|
namespace VOID.Generic.Objects.Unit
|
|
{
|
|
[Serializable]
|
|
public class UnitObjectNavAgent : ObjectComponent<UnitObject>
|
|
{
|
|
#region Initialize
|
|
|
|
protected override void SelfInitialize()
|
|
{
|
|
_navMeshAgent = parent.GetComponent<NavMeshAgent>();
|
|
_rigidbody = parent.GetComponent<Rigidbody>();
|
|
|
|
_lastPosition = parent.transform.position;
|
|
}
|
|
|
|
#endregion
|
|
|
|
// Config
|
|
[field: SerializeField] public float moveSpeed { get; private set; } = 2;
|
|
|
|
// Getters
|
|
public Vector3 velocity => _navMeshAgent.velocity;
|
|
public Vector3 localVelocity => _navMeshAgent.transform.InverseTransformDirection(velocity);
|
|
|
|
// Current path and movement info
|
|
private MoveEvent _currentMoveEvent;
|
|
private NavMeshPath _path;
|
|
private PathCorrected _pathCorrected;
|
|
private Vector3 _lastPosition;
|
|
|
|
// Components
|
|
private NavMeshAgent _navMeshAgent;
|
|
private Rigidbody _rigidbody;
|
|
|
|
private static readonly List<Vector3> Directions = new()
|
|
{
|
|
Vector3.forward,
|
|
Vector3.Normalize(Vector3.forward + Vector3.right),
|
|
Vector3.right,
|
|
Vector3.Normalize(Vector3.back + Vector3.right),
|
|
Vector3.back,
|
|
Vector3.Normalize(Vector3.back + Vector3.left),
|
|
Vector3.left,
|
|
Vector3.Normalize(Vector3.forward + Vector3.left),
|
|
};
|
|
|
|
/// <summary>
|
|
/// Checks if given point is on navmesh for current <see cref="UnitObject"/>.
|
|
/// </summary>
|
|
public bool IsValidGround(Vector3 position)
|
|
{
|
|
const float maxDistance = 0.1f;
|
|
NavMesh.SamplePosition(position, out var hit, maxDistance, _navMeshAgent.areaMask);
|
|
if (!hit.hit) return false;
|
|
|
|
// Make sure that hit point is not too far horizontally
|
|
return Vector3.Distance(
|
|
Vector3.Scale(position, new Vector3(1, 0, 1)),
|
|
Vector3.Scale(hit.position, new Vector3(1, 0, 1))
|
|
) < maxDistance / 2;
|
|
}
|
|
|
|
/// <summary>
|
|
/// To find destination checks all 8 directions around target <see cref="AbstractObject"/> and select point with shortest path.
|
|
/// </summary>
|
|
/// <param name="targetObject">Object to which we want move</param>
|
|
/// <param name="untilInRange">If not <b>zero</b> then path can be ended ealier when within provided range.<br/>
|
|
/// ALSO selecting between those 8 directions prioritize those points within this range</param>
|
|
public void CalculatePathTo(AbstractObject targetObject, float? untilInRange = null)
|
|
{
|
|
var unitPos = targetObject.transform.position;
|
|
var positions = Directions
|
|
.Select(direction => unitPos + targetObject.transform.TransformDirection(direction) * targetObject.basicData.radius)
|
|
.Where(position => position != default)
|
|
.ToList();
|
|
|
|
var isInRange = new bool[positions.Count];
|
|
var paths = new NavMeshPath[positions.Count];
|
|
var lengths = new float[positions.Count];
|
|
var correctedPaths = new PathCorrected[positions.Count];
|
|
|
|
// Get points in all directions which will be used to decided to which one will we move
|
|
paths.ToList().ForEach((_, i) =>
|
|
{
|
|
CalculatePathTo(positions[i], untilInRange.GetValueOrDefault(), unitPos);
|
|
paths[i] = GetPath();
|
|
correctedPaths[i] = _pathCorrected;
|
|
positions[i] = paths[i].corners.Last();
|
|
isInRange[i] = !untilInRange.HasValue || Vector3.Distance(positions[i], unitPos) < untilInRange;
|
|
lengths[i] = NavMeshPathUtil.GetPathLength(paths[i]);
|
|
});
|
|
|
|
// Find closest point that also are within given range
|
|
var selectedKey = paths
|
|
.Select((_, i) => i)
|
|
.Where(i => isInRange[i])
|
|
.OrderBy(i => lengths[i])
|
|
.Aggregate(-1, (a, i) => a == -1 || lengths[a] > lengths[i] ? i : a);
|
|
|
|
// Point found! And it is without needed range!
|
|
if (selectedKey != -1)
|
|
{
|
|
_path = paths[selectedKey];
|
|
_pathCorrected = correctedPaths[selectedKey];
|
|
return;
|
|
}
|
|
|
|
// Find any closest point
|
|
selectedKey = paths
|
|
.Select((_, i) => i)
|
|
.OrderBy(i => lengths[i])
|
|
.Aggregate(-1, (a, i) => a == -1 || lengths[a] > lengths[i] ? i : a);
|
|
|
|
// Point found! But it is NOT within needed range! :(
|
|
if (selectedKey != -1)
|
|
{
|
|
_path = paths[selectedKey];
|
|
_pathCorrected = correctedPaths[selectedKey];
|
|
return;
|
|
}
|
|
|
|
// Somehow all paths are invalid!
|
|
_path = default;
|
|
_pathCorrected = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates path from agent position to destination, when range provided whole path will be shortened.
|
|
/// After that you can use <see cref="GetPath"/> and <see cref="GetCorrectedPath"/>.
|
|
/// </summary>
|
|
/// <param name="destination">Position to which we want move</param>
|
|
/// <param name="untilInRange">Possible ending path earlier when gets in range</param>
|
|
/// <param name="untilInRangeOfPosition">If given then <b><see cref="untilInRange"/> will be checked to this position</b></param>
|
|
public void CalculatePathTo(Vector3 destination, float untilInRange = default, Vector3 untilInRangeOfPosition = default)
|
|
{
|
|
if (_path?.corners.IsNullOrEmpty() == false && parent.transform.position == _path.corners.First() && destination == _path.corners.Last()) return;
|
|
|
|
_path = NavMeshPathUtil.GetPathTo(_navMeshAgent, destination);
|
|
var pathCorrected = _path.corners.ToList();
|
|
|
|
// Early path ending when in range to given position or destination
|
|
if (untilInRange > 0f)
|
|
{
|
|
untilInRangeOfPosition = untilInRangeOfPosition == default ? destination : untilInRangeOfPosition;
|
|
pathCorrected = NavMeshPathUtil.GetPathShortenedByRange(_path, untilInRangeOfPosition, untilInRange);
|
|
}
|
|
|
|
// Fixing height on path to be sure it wont move through things like ramps, stairs, ladders, etc.
|
|
_pathCorrected = NavMeshPathUtil.GetCorrectedPath(pathCorrected, parent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// NavMeshPath's array of Vector3. This path can have invalid height and should not be used by anything else than Unity's NavMesh Components.
|
|
/// </summary>
|
|
public NavMeshPath GetPath()
|
|
{
|
|
return _path;
|
|
}
|
|
|
|
/// <summary>
|
|
/// List of Vector3 with corrected height and safe to use for further calculations and drawings.
|
|
/// </summary>
|
|
public List<Vector3> GetCorrectedPath()
|
|
{
|
|
if (_path.status is not NavMeshPathStatus.PathInvalid) return _pathCorrected.path;
|
|
|
|
Debug.LogWarning("(Unity Internal) Cant calculate path. This can be false-positive.");
|
|
return new List<Vector3>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Move current Unit to target object (until in range to that object).
|
|
/// </summary>
|
|
/// <param name="targetObject">Movement target</param>
|
|
/// <param name="untilInRange">Range which path will be shortened</param>
|
|
/// <returns>TRUE when move started successfully</returns>
|
|
public bool Move(AbstractObject targetObject, float untilInRange = 0f)
|
|
{
|
|
CalculatePathTo(targetObject, untilInRange);
|
|
return Move();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Move current Unit to target destination (shortened by given range).
|
|
/// </summary>
|
|
/// <param name="targetPosition">Movement target</param>
|
|
/// <param name="untilInRange">Range which path will be shortened</param>
|
|
/// <returns>TRUE when move started successfully</returns>
|
|
public bool Move(Vector3 targetPosition, float untilInRange = 0f)
|
|
{
|
|
CalculatePathTo(targetPosition, untilInRange);
|
|
return Move();
|
|
}
|
|
|
|
private bool Move()
|
|
{
|
|
_navMeshAgent.speed = moveSpeed;
|
|
_navMeshAgent.enabled = true;
|
|
|
|
// Initialize whole movement by agent, but not moving yet...
|
|
_navMeshAgent.SetPath(_path);
|
|
_navMeshAgent.isStopped = true;
|
|
|
|
// ...we can be already at our destination so there is no need to move
|
|
if (CanFinish())
|
|
{
|
|
MoveFinished();
|
|
return true;
|
|
}
|
|
|
|
_currentMoveEvent = new MoveEvent
|
|
{
|
|
unit = parent,
|
|
pathCorrectedTarget = _path.corners.Last(),
|
|
path = _path,
|
|
pathCorrected = _pathCorrected
|
|
};
|
|
parent.unitEvents.onMoveStart.Invoke(_currentMoveEvent);
|
|
|
|
// Manage movement on every fixed update
|
|
parent.basicEvents.onFixedUpdate.AddListener(OnMove);
|
|
|
|
_navMeshAgent.isStopped = false;
|
|
parent.unitState.isMoving = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rotate current Unit to look at given point. Only yaw, which mean he cant look up or down.
|
|
/// </summary>
|
|
/// <param name="lootAt">Point at which unit will look</param>
|
|
public void Rotate(Vector3 lootAt)
|
|
{
|
|
var startPos = parent.transform.position;
|
|
startPos.y = 0f;
|
|
lootAt.y = 0f;
|
|
parent.transform.rotation = Quaternion.LookRotation(lootAt - startPos, Vector3.up);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops Unit's movement.
|
|
/// </summary>
|
|
public void Stop()
|
|
{
|
|
if (_navMeshAgent.isStopped) return;
|
|
|
|
MoveFinished();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Destination reached, clear everything.
|
|
/// </summary>
|
|
private void MoveFinished()
|
|
{
|
|
parent.basicEvents.onFixedUpdate.RemoveListener(OnMove);
|
|
|
|
// Clear unit's path and stop its movement
|
|
_navMeshAgent.ResetPath();
|
|
_navMeshAgent.isStopped = true;
|
|
_navMeshAgent.enabled = false;
|
|
|
|
parent.unitState.isMoving = false;
|
|
|
|
if (_currentMoveEvent != null) parent.unitEvents.onMoveEnd.Invoke(_currentMoveEvent);
|
|
_currentMoveEvent = null;
|
|
|
|
// Clear
|
|
_path = null;
|
|
_pathCorrected = null;
|
|
|
|
_lastPosition = parent.transform.position;
|
|
}
|
|
|
|
/// <summary>
|
|
/// For every FixedUpdate check for:
|
|
/// - NavMeshLink enter and movement
|
|
/// - Turn movement resource consuming
|
|
/// - Ending movement when reaching destination
|
|
/// </summary>
|
|
private void OnMove()
|
|
{
|
|
_navMeshAgent.velocity = _navMeshAgent.desiredVelocity;
|
|
|
|
if (_navMeshAgent.isOnOffMeshLink)
|
|
{
|
|
LinkStart();
|
|
return;
|
|
}
|
|
|
|
if (parent.basicState.turnManager)
|
|
{
|
|
var neededMovePoints = Vector3.Distance(_lastPosition, parent.transform.position);
|
|
var canMove = parent.unitData.UseResource(UnitResourceType.MovePoints, neededMovePoints);
|
|
if (!canMove)
|
|
{
|
|
MoveFinished();
|
|
return;
|
|
}
|
|
}
|
|
|
|
_lastPosition = parent.transform.position;
|
|
|
|
if (CanFinish())
|
|
{
|
|
// When unit is REALLY close to destination - make sure it stand at destination point
|
|
_rigidbody.MovePosition(_pathCorrected.path.Last());
|
|
|
|
MoveFinished();
|
|
}
|
|
}
|
|
|
|
private bool CanFinish()
|
|
{
|
|
return _navMeshAgent.remainingDistance <= 0.1f;
|
|
}
|
|
|
|
#region LINK TRAVERSE
|
|
|
|
// Current NavMeshLink handler
|
|
private Vector3 _linkPathStart;
|
|
private List<Vector3> _linkPath;
|
|
private PathCorrectedLinkData _linkData;
|
|
private Vector3 _linkExitPosition;
|
|
private Coroutine _linkCoroutine;
|
|
private ActionLoopAnimationList _linkTraverseAnimation;
|
|
|
|
/// <summary>
|
|
/// When path finding moves on NavMeshLink prepare for custom moving through it.
|
|
/// </summary>
|
|
private void LinkStart()
|
|
{
|
|
if (parent.unitState.isTraversing) return;
|
|
|
|
// When somehow NavMeshLink is not valid - end traversing instantly
|
|
var offMeshLink = _navMeshAgent.currentOffMeshLinkData.owner as NavMeshLink;
|
|
if (!offMeshLink)
|
|
{
|
|
Debug.LogWarning(
|
|
$"{parent.name} tried to use {nameof(NavMeshLink)}, but link is not found!"
|
|
+ "Instantly ending link movement!");
|
|
LinkEnd();
|
|
return;
|
|
}
|
|
|
|
// Get info about next NavMeshLink
|
|
_linkData = GetLinkData();
|
|
if (_linkData == null)
|
|
{
|
|
Debug.LogWarning(
|
|
$"{parent.name} tried to use {nameof(NavMeshLink)}, but {nameof(PathCorrectedLinkData)} is missing!"
|
|
+ "Instantly ending link movement!");
|
|
LinkEnd();
|
|
return;
|
|
}
|
|
|
|
// Get exit point for this traversable
|
|
_linkExitPosition = _linkData.isReverse
|
|
? _linkData.traversable.traversableData.linkStartPosition
|
|
: _linkData.traversable.traversableData.linkEndPosition;
|
|
if (_linkExitPosition == default)
|
|
{
|
|
Debug.LogWarning(
|
|
$"{parent.name} tried to use {nameof(NavMeshLink)}, but start and/or end position is missing!"
|
|
+ "Instantly ending link movement!");
|
|
LinkEnd();
|
|
return;
|
|
}
|
|
|
|
// Get animation for this traversal
|
|
_linkTraverseAnimation = _linkData.isReverse
|
|
? SettingsManager.humanoidAnimation.defaultLadderDownAnimation
|
|
: SettingsManager.humanoidAnimation.defaultLadderUpAnimation;
|
|
if (_linkExitPosition == default)
|
|
{
|
|
Debug.LogWarning(
|
|
$"{parent.name} tried to use {nameof(NavMeshLink)}, but traverse animation loop is missing!"
|
|
+ "Instantly ending link movement!");
|
|
LinkEnd();
|
|
return;
|
|
}
|
|
|
|
parent.unitAnimator.SetLoopAnimation(AnimationType.Traverse, _linkTraverseAnimation);
|
|
_navMeshAgent.updateRotation = false;
|
|
parent.unitState.isTraversing = true;
|
|
parent.unitAnimator.applyRootMotion = true;
|
|
|
|
// Finds:
|
|
// 1. Starting point for start animation
|
|
// 2. Starting and ending point for loop animation
|
|
FindLinkPath();
|
|
|
|
_linkCoroutine = parent.StartCoroutine(LinkTraverse());
|
|
}
|
|
|
|
private IEnumerator LinkTraverse()
|
|
{
|
|
yield return LinkTraverse_Start();
|
|
yield return LinkTraverse_LoopStart();
|
|
yield return LinkTraverse_Loop();
|
|
yield return LinkTraverse_LoopEnd();
|
|
yield return LinkTraverse_End();
|
|
|
|
LinkEnd();
|
|
}
|
|
|
|
private IEnumerator LinkTraverse_Start()
|
|
{
|
|
var transitionFrames = Mathf.RoundToInt(0.1f / Time.fixedDeltaTime);
|
|
|
|
var fromPosition = parent.transform.position;
|
|
var fromRotation = parent.transform.rotation;
|
|
|
|
var toPosition = _linkPathStart;
|
|
var toRotation = _linkData.traversable.transform.rotation;
|
|
|
|
for (var i = 1; i <= transitionFrames; i++)
|
|
{
|
|
yield return new WaitForFixedUpdate();
|
|
parent.transform.position += (toPosition - fromPosition) / transitionFrames;
|
|
parent.transform.rotation = Quaternion.Slerp(fromRotation, toRotation, (float)i / transitionFrames);
|
|
}
|
|
}
|
|
|
|
private IEnumerator LinkTraverse_LoopStart()
|
|
{
|
|
parent.unitAnimator.StartLoopAnimation(new AnimationGenericEvent
|
|
{ animationType = AnimationType.Traverse, unit = parent });
|
|
|
|
yield return new WaitForSeconds(_linkTraverseAnimation.list.startClip.length);
|
|
}
|
|
|
|
private IEnumerator LinkTraverse_Loop()
|
|
{
|
|
var linkPathDistance = Vector3.Distance(_linkPath[0], _linkPath[^1]);
|
|
var loopSpeedY = _linkTraverseAnimation.list.loopClip.averageSpeed.y;
|
|
var loopLength = Mathf.Abs(linkPathDistance / loopSpeedY);
|
|
yield return new WaitForSeconds(loopLength);
|
|
}
|
|
|
|
private IEnumerator LinkTraverse_LoopEnd()
|
|
{
|
|
parent.unitAnimator.EndLoopAnimation();
|
|
|
|
yield return new WaitForSeconds(_linkTraverseAnimation.list.endClip.length);
|
|
}
|
|
|
|
private IEnumerator LinkTraverse_End()
|
|
{
|
|
var transitionFrames = Mathf.RoundToInt(0.1f / Time.fixedDeltaTime);
|
|
|
|
var fromPosition = parent.transform.position;
|
|
var fromRotation = parent.transform.rotation;
|
|
|
|
var toPosition = _linkExitPosition;
|
|
var toRotation = _linkData.traversable.transform.rotation;
|
|
|
|
for (var i = 1; i <= transitionFrames; i++)
|
|
{
|
|
yield return new WaitForFixedUpdate();
|
|
parent.transform.position += (toPosition - fromPosition) / transitionFrames;
|
|
parent.transform.rotation = Quaternion.Slerp(fromRotation, toRotation, (float)i / transitionFrames);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finish link movement. No matter if its valid or not.
|
|
/// </summary>
|
|
private void LinkEnd()
|
|
{
|
|
_navMeshAgent.updateRotation = true;
|
|
_navMeshAgent.CompleteOffMeshLink();
|
|
parent.unitAnimator.EndLoopAnimation();
|
|
parent.unitState.isTraversing = false;
|
|
parent.unitAnimator.applyRootMotion = false;
|
|
if (_linkCoroutine != null)
|
|
{
|
|
parent.StopCoroutine(_linkCoroutine);
|
|
_linkCoroutine = null;
|
|
}
|
|
}
|
|
|
|
private void FindLinkPath()
|
|
{
|
|
// Unit's offset to traversable
|
|
var ladderOffset = Vector3.Scale(parent.unitData.ladderPosition.localPosition, new Vector3(1, 1, - 1));
|
|
var ladderOffsetVector = _linkData.traversable.transform.TransformDirection(ladderOffset);
|
|
|
|
// Whole traversable path with offset added
|
|
_linkPath = _linkData.traversable.traversableData.path
|
|
.Select(transform => transform.position + ladderOffsetVector)
|
|
.ToList();
|
|
if (_linkData.isReverse) _linkPath.Reverse();
|
|
|
|
var startOffset = _linkData.traversable.transform.TransformDirection(
|
|
_linkTraverseAnimation.list.startClip.averageSpeed * _linkTraverseAnimation.list.startClip.length);
|
|
var endOffset = _linkData.traversable.transform.TransformDirection(
|
|
_linkTraverseAnimation.list.endClip.averageSpeed * _linkTraverseAnimation.list.endClip.length);
|
|
|
|
// Path for loop animation
|
|
_linkPath[0] += new Vector3(0, startOffset.y, 0);
|
|
_linkPath[^1] -= new Vector3(0, endOffset.y, 0);
|
|
|
|
// Starting point for animation
|
|
_linkPathStart = _linkPath[0] - startOffset;
|
|
|
|
Debug.DrawLine(_linkPathStart, _linkPath[0], Color.red, 5f);
|
|
Debug.DrawLine(_linkPath[0], _linkPath[^1], Color.green, 5f);
|
|
Debug.DrawLine(_linkPath[^1], _linkPath[^1] + endOffset, Color.blue, 5f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns next link to use from generated path.
|
|
/// </summary>
|
|
private PathCorrectedLinkData GetLinkData()
|
|
{
|
|
var currentIndex = _pathCorrected.traversables.IndexOf(_linkData);
|
|
return _pathCorrected.traversables.ElementAtOrDefault(currentIndex + 1);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
} |