using UnityEngine; using UnityEngine.Events; namespace Streamingle { /// /// Launches throwable objects at a target (avatar's head). /// Connect to WefLab donation events to throw objects on donations. /// Uses parabolic interpolation for smooth arc trajectories. /// public class ThrowableObjectLauncher : MonoBehaviour { [Header("Target")] [Tooltip("The target transform to throw objects at (usually avatar's head)")] public Transform target; [Tooltip("Fixed offset from target position")] public Vector3 targetOffset = Vector3.zero; [Tooltip("Random offset range for hit position")] public Vector3 targetRandomOffset = new Vector3(0.1f, 0.1f, 0.1f); [Header("Target Collider")] [Tooltip("Automatically create a collider on target for bounce detection")] public bool autoCreateTargetCollider = true; [Tooltip("Radius of the auto-created sphere collider")] public float targetColliderRadius = 0.15f; [Header("Spawn Settings")] [Tooltip("Prefabs to throw (randomly selected)")] public GameObject[] throwablePrefabs; [Tooltip("Spawn position offset from this transform")] public Vector3 spawnOffset = new Vector3(0, 1f, 0); [Tooltip("Randomize spawn position within this range")] public Vector3 spawnRandomRange = new Vector3(5f, 1f, 5f); [Header("Throw Settings")] [Tooltip("Delay between throws when throwing multiple")] public float throwInterval = 0.15f; [Header("Object Lifetime")] [Tooltip("Destroy thrown objects after this time (0 = never)")] public float objectLifetime = 5f; [Tooltip("Use object pooling instead of instantiate/destroy")] public bool usePooling = true; [Tooltip("Pool size per prefab")] public int poolSizePerPrefab = 10; [Header("Events")] public UnityEvent onObjectThrown; public UnityEvent onObjectHit; // Object pool private GameObject[][] objectPool; private int[] poolIndices; // Auto-created collider private SphereCollider createdTargetCollider; void Start() { if (usePooling) { InitializePool(); } if (autoCreateTargetCollider && target != null) { SetupTargetCollider(); } } private void SetupTargetCollider() { // Check if target already has a collider var existingCollider = target.GetComponent(); if (existingCollider != null) { UnityEngine.Debug.Log($"[ThrowableObjectLauncher] Target already has collider: {existingCollider.GetType().Name}"); return; } // Create sphere collider on target with offset applied createdTargetCollider = target.gameObject.AddComponent(); createdTargetCollider.radius = targetColliderRadius; // Apply targetOffset to collider center (convert world offset to local space) createdTargetCollider.center = target.InverseTransformDirection(targetOffset); // Ensure target has rigidbody (kinematic) for collision detection var rb = target.GetComponent(); if (rb == null) { rb = target.gameObject.AddComponent(); rb.isKinematic = true; rb.useGravity = false; } UnityEngine.Debug.Log($"[ThrowableObjectLauncher] Created SphereCollider on target: {target.name}, radius: {targetColliderRadius}, center: {createdTargetCollider.center}"); } void OnDestroy() { // Clean up created collider if (createdTargetCollider != null) { Destroy(createdTargetCollider); } } private void InitializePool() { if (throwablePrefabs == null || throwablePrefabs.Length == 0) return; objectPool = new GameObject[throwablePrefabs.Length][]; poolIndices = new int[throwablePrefabs.Length]; for (int i = 0; i < throwablePrefabs.Length; i++) { if (throwablePrefabs[i] == null) continue; objectPool[i] = new GameObject[poolSizePerPrefab]; for (int j = 0; j < poolSizePerPrefab; j++) { var obj = Instantiate(throwablePrefabs[i], transform); obj.SetActive(false); // Add throwable component if not present var throwable = obj.GetComponent(); if (throwable == null) { throwable = obj.AddComponent(); } throwable.launcher = this; objectPool[i][j] = obj; } } } /// /// Throw a random object at the target /// public void ThrowObject() { ThrowObject(Random.Range(0, throwablePrefabs.Length)); } /// /// Throw a specific object at the target /// public void ThrowObject(int prefabIndex) { if (throwablePrefabs == null || throwablePrefabs.Length == 0) { UnityEngine.Debug.LogWarning("[ThrowableObjectLauncher] No throwable prefabs assigned"); return; } if (target == null) { UnityEngine.Debug.LogWarning("[ThrowableObjectLauncher] No target assigned"); return; } prefabIndex = Mathf.Clamp(prefabIndex, 0, throwablePrefabs.Length - 1); if (throwablePrefabs[prefabIndex] == null) return; // Get or create object GameObject obj = GetObject(prefabIndex); if (obj == null) return; // Calculate spawn position Vector3 spawnPos = transform.position + transform.TransformDirection(spawnOffset); spawnPos += new Vector3( Random.Range(-spawnRandomRange.x, spawnRandomRange.x), Random.Range(-spawnRandomRange.y, spawnRandomRange.y), Random.Range(-spawnRandomRange.z, spawnRandomRange.z) ); // Calculate random offset for target position Vector3 randomOffset = new Vector3( Random.Range(-targetRandomOffset.x, targetRandomOffset.x), Random.Range(-targetRandomOffset.y, targetRandomOffset.y), Random.Range(-targetRandomOffset.z, targetRandomOffset.z) ); // Combine fixed offset + random offset Vector3 combinedOffset = targetOffset + randomOffset; // Setup object position obj.transform.position = spawnPos; obj.transform.rotation = Random.rotation; obj.SetActive(true); // Setup throwable component with combined offset var throwable = obj.GetComponent(); if (throwable != null) { throwable.Initialize(target, objectLifetime, combinedOffset); } onObjectThrown?.Invoke(obj); } /// /// Throw multiple objects /// public void ThrowMultiple(int count) { for (int i = 0; i < count; i++) { // Delay each throw slightly float delay = i * throwInterval; StartCoroutine(ThrowDelayed(delay)); } } private System.Collections.IEnumerator ThrowDelayed(float delay) { yield return new WaitForSeconds(delay); ThrowObject(); } private GameObject GetObject(int prefabIndex) { GameObject obj; if (usePooling && objectPool != null && objectPool[prefabIndex] != null) { // Find inactive object in pool for (int i = 0; i < poolSizePerPrefab; i++) { int idx = (poolIndices[prefabIndex] + i) % poolSizePerPrefab; if (!objectPool[prefabIndex][idx].activeInHierarchy) { poolIndices[prefabIndex] = (idx + 1) % poolSizePerPrefab; obj = objectPool[prefabIndex][idx]; // Ensure launcher reference is set var throwable = obj.GetComponent(); if (throwable != null) throwable.launcher = this; return obj; } } // All in use, reuse oldest poolIndices[prefabIndex] = (poolIndices[prefabIndex] + 1) % poolSizePerPrefab; obj = objectPool[prefabIndex][poolIndices[prefabIndex]]; obj.SetActive(false); // Ensure launcher reference is set var throwableOldest = obj.GetComponent(); if (throwableOldest != null) throwableOldest.launcher = this; return obj; } else { // Instantiate new obj = Instantiate(throwablePrefabs[prefabIndex]); var throwable = obj.GetComponent(); if (throwable == null) { throwable = obj.AddComponent(); } throwable.launcher = this; return obj; } } /// /// Called by ThrowableObject when it reaches the target /// public void OnObjectHitTarget(GameObject obj, Collider hitCollider, Collision collision) { onObjectHit?.Invoke(obj, collision); } #region Context Menu Test Methods [ContextMenu("Test: Throw 1")] private void TestThrowOne() { ThrowObject(); } [ContextMenu("Test: Throw 5")] private void TestThrowFive() { ThrowMultiple(5); } #endregion void OnDrawGizmosSelected() { // Draw spawn area Gizmos.color = Color.green; Vector3 spawnPos = transform.position + transform.TransformDirection(spawnOffset); Gizmos.DrawWireCube(spawnPos, spawnRandomRange * 2); // Draw target if (target != null) { Gizmos.color = Color.red; Vector3 targetPos = target.position + targetOffset; Gizmos.DrawWireSphere(targetPos, 0.2f); Gizmos.DrawWireCube(targetPos, targetRandomOffset * 2); // Draw target collider radius (at offset position) if (autoCreateTargetCollider) { Gizmos.color = Color.cyan; Gizmos.DrawWireSphere(target.position + targetOffset, targetColliderRadius); } // Draw trajectory preview (parabolic arc) Gizmos.color = Color.yellow; DrawParabolicArc(spawnPos, targetPos, 2f, 20); } } private void DrawParabolicArc(Vector3 start, Vector3 end, float arcHeight, int segments) { Vector3 prevPos = start; for (int i = 1; i <= segments; i++) { float t = i / (float)segments; Vector3 linearPos = Vector3.Lerp(start, end, t); float parabola = 4f * arcHeight * t * (1f - t); linearPos.y += parabola; Gizmos.DrawLine(prevPos, linearPos); prevPos = linearPos; } } } }