init
This commit is contained in:
@@ -0,0 +1,540 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user