using UnityEngine;
namespace PlatypusIdeas.AirPath.Runtime.Modes {
public class TargetFollowPathfindingMode : PathfindingModeBase {
// Target configuration
private Transform _targetTransform;
private Vector2Int _lastTargetGridPos;
private Vector2Int _lastStartGridPos;
private bool _targetWasOutOfBounds = false;
// Recalculation settings
[System.Serializable]
public class RecalculationSettings {
public float MinRecalculationInterval = 0.5f;
public float TargetMoveThreshold = 2f;
public float SwarmMoveThreshold = 3f;
public bool UseDistanceBasedThrottling = true;
public float MaxThrottleDistance = 50f;
public float MaxThrottleMultiplier = 3f;
public bool ShowBoundaryWarnings = true;
}
private RecalculationSettings _settings;
private float _lastRecalculationTime;
// Visual indicators
private GameObject _targetIndicator;
private GameObject _boundaryWarningIndicator;
public override string ModeName => "Target Follow";
public TargetFollowPathfindingMode(RecalculationSettings settings) {
_settings = settings ?? new RecalculationSettings();
}
public void SetTarget(Transform target) {
if (target == _targetTransform) return;
_targetTransform = target;
if (_targetTransform) {
// Use safe conversion to handle out-of-bounds targets
_lastTargetGridPos = WorldToGridPositionSafe(_targetTransform.position, out _targetWasOutOfBounds);
if (_targetWasOutOfBounds && _settings.ShowBoundaryWarnings) {
Debug.LogWarning(
$"[Target Follow] Target '{_targetTransform.name}' is outside grid bounds. Position will be clamped.");
}
if (_isActive) {
ForceRecalculation();
UpdateTargetIndicator();
}
UpdateInstructionText($"Following target: {_targetTransform.name}" +
(_targetWasOutOfBounds ? " (clamped to grid bounds)" : ""));
}
else {
UpdateInstructionText("No target set - assign a target GameObject");
DestroyIndicators();
}
}
public Transform GetTarget() => _targetTransform;
public override void OnActivate() {
base.OnActivate();
_lastRecalculationTime = Time.time;
CreateIndicators();
if (_targetTransform) {
ForceRecalculation();
UpdateInstructionText($"Following target: {_targetTransform.name}");
}
else {
UpdateInstructionText("Target Follow Mode: Please assign a target GameObject");
}
}
public override void OnDeactivate() {
base.OnDeactivate();
DestroyIndicators();
}
public override void UpdateMode() {
if (!_isActive || !_targetTransform) return;
// Check if recalculation is needed
if (ShouldRecalculatePath()) {
RecalculatePath();
}
// Update visual indicators
UpdateTargetIndicator();
UpdateBoundaryWarning();
}
public override void Cleanup() {
base.Cleanup();
DestroyIndicators();
}
public override void DrawDebugVisualization() {
if (!_isActive || _targetTransform == null) return;
// Draw actual target position
Gizmos.color = _targetWasOutOfBounds ? Color.red : Color.yellow;
Gizmos.DrawWireSphere(_targetTransform.position, 2f);
Gizmos.DrawLine(_targetTransform.position, _targetTransform.position + Vector3.up * 10f);
// Draw clamped position if different
if (_targetWasOutOfBounds) {
var clampedWorldPos = GridToWorldPosition(_lastTargetGridPos, 0);
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(clampedWorldPos, 2.5f);
Gizmos.DrawLine(_targetTransform.position, clampedWorldPos);
}
// Draw recalculation threshold radius
var targetGridWorld = GridToWorldPosition(_lastTargetGridPos);
Gizmos.color = new Color(1f, 1f, 0f, 0.3f);
Gizmos.DrawWireSphere(targetGridWorld,
_settings.TargetMoveThreshold * Context.PathfindingService.WorldToGridPosition(Vector3.one * 10f).x /
10f);
}
public override string GetStatusInfo() {
if (!_targetTransform) {
return "No target assigned";
}
var timeSinceRecalc = Time.time - _lastRecalculationTime;
var throttleMultiplier = GetThrottleMultiplier();
var boundaryStatus = _targetWasOutOfBounds ? " [OUT OF BOUNDS]" : "";
return $"Target: {_targetTransform.name}{boundaryStatus} | " +
$"Last Recalc: {timeSinceRecalc:F1}s ago | " +
$"Throttle: {throttleMultiplier:F1}x";
}
/// Determines whether the pathfinding algorithm should recalculate the path based on several conditions.
/// The conditions include:
/// - Time since the last recalculation exceeding a minimum interval, adjusted by a throttle multiplier.
/// - Changes in the target's position beyond a defined threshold.
/// - Changes in the target's boundary status (in or out of bounds).
/// Additional checks are performed for swarm movement in relation to the target position when a path is active.
/// <returns>
/// True if the path should be recalculated based on the defined conditions; otherwise, false.
/// </returns>
private bool ShouldRecalculatePath() {
// Check time threshold with throttling
var throttleMultiplier = GetThrottleMultiplier();
var adjustedInterval = _settings.MinRecalculationInterval * throttleMultiplier;
if (Time.time - _lastRecalculationTime < adjustedInterval) {
return false;
}
// Check target movement threshold with safe boundary handling
var currentTargetGridPos = WorldToGridPositionSafe(_targetTransform.position, out bool currentlyOutOfBounds);
// Force recalculation if target boundary status changed
if (currentlyOutOfBounds != _targetWasOutOfBounds) {
_targetWasOutOfBounds = currentlyOutOfBounds;
return true;
}
// Check if target has moved from last known position
var targetGridDistance = Vector2Int.Distance(currentTargetGridPos, _lastTargetGridPos);
// IMPORTANT: Always check target movement, even if birds are at the target
// This ensures the system remains responsive when the target moves after birds arrive
if (targetGridDistance >= _settings.TargetMoveThreshold) {
return true; // Target has moved significantly - recalculate
}
// Check swarm movement threshold only if we have an active path
// AND the swarm hasn't reached the target yet
if (!IsPathActive()) return true;
var currentSwarmPos = WorldToGridPositionSafe(GetAverageBirdPosition(), out _);
// Only check swarm movement if they're not already at the target
// This prevents blocking recalculation when birds are at target but target moves
if (currentSwarmPos != currentTargetGridPos) {
var swarmGridDistance = Vector2Int.Distance(currentSwarmPos, _lastStartGridPos);
if (swarmGridDistance < _settings.SwarmMoveThreshold) {
return false; // Swarm hasn't moved enough yet
}
}
return true;
}
/// Calculates a throttle multiplier used for adjusting the frequency of pathfinding recalculations
/// based on the distance between the swarm and the target. The multiplier increases as the target
/// is farther away, helping to optimize performance by reducing recalculations for distant targets.
/// If distance-based throttling is disabled or the path is inactive, the multiplier defaults to 1.
/// <returns>
/// The calculated throttle multiplier, which varies from 1 to a maximum value defined by the settings.
/// The exact value is interpolated linearly based on the distance between the swarm and the target.
/// </returns>
private float GetThrottleMultiplier() {
if (!_settings.UseDistanceBasedThrottling || !IsPathActive()) {
return 1f;
}
var swarmPos = GetAverageBirdPosition();
var targetPos = _targetTransform.position;
var distance = Vector3.Distance(swarmPos, targetPos);
// Linear interpolation based on distance
var t = Mathf.Clamp01(distance / _settings.MaxThrottleDistance);
return Mathf.Lerp(1f, _settings.MaxThrottleMultiplier, t);
}
private void RecalculatePath() {
Vector2Int startPos;
Vector2Int endPos;
var startOutOfBounds = false;
var endOutOfBounds = false;
// Determine start position with boundary safety
if (IsPathActive()) {
startPos = WorldToGridPositionSafe(GetAverageBirdPosition(), out startOutOfBounds);
}
else {
startPos = WorldToGridPositionSafe(Context.TerrainController.transform.position, out startOutOfBounds);
}
// Get current target position with boundary safety
endPos = WorldToGridPositionSafe(_targetTransform.position, out endOutOfBounds);
_targetWasOutOfBounds = endOutOfBounds;
// Log boundary violations if needed
if ((startOutOfBounds || endOutOfBounds) && _settings.ShowBoundaryWarnings) {
var warning = "[Target Follow] Boundary violation detected: ";
if (startOutOfBounds) warning += "Swarm is outside grid. ";
if (endOutOfBounds) warning += $"Target '{_targetTransform.name}' is outside grid. ";
warning += "Positions clamped to valid bounds.";
Debug.LogWarning(warning);
}
// Store last positions BEFORE the early return check
_lastStartGridPos = startPos;
_lastTargetGridPos = endPos;
_lastRecalculationTime = Time.time;
// Check if start and end are the same (birds have reached the target)
// but don't prevent future recalculations - just skip this one
if (startPos == endPos) {
Debug.Log($"[Target Follow] Birds are at target position ({endPos.x}, {endPos.y})");
// Don't return early - still visualize and keep the system responsive
// Just skip the actual path request
// Still update visualization to show current state
ClearVisualization();
SetCellColor(startPos, startOutOfBounds ? WarningColor : StartColor);
// Important: Don't request a path to the same position
// but keep the system active for when the target moves again
return;
}
// Visualize with warning colors if out of bounds
ClearVisualization();
SetCellColor(startPos, startOutOfBounds ? WarningColor : StartColor);
SetCellColor(endPos, endOutOfBounds ? WarningColor : EndColor);
// Use the base class RequestPath method which will invoke the event properly
RequestPath(startPos, endPos);
}
private void ForceRecalculation() {
_lastRecalculationTime = 0; // Force time check to pass
RecalculatePath();
}
private void CreateIndicators() {
// Create target indicator
if (!_targetIndicator) {
_targetIndicator = GameObject.CreatePrimitive(PrimitiveType.Sphere);
_targetIndicator.name = "TargetIndicator";
_targetIndicator.transform.localScale = Vector3.one * 3f;
var renderer = _targetIndicator.GetComponent<MeshRenderer>();
if (renderer) {
renderer.material.color = Color.yellow;
}
var collider = _targetIndicator.GetComponent<Collider>();
if (collider) Object.Destroy(collider);
_targetIndicator.SetActive(false);
}
// Create boundary warning indicator
if (!_boundaryWarningIndicator) {
_boundaryWarningIndicator = GameObject.CreatePrimitive(PrimitiveType.Cube);
_boundaryWarningIndicator.name = "BoundaryWarningIndicator";
_boundaryWarningIndicator.transform.localScale = new Vector3(1f, 5f, 1f);
var renderer = _boundaryWarningIndicator.GetComponent<MeshRenderer>();
if (renderer) {
renderer.material.color = WarningColor;
}
var collider = _boundaryWarningIndicator.GetComponent<Collider>();
if (collider) Object.Destroy(collider);
_boundaryWarningIndicator.SetActive(false);
}
}
private void UpdateTargetIndicator() {
if (_targetIndicator is not null && _targetTransform) {
// Show indicator at clamped position if out of bounds
if (_targetWasOutOfBounds) {
var clampedPos = GridToWorldPosition(_lastTargetGridPos, 2f);
_targetIndicator.transform.position = clampedPos;
var renderer = _targetIndicator.GetComponent<MeshRenderer>();
if (renderer) {
renderer.material.color = WarningColor;
}
}
else {
_targetIndicator.transform.position = _targetTransform.position + Vector3.up * 2f;
var renderer = _targetIndicator.GetComponent<MeshRenderer>();
if (renderer) {
renderer.material.color = Color.yellow;
}
}
_targetIndicator.SetActive(true);
}
}
private void UpdateBoundaryWarning() {
if (_boundaryWarningIndicator && _targetTransform) {
if (_targetWasOutOfBounds) {
var clampedPos = GridToWorldPosition(_lastTargetGridPos, 5f);
_boundaryWarningIndicator.transform.position = clampedPos;
_boundaryWarningIndicator.SetActive(true);
// Pulse effect for warning
var scale = _boundaryWarningIndicator.transform.localScale;
scale.y = 5f + Mathf.Sin(Time.time * 3f) * 0.5f;
_boundaryWarningIndicator.transform.localScale = scale;
}
else {
_boundaryWarningIndicator.SetActive(false);
}
}
}
private void DestroyIndicators() {
if (_targetIndicator) {
if (Application.isPlaying) Object.Destroy(_targetIndicator);
else Object.DestroyImmediate(_targetIndicator);
_targetIndicator = null;
}
if (_boundaryWarningIndicator) {
if (Application.isPlaying) Object.Destroy(_boundaryWarningIndicator);
else Object.DestroyImmediate(_boundaryWarningIndicator);
_boundaryWarningIndicator = null;
}
}
}
}