chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
211
Assets/Scripts/Combat/EnemyTargetDetector.cs
Normal file
211
Assets/Scripts/Combat/EnemyTargetDetector.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Finds nearby enemies (tagged "Enemy") that satisfy the assist constraints.
|
||||
/// Designed for zero-allocation queries during combat assists.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class EnemyTargetDetector : MonoBehaviour
|
||||
{
|
||||
private const float MinAssistRadius = 0.5f;
|
||||
|
||||
[Header("Assist Window")]
|
||||
[Tooltip("Maximum distance at which the assist can consider enemies.")]
|
||||
[SerializeField] private float assistRadius = 4.0f;
|
||||
|
||||
[Tooltip("Maximum field of view in degrees around the forward vector where assist is allowed.")]
|
||||
[SerializeField, Range(30f, 180f)] private float assistFOVDegrees = 130f;
|
||||
|
||||
[Tooltip("Optional hard cap on the delta angle to avoid 180° turns.")]
|
||||
[SerializeField, Range(45f, 180f)] private float maxAssistAngle = 150f;
|
||||
|
||||
[Tooltip("Layer mask used for enemy overlap queries.")]
|
||||
[SerializeField] private LayerMask enemyLayers = ~0;
|
||||
|
||||
[Tooltip("Maximum number of enemy candidates that will be considered per query.")]
|
||||
[SerializeField, Min(1)] private int maxCandidates = 12;
|
||||
|
||||
[Header("Line Of Sight")]
|
||||
[Tooltip("If enabled, a raycast must reach the target without hitting the obstruction layers.")]
|
||||
[SerializeField] private bool requireLineOfSight = false;
|
||||
|
||||
[SerializeField] private LayerMask obstructionLayers = ~0;
|
||||
|
||||
[SerializeField, Tooltip("Height offset for line of sight ray origin.")]
|
||||
private float losHeightOffset = 1.6f;
|
||||
|
||||
[SerializeField, Tooltip("Height offset for line of sight ray destination.")]
|
||||
private float targetHeightOffset = 1.2f;
|
||||
|
||||
private Collider[] candidateBuffer;
|
||||
private Transform hardLockOverride;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
maxCandidates = Mathf.Max(1, maxCandidates);
|
||||
candidateBuffer = new Collider[maxCandidates];
|
||||
if (assistRadius < MinAssistRadius)
|
||||
{
|
||||
assistRadius = MinAssistRadius;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optionally sets a manually chosen target that should be prioritised over soft candidates.
|
||||
/// </summary>
|
||||
public void SetHardLockTarget(Transform target)
|
||||
{
|
||||
hardLockOverride = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find the best enemy target within the configured assist window.
|
||||
/// </summary>
|
||||
public bool TryFindBestTarget(out Transform target, out Vector3 targetPosition)
|
||||
{
|
||||
target = null;
|
||||
targetPosition = Vector3.zero;
|
||||
|
||||
if (!enabled)
|
||||
return false;
|
||||
|
||||
if (hardLockOverride != null && IsTargetValid(hardLockOverride, out var hardLockPos))
|
||||
{
|
||||
target = hardLockOverride;
|
||||
targetPosition = hardLockPos;
|
||||
return true;
|
||||
}
|
||||
|
||||
var origin = transform.position;
|
||||
var forward = transform.forward;
|
||||
forward.y = 0f;
|
||||
forward.Normalize();
|
||||
|
||||
int hits = Physics.OverlapSphereNonAlloc(origin, assistRadius, candidateBuffer, enemyLayers, QueryTriggerInteraction.Collide);
|
||||
if (hits <= 0)
|
||||
return false;
|
||||
|
||||
float bestScore = float.MaxValue;
|
||||
Transform bestTarget = null;
|
||||
Vector3 bestPosition = Vector3.zero;
|
||||
|
||||
for (int i = 0; i < hits; i++)
|
||||
{
|
||||
Collider candidate = candidateBuffer[i];
|
||||
if (candidate == null)
|
||||
continue;
|
||||
|
||||
Transform candidateTransform = candidate.transform;
|
||||
if (!candidateTransform.CompareTag("Enemy"))
|
||||
continue;
|
||||
|
||||
if (!IsTargetValid(candidateTransform, out var candidatePosition))
|
||||
continue;
|
||||
|
||||
Vector3 toTarget = candidatePosition - origin;
|
||||
toTarget.y = 0f;
|
||||
float sqrDist = toTarget.sqrMagnitude;
|
||||
if (sqrDist < 0.0001f)
|
||||
continue;
|
||||
|
||||
Vector3 direction = toTarget / Mathf.Sqrt(sqrDist);
|
||||
float angle = Vector3.Angle(forward, direction);
|
||||
if (angle > assistFOVDegrees)
|
||||
continue;
|
||||
|
||||
if (angle > maxAssistAngle)
|
||||
continue;
|
||||
|
||||
if (requireLineOfSight && !HasLineOfSight(origin, candidatePosition))
|
||||
continue;
|
||||
|
||||
// Score by angle first, then distance.
|
||||
float score = angle * 10f + sqrDist;
|
||||
if (score < bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestTarget = candidateTransform;
|
||||
bestPosition = candidatePosition;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestTarget == null)
|
||||
return false;
|
||||
|
||||
target = bestTarget;
|
||||
targetPosition = bestPosition;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsTargetValid(Transform candidate, out Vector3 targetPosition)
|
||||
{
|
||||
targetPosition = candidate.position;
|
||||
|
||||
if (candidate == null)
|
||||
return false;
|
||||
|
||||
// Verify active in hierarchy (common alive check surrogate).
|
||||
if (!candidate.gameObject.activeInHierarchy)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool HasLineOfSight(Vector3 origin, Vector3 targetPosition)
|
||||
{
|
||||
Vector3 rayOrigin = origin + Vector3.up * losHeightOffset;
|
||||
Vector3 rayTarget = targetPosition + Vector3.up * targetHeightOffset;
|
||||
Vector3 delta = rayTarget - rayOrigin;
|
||||
float distance = delta.magnitude;
|
||||
if (distance <= 0.001f)
|
||||
return true;
|
||||
|
||||
Vector3 direction = delta / distance;
|
||||
if (Physics.Raycast(rayOrigin, direction, distance, obstructionLayers, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Inspector API
|
||||
|
||||
public float AssistRadius
|
||||
{
|
||||
get => assistRadius;
|
||||
set => assistRadius = Mathf.Max(MinAssistRadius, value);
|
||||
}
|
||||
|
||||
public float AssistFOVDegrees
|
||||
{
|
||||
get => assistFOVDegrees;
|
||||
set => assistFOVDegrees = Mathf.Clamp(value, 30f, 180f);
|
||||
}
|
||||
|
||||
public float MaxAssistAngle
|
||||
{
|
||||
get => maxAssistAngle;
|
||||
set => maxAssistAngle = Mathf.Clamp(value, 45f, 180f);
|
||||
}
|
||||
|
||||
public LayerMask EnemyLayers
|
||||
{
|
||||
get => enemyLayers;
|
||||
set => enemyLayers = value;
|
||||
}
|
||||
|
||||
public bool RequireLineOfSight
|
||||
{
|
||||
get => requireLineOfSight;
|
||||
set => requireLineOfSight = value;
|
||||
}
|
||||
|
||||
public LayerMask ObstructionLayers
|
||||
{
|
||||
get => obstructionLayers;
|
||||
set => obstructionLayers = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user