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 { #region Initialize protected override void SelfInitialize() { _navMeshAgent = parent.GetComponent(); _rigidbody = parent.GetComponent(); _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 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), }; /// /// Checks if given point is on navmesh for current . /// 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; } /// /// To find destination checks all 8 directions around target and select point with shortest path. /// /// Object to which we want move /// If not zero then path can be ended ealier when within provided range.
/// ALSO selecting between those 8 directions prioritize those points within this range 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; } /// /// Calculates path from agent position to destination, when range provided whole path will be shortened. /// After that you can use and . /// /// Position to which we want move /// Possible ending path earlier when gets in range /// If given then will be checked to this position 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); } /// /// NavMeshPath's array of Vector3. This path can have invalid height and should not be used by anything else than Unity's NavMesh Components. /// public NavMeshPath GetPath() { return _path; } /// /// List of Vector3 with corrected height and safe to use for further calculations and drawings. /// public List 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(); } /// /// Move current Unit to target object (until in range to that object). /// /// Movement target /// Range which path will be shortened /// TRUE when move started successfully public bool Move(AbstractObject targetObject, float untilInRange = 0f) { CalculatePathTo(targetObject, untilInRange); return Move(); } /// /// Move current Unit to target destination (shortened by given range). /// /// Movement target /// Range which path will be shortened /// TRUE when move started successfully 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; } /// /// Rotate current Unit to look at given point. Only yaw, which mean he cant look up or down. /// /// Point at which unit will look 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); } /// /// Stops Unit's movement. /// public void Stop() { if (_navMeshAgent.isStopped) return; MoveFinished(); } /// /// Destination reached, clear everything. /// 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; } /// /// For every FixedUpdate check for: /// - NavMeshLink enter and movement /// - Turn movement resource consuming /// - Ending movement when reaching destination /// 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 _linkPath; private PathCorrectedLinkData _linkData; private Vector3 _linkExitPosition; private Coroutine _linkCoroutine; private ActionLoopAnimationList _linkTraverseAnimation; /// /// When path finding moves on NavMeshLink prepare for custom moving through it. /// 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); } } /// /// Finish link movement. No matter if its valid or not. /// 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); } /// /// Returns next link to use from generated path. /// private PathCorrectedLinkData GetLinkData() { var currentIndex = _pathCorrected.traversables.IndexOf(_linkData); return _pathCorrected.traversables.ElementAtOrDefault(currentIndex + 1); } #endregion } }