using UnityEngine; public class Projectile : MonoBehaviour { [Header("Flight Settings")] [SerializeField] private float speed = 50f; [SerializeField] private float lifeTime = 5f; [SerializeField] private bool usePhysics = false; [Header("Damage Settings")] [SerializeField] private int damage = 50; [SerializeField] private bool isExplosive = false; [SerializeField] private float explosionRadius = 4f; [SerializeField] private LayerMask damageableLayers = ~0; [Header("Visual & Audio Effects")] [SerializeField] private GameObject impactEffectPrefab; [SerializeField] private AudioClip impactSound; private Rigidbody rb; private string ownerTag = "Player"; private bool hasImpacted = false; private void Awake() { rb = GetComponent(); if (rb != null) { rb.useGravity = usePhysics; if (usePhysics) { rb.linearVelocity = transform.forward * speed; } } } private void Start() { Destroy(gameObject, lifeTime); } private void Update() { if (!usePhysics || rb == null) { // Manual kinematic translation with raycast sweep to prevent tunneling float step = speed * Time.deltaTime; Ray ray = new Ray(transform.position, transform.forward); if (Physics.Raycast(ray, out RaycastHit hit, step, damageableLayers, QueryTriggerInteraction.Ignore)) { if (hit.collider.CompareTag(ownerTag)) return; transform.position = hit.point; OnHit(hit.collider, hit.point, hit.normal); return; } transform.position += transform.forward * step; } } public void Initialize(int customDamage, float customSpeed, string shooterTag) { damage = customDamage; speed = customSpeed; ownerTag = shooterTag; if (rb != null && usePhysics) { rb.linearVelocity = transform.forward * speed; } } private void OnTriggerEnter(Collider other) { if (hasImpacted || other.CompareTag(ownerTag)) return; HandleCollision(other, transform.position, -transform.forward); } private void OnCollisionEnter(Collision collision) { if (hasImpacted || collision.collider.CompareTag(ownerTag)) return; ContactPoint contact = collision.GetContact(0); HandleCollision(collision.collider, contact.point, contact.normal); } private void OnHit(Collider hitCollider, Vector3 hitPoint, Vector3 hitNormal) { if (hasImpacted) return; HandleCollision(hitCollider, hitPoint, hitNormal); } private void HandleCollision(Collider hitCollider, Vector3 hitPoint, Vector3 hitNormal) { hasImpacted = true; if (isExplosive) { Explode(hitPoint); } else { // Direct hit damage if (hitCollider.TryGetComponent(out var enemy)) { enemy.TakeDamage(damage); } else if (hitCollider.TryGetComponent(out var playerHealth)) { playerHealth.TakeDamage(damage); } } // Spawn visual impact effect if (impactEffectPrefab != null) { Quaternion rot = hitNormal != Vector3.zero ? Quaternion.LookRotation(hitNormal) : Quaternion.identity; GameObject impact = Instantiate(impactEffectPrefab, hitPoint, rot); Destroy(impact, 2.5f); } // Play impact sound if (impactSound != null) { AudioSource.PlayClipAtPoint(impactSound, hitPoint); } Destroy(gameObject); } private void Explode(Vector3 center) { Collider[] hits = Physics.OverlapSphere(center, explosionRadius, damageableLayers); foreach (Collider col in hits) { if (col.CompareTag(ownerTag)) continue; if (col.TryGetComponent(out var enemy)) { float distance = Vector3.Distance(center, col.transform.position); float damageFalloff = Mathf.Clamp01(1f - (distance / explosionRadius)); int finalDamage = Mathf.RoundToInt(damage * damageFalloff); enemy.TakeDamage(Mathf.Max(1, finalDamage)); } else if (col.TryGetComponent(out var playerHealth)) { float distance = Vector3.Distance(center, col.transform.position); float damageFalloff = Mathf.Clamp01(1f - (distance / explosionRadius)); int finalDamage = Mathf.RoundToInt(damage * damageFalloff); playerHealth.TakeDamage(Mathf.Max(1, finalDamage)); } } } private void OnDrawGizmosSelected() { if (isExplosive) { Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, explosionRadius); } } }