341 lines
12 KiB
C#

using UnityEngine;
using UnityEngine.Events;
namespace Streamingle
{
/// <summary>
/// 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.
/// </summary>
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<GameObject> onObjectThrown;
public UnityEvent<GameObject, Collision> 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<Collider>();
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<SphereCollider>();
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<Rigidbody>();
if (rb == null)
{
rb = target.gameObject.AddComponent<Rigidbody>();
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<ThrowableObject>();
if (throwable == null)
{
throwable = obj.AddComponent<ThrowableObject>();
}
throwable.launcher = this;
objectPool[i][j] = obj;
}
}
}
/// <summary>
/// Throw a random object at the target
/// </summary>
public void ThrowObject()
{
ThrowObject(Random.Range(0, throwablePrefabs.Length));
}
/// <summary>
/// Throw a specific object at the target
/// </summary>
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<ThrowableObject>();
if (throwable != null)
{
throwable.Initialize(target, objectLifetime, combinedOffset);
}
onObjectThrown?.Invoke(obj);
}
/// <summary>
/// Throw multiple objects
/// </summary>
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<ThrowableObject>();
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<ThrowableObject>();
if (throwableOldest != null) throwableOldest.launcher = this;
return obj;
}
else
{
// Instantiate new
obj = Instantiate(throwablePrefabs[prefabIndex]);
var throwable = obj.GetComponent<ThrowableObject>();
if (throwable == null)
{
throwable = obj.AddComponent<ThrowableObject>();
}
throwable.launcher = this;
return obj;
}
}
/// <summary>
/// Called by ThrowableObject when it reaches the target
/// </summary>
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;
}
}
}
}