316 lines
9.6 KiB
C#
316 lines
9.6 KiB
C#
using UnityEngine;
|
|
|
|
namespace Streamingle
|
|
{
|
|
/// <summary>
|
|
/// Component attached to throwable objects.
|
|
/// Uses parabolic interpolation for smooth arc trajectory.
|
|
/// On reaching target, enables collision detection - bounces off colliders or falls.
|
|
/// </summary>
|
|
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<Rigidbody>();
|
|
audioSource = GetComponent<AudioSource>();
|
|
colliders = GetComponents<Collider>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize the throwable object with target position
|
|
/// </summary>
|
|
public void Initialize(Transform target, float lifetime)
|
|
{
|
|
Initialize(target, lifetime, Vector3.zero);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize with offset for target position
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate position along parabolic arc
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when object reaches the target position
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|