341 lines
12 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|