430 lines
10 KiB
C#
430 lines
10 KiB
C#
using Lean.Pool;
|
|
using UnityEngine;
|
|
|
|
namespace ArcherEnemy
|
|
{
|
|
/// <summary>
|
|
/// AI component for archer enemy that shoots arrows at target
|
|
/// Should be attached to archer enemy prefab
|
|
/// </summary>
|
|
public class ArcherShootingAI : MonoBehaviour
|
|
{
|
|
[Header("References")]
|
|
[Tooltip("Transform point from which arrows are shot (usually hand bone or weapon tip)")]
|
|
public Transform shootPoint;
|
|
|
|
[Tooltip("Arrow prefab with ArcherProjectile component")]
|
|
public GameObject arrowPrefab;
|
|
|
|
[Tooltip("Animator for triggering shoot animation")]
|
|
public Animator animator;
|
|
|
|
[Header("Targeting")]
|
|
[Tooltip("Auto-find player on start")]
|
|
public bool autoFindPlayer = true;
|
|
|
|
[Tooltip("Player tag to search for")]
|
|
public string playerTag = "Player";
|
|
|
|
[Tooltip("Height offset for aiming (aim at chest/head)")]
|
|
public float targetHeightOffset = 1.2f;
|
|
|
|
[Header("Shooting Parameters")]
|
|
[Tooltip("Minimum distance to shoot from")]
|
|
public float minShootDistance = 8f;
|
|
|
|
[Tooltip("Maximum distance to shoot from")]
|
|
public float maxShootDistance = 25f;
|
|
|
|
[Tooltip("Time between shots (seconds)")]
|
|
public float shootCooldown = 2f;
|
|
|
|
[Tooltip("Arrow launch speed (m/s)")]
|
|
public float arrowSpeed = 30f;
|
|
|
|
[Tooltip("How much to lead the target (predict movement)")]
|
|
[Range(0f, 1f)]
|
|
public float leadTargetAmount = 0.5f;
|
|
|
|
[Header("Animation")]
|
|
[Tooltip("Animator trigger parameter for shooting")]
|
|
public string shootTriggerName = "Shoot";
|
|
|
|
[Tooltip("Delay after animation starts before spawning arrow")]
|
|
public float shootAnimationDelay = 0.3f;
|
|
|
|
[Header("Aiming")]
|
|
[Tooltip("How quickly archer rotates to face target (degrees/sec)")]
|
|
public float turnSpeed = 180f;
|
|
|
|
[Tooltip("Angle tolerance for shooting (degrees)")]
|
|
public float aimTolerance = 15f;
|
|
|
|
[Header("Effects")]
|
|
[Tooltip("Muzzle flash effect at shoot point")]
|
|
public GameObject muzzleFlashPrefab;
|
|
|
|
[Tooltip("Shoot sound")]
|
|
public AudioClip shootSound;
|
|
|
|
[Header("Debug")]
|
|
[Tooltip("Enable debug logging")]
|
|
public bool enableDebug = false;
|
|
|
|
[Tooltip("Show aiming gizmos")]
|
|
public bool showGizmos = true;
|
|
|
|
#region Private Fields
|
|
|
|
private Transform target;
|
|
private float lastShootTime = -999f;
|
|
private bool isAiming = false;
|
|
private AudioSource audioSource;
|
|
private Vector3 lastKnownTargetVelocity;
|
|
private Vector3 lastTargetPosition;
|
|
|
|
#endregion Private Fields
|
|
|
|
#region Unity Lifecycle
|
|
|
|
private void Awake()
|
|
{
|
|
// Setup audio source
|
|
if (shootSound != null)
|
|
{
|
|
audioSource = GetComponent<AudioSource>();
|
|
if (audioSource == null)
|
|
{
|
|
audioSource = gameObject.AddComponent<AudioSource>();
|
|
audioSource.playOnAwake = false;
|
|
audioSource.spatialBlend = 1f;
|
|
}
|
|
}
|
|
|
|
// Find animator if not assigned
|
|
if (animator == null)
|
|
{
|
|
animator = GetComponent<Animator>();
|
|
}
|
|
|
|
// Find shoot point if not assigned
|
|
if (shootPoint == null)
|
|
{
|
|
// Try to find "ShootPoint" child transform
|
|
Transform shootPointChild = transform.Find("ShootPoint");
|
|
shootPoint = shootPointChild != null ? shootPointChild : transform;
|
|
}
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
if (autoFindPlayer)
|
|
{
|
|
FindPlayer();
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (target != null && isAiming)
|
|
{
|
|
RotateTowardsTarget();
|
|
UpdateTargetVelocity();
|
|
}
|
|
}
|
|
|
|
#endregion Unity Lifecycle
|
|
|
|
#region Target Management
|
|
|
|
/// <summary>
|
|
/// Finds player by tag
|
|
/// </summary>
|
|
private void FindPlayer()
|
|
{
|
|
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
|
|
if (player != null)
|
|
{
|
|
SetTarget(player.transform);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the target to shoot at
|
|
/// </summary>
|
|
public void SetTarget(Transform newTarget)
|
|
{
|
|
target = newTarget;
|
|
if (target != null)
|
|
{
|
|
lastTargetPosition = target.position;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates target velocity for prediction
|
|
/// </summary>
|
|
private void UpdateTargetVelocity()
|
|
{
|
|
if (target == null) return;
|
|
|
|
Vector3 currentPosition = target.position;
|
|
lastKnownTargetVelocity = (currentPosition - lastTargetPosition) / Time.deltaTime;
|
|
lastTargetPosition = currentPosition;
|
|
}
|
|
|
|
#endregion Target Management
|
|
|
|
#region Shooting Logic
|
|
|
|
/// <summary>
|
|
/// Checks if archer can shoot at target
|
|
/// </summary>
|
|
public bool CanShoot()
|
|
{
|
|
if (target == null) return false;
|
|
|
|
float distance = Vector3.Distance(transform.position, target.position);
|
|
|
|
// Check distance range
|
|
if (distance < minShootDistance || distance > maxShootDistance)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check cooldown
|
|
if (Time.time < lastShootTime + shootCooldown)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if facing target (within tolerance)
|
|
Vector3 directionToTarget = (target.position - transform.position).normalized;
|
|
float angleToTarget = Vector3.Angle(transform.forward, directionToTarget);
|
|
|
|
if (angleToTarget > aimTolerance)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initiates shooting sequence
|
|
/// </summary>
|
|
public void StartShooting()
|
|
{
|
|
if (!CanShoot())
|
|
{
|
|
if (enableDebug) Debug.Log("[ArcherShootingAI] Cannot shoot - conditions not met");
|
|
return;
|
|
}
|
|
|
|
isAiming = true;
|
|
|
|
// Trigger animation
|
|
if (animator != null && !string.IsNullOrEmpty(shootTriggerName))
|
|
{
|
|
animator.SetTrigger(shootTriggerName);
|
|
}
|
|
|
|
// Schedule arrow spawn after animation delay
|
|
Invoke(nameof(SpawnArrow), shootAnimationDelay);
|
|
|
|
lastShootTime = Time.time;
|
|
|
|
if (enableDebug) Debug.Log($"[ArcherShootingAI] Started shooting at {target.name}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawns arrow projectile
|
|
/// </summary>
|
|
private void SpawnArrow()
|
|
{
|
|
if (arrowPrefab == null)
|
|
{
|
|
Debug.LogError("[ArcherShootingAI] Arrow prefab not assigned!");
|
|
return;
|
|
}
|
|
|
|
if (shootPoint == null)
|
|
{
|
|
Debug.LogError("[ArcherShootingAI] Shoot point not assigned!");
|
|
return;
|
|
}
|
|
|
|
// Calculate aim point with prediction
|
|
Vector3 aimPoint = CalculateAimPoint();
|
|
|
|
// Calculate shoot direction
|
|
Vector3 shootDirection = (aimPoint - shootPoint.position).normalized;
|
|
|
|
// Calculate rotation for arrow
|
|
Quaternion arrowRotation = Quaternion.LookRotation(shootDirection);
|
|
|
|
// Spawn arrow
|
|
GameObject arrow = LeanPool.Spawn(arrowPrefab, shootPoint.position, arrowRotation);
|
|
|
|
// Configure arrow projectile
|
|
var projectile = arrow.GetComponent<ArcherProjectile>();
|
|
if (projectile != null)
|
|
{
|
|
projectile.initialSpeed = arrowSpeed;
|
|
}
|
|
|
|
// Play effects
|
|
PlayShootEffects();
|
|
|
|
if (enableDebug)
|
|
Debug.Log($"[ArcherShootingAI] Arrow spawned, direction: {shootDirection}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates aim point with target prediction
|
|
/// </summary>
|
|
private Vector3 CalculateAimPoint()
|
|
{
|
|
if (target == null) return shootPoint.position + transform.forward * 10f;
|
|
|
|
// Base aim point
|
|
Vector3 targetPosition = target.position + Vector3.up * targetHeightOffset;
|
|
|
|
// Add prediction based on target velocity
|
|
if (leadTargetAmount > 0f && lastKnownTargetVelocity.sqrMagnitude > 0.1f)
|
|
{
|
|
// Estimate time to target
|
|
float distance = Vector3.Distance(shootPoint.position, targetPosition);
|
|
float timeToTarget = distance / arrowSpeed;
|
|
|
|
// Predict future position
|
|
Vector3 predictedOffset = lastKnownTargetVelocity * timeToTarget * leadTargetAmount;
|
|
targetPosition += predictedOffset;
|
|
}
|
|
|
|
return targetPosition;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops shooting sequence
|
|
/// </summary>
|
|
public void StopShooting()
|
|
{
|
|
isAiming = false;
|
|
CancelInvoke(nameof(SpawnArrow));
|
|
}
|
|
|
|
#endregion Shooting Logic
|
|
|
|
#region Effects
|
|
|
|
private void PlayShootEffects()
|
|
{
|
|
// Spawn muzzle flash
|
|
if (muzzleFlashPrefab != null && shootPoint != null)
|
|
{
|
|
GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, shootPoint.position, shootPoint.rotation);
|
|
LeanPool.Despawn(flash, 2f);
|
|
}
|
|
|
|
// Play shoot sound
|
|
if (audioSource != null && shootSound != null)
|
|
{
|
|
audioSource.PlayOneShot(shootSound);
|
|
}
|
|
}
|
|
|
|
#endregion Effects
|
|
|
|
#region Rotation
|
|
|
|
/// <summary>
|
|
/// Smoothly rotates archer to face target
|
|
/// </summary>
|
|
private void RotateTowardsTarget()
|
|
{
|
|
if (target == null) return;
|
|
|
|
Vector3 directionToTarget = (target.position - transform.position);
|
|
directionToTarget.y = 0f; // Keep rotation on Y axis only
|
|
|
|
if (directionToTarget.sqrMagnitude > 0.001f)
|
|
{
|
|
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
|
|
transform.rotation = Quaternion.RotateTowards(
|
|
transform.rotation,
|
|
targetRotation,
|
|
turnSpeed * Time.deltaTime
|
|
);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if archer is facing target within tolerance
|
|
/// </summary>
|
|
public bool IsFacingTarget()
|
|
{
|
|
if (target == null) return false;
|
|
|
|
Vector3 directionToTarget = (target.position - transform.position).normalized;
|
|
float angle = Vector3.Angle(transform.forward, directionToTarget);
|
|
|
|
return angle <= aimTolerance;
|
|
}
|
|
|
|
#endregion Rotation
|
|
|
|
#region Public Query Methods
|
|
|
|
public Transform GetTarget() => target;
|
|
|
|
public bool IsAiming() => isAiming;
|
|
|
|
public float GetTimeSinceLastShot() => Time.time - lastShootTime;
|
|
|
|
public float GetDistanceToTarget() => target != null ? Vector3.Distance(transform.position, target.position) : float.MaxValue;
|
|
|
|
public bool IsInShootingRange() => GetDistanceToTarget() >= minShootDistance && GetDistanceToTarget() <= maxShootDistance;
|
|
|
|
#endregion Public Query Methods
|
|
|
|
#region Gizmos
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
if (!showGizmos) return;
|
|
|
|
// Draw shooting range
|
|
Gizmos.color = Color.yellow;
|
|
UnityEditor.Handles.color = new Color(1f, 1f, 0f, 0.1f);
|
|
UnityEditor.Handles.DrawSolidDisc(transform.position, Vector3.up, minShootDistance);
|
|
|
|
Gizmos.color = Color.green;
|
|
UnityEditor.Handles.color = new Color(0f, 1f, 0f, 0.1f);
|
|
UnityEditor.Handles.DrawSolidDisc(transform.position, Vector3.up, maxShootDistance);
|
|
|
|
// Draw shoot point
|
|
if (shootPoint != null)
|
|
{
|
|
Gizmos.color = Color.red;
|
|
Gizmos.DrawWireSphere(shootPoint.position, 0.1f);
|
|
Gizmos.DrawLine(shootPoint.position, shootPoint.position + shootPoint.forward * 2f);
|
|
}
|
|
|
|
// Draw aim line to target
|
|
if (target != null && Application.isPlaying)
|
|
{
|
|
Vector3 aimPoint = CalculateAimPoint();
|
|
Gizmos.color = Color.cyan;
|
|
Gizmos.DrawLine(shootPoint != null ? shootPoint.position : transform.position, aimPoint);
|
|
Gizmos.DrawWireSphere(aimPoint, 0.2f);
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
#endregion Gizmos
|
|
}
|
|
} |