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;
}
}
}