using UnityEngine; namespace Streamingle { /// /// Component attached to throwable objects. /// Uses parabolic interpolation for smooth arc trajectory. /// On reaching target, enables collision detection - bounces off colliders or falls. /// public class ThrowableObject : MonoBehaviour { [HideInInspector] public ThrowableObjectLauncher launcher; [Header("Settings")] [Tooltip("Play sound on hit")] public AudioClip[] hitSounds; [Tooltip("Spawn particle effect on hit")] public GameObject hitEffectPrefab; [Tooltip("Destroy hit effect after this time")] public float hitEffectLifetime = 2f; [Header("Trajectory Settings")] [Tooltip("Time to reach target in seconds")] public float flightDuration = 1.5f; [Tooltip("Arc height (higher = more curved trajectory)")] public float arcHeight = 2f; [Tooltip("Rotation speed while flying")] public float rotationSpeed = 180f; [Header("Collision Settings")] [Tooltip("Bounce force multiplier when hitting collider")] public float bounceForce = 5f; [Tooltip("Additional upward force on bounce")] public float bounceUpForce = 2f; [Tooltip("Time to stay after reaching target before deactivating")] public float postArrivalLifetime = 3f; // Trajectory state private Vector3 startPosition; private Vector3 targetPosition; private Transform targetTransform; private float flightTime; private float currentTime; private bool isFlying = false; private bool hasArrived = false; private bool hasCollided = false; private Vector3 rotationAxis; private Vector3 arrivalVelocity; // Components private Rigidbody rb; private AudioSource audioSource; private Collider[] colliders; // Lifetime private float lifetime; private float spawnTime; void Awake() { rb = GetComponent(); audioSource = GetComponent(); colliders = GetComponents(); } /// /// Initialize the throwable object with target position /// public void Initialize(Transform target, float lifetime) { Initialize(target, lifetime, Vector3.zero); } /// /// Initialize with offset for target position /// public void Initialize(Transform target, float lifetime, Vector3 targetOffset) { this.lifetime = lifetime; this.spawnTime = Time.time; this.hasArrived = false; this.hasCollided = false; this.isFlying = true; this.currentTime = 0f; this.flightTime = flightDuration; this.startPosition = transform.position; this.targetTransform = target; // Calculate target position with offset if (target != null) { this.targetPosition = target.position + targetOffset; } else { this.targetPosition = transform.position + Vector3.forward * 5f; } // Random rotation axis for spinning effect rotationAxis = Random.insideUnitSphere.normalized; // Disable physics during flight if (rb != null) { rb.isKinematic = true; rb.useGravity = false; } // Disable colliders during flight SetCollidersEnabled(false); } void Update() { // Check lifetime if (lifetime > 0 && Time.time - spawnTime > lifetime) { Deactivate(); return; } if (!isFlying) return; // Store previous position for velocity calculation Vector3 prevPos = transform.position; currentTime += Time.deltaTime; float t = Mathf.Clamp01(currentTime / flightTime); // Calculate parabolic position Vector3 currentPos = CalculateParabolicPosition(t); transform.position = currentPos; // Calculate velocity for when we switch to physics if (Time.deltaTime > 0) { arrivalVelocity = (currentPos - prevPos) / Time.deltaTime; } // Rotate while flying transform.Rotate(rotationAxis, rotationSpeed * Time.deltaTime, Space.World); // Check if reached target if (t >= 1f) { OnReachedTarget(); } } /// /// Calculate position along parabolic arc /// private Vector3 CalculateParabolicPosition(float t) { // Linear interpolation for base position Vector3 linearPos = Vector3.Lerp(startPosition, targetPosition, t); // Parabolic arc (peaks at t=0.5) float parabola = 4f * arcHeight * t * (1f - t); // Add arc height to Y position linearPos.y += parabola; return linearPos; } /// /// Called when object reaches the target position /// private void OnReachedTarget() { isFlying = false; hasArrived = true; // Re-enable colliders for collision detection SetCollidersEnabled(true); // Enable physics - object will either hit a collider and bounce or fall if (rb != null) { rb.isKinematic = false; rb.useGravity = true; // Continue with the arrival velocity (maintains momentum) rb.linearVelocity = arrivalVelocity; rb.angularVelocity = rotationAxis * rotationSpeed * Mathf.Deg2Rad; } // Schedule deactivation (object falls if no collision) Invoke(nameof(Deactivate), postArrivalLifetime); } void OnCollisionEnter(Collision collision) { if (!hasArrived || hasCollided) return; // Check if collision is with the target bool isTargetHit = false; if (targetTransform != null) { // Check if collided object is the target or a child of target Transform hitTransform = collision.collider.transform; isTargetHit = hitTransform == targetTransform || hitTransform.IsChildOf(targetTransform); } hasCollided = true; // Only notify launcher and play effects if hit the target if (isTargetHit) { if (launcher != null) { launcher.OnObjectHitTarget(gameObject, collision.collider, collision); } // Play hit sound PlayHitSound(); // Spawn hit effect SpawnHitEffect(collision); } // Apply bounce force regardless of what was hit if (rb != null && collision.contacts.Length > 0) { Vector3 normal = collision.contacts[0].normal; Vector3 reflectedVel = Vector3.Reflect(rb.linearVelocity, normal); // Apply bounce with some force rb.linearVelocity = reflectedVel.normalized * bounceForce + Vector3.up * bounceUpForce; rb.angularVelocity = Random.insideUnitSphere * 10f; } } private void SetCollidersEnabled(bool enabled) { if (colliders == null) return; foreach (var col in colliders) { if (col != null) col.enabled = enabled; } } private void PlayHitSound() { if (hitSounds == null || hitSounds.Length == 0) return; AudioClip clip = hitSounds[Random.Range(0, hitSounds.Length)]; if (clip == null) return; if (audioSource != null) { audioSource.PlayOneShot(clip); } else { AudioSource.PlayClipAtPoint(clip, transform.position); } } private void SpawnHitEffect(Collision collision = null) { if (hitEffectPrefab == null) return; Vector3 pos = transform.position; Quaternion rot = Quaternion.identity; if (collision != null && collision.contacts.Length > 0) { pos = collision.contacts[0].point; rot = Quaternion.LookRotation(collision.contacts[0].normal); } GameObject effect = Instantiate(hitEffectPrefab, pos, rot); if (hitEffectLifetime > 0) { Destroy(effect, hitEffectLifetime); } } private void Deactivate() { CancelInvoke(); isFlying = false; hasArrived = false; hasCollided = false; if (launcher != null && launcher.usePooling) { // Return to pool gameObject.SetActive(false); } else { // Destroy Destroy(gameObject); } } void OnDisable() { CancelInvoke(); isFlying = false; hasArrived = false; hasCollided = false; } } }