This commit is contained in:
2026-07-09 21:33:33 +02:00
commit 84a2a365f3
2364 changed files with 950134 additions and 0 deletions
@@ -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
}
}