MiniGames/Assets/Scripts/ClawGrab/ClawBubbleSpawner.cs

305 lines
10 KiB
C#
Raw Normal View History

// ClawBubbleSpawner.cs
using UnityEngine;
using System.Collections.Generic;
2025-09-06 00:08:22 +05:00
using DG.Tweening;
public class ClawBubbleSpawner : MonoBehaviour
{
public static ClawBubbleSpawner Instance;
2025-09-06 00:08:22 +05:00
public GameObject spawnPointLid;
public List<Material> bubbleTypeMaterials;
[Header("Single Prefab")]
[Tooltip("The ONLY bubble prefab (must contain ClawBubble + Rigidbody).")]
public GameObject bubblePrefab;
[Header("Spawn Point & Limits")]
[Tooltip("Where bubbles emerge (center opening).")]
public Transform spawnPoint;
[Tooltip("Seed bubbles at round start.")]
public int initialSeedCount = 6;
[Tooltip("Max number of bubbles alive at once.")]
public int maxConcurrentBubbles = 12;
[Tooltip("Spawn this many after every grab.")]
public int spawnPerGrab = 1;
[Tooltip("Extra spawns when a trap is grabbed (Bomb/Shock).")]
public int extraOnTrap = 2;
[Tooltip("Cooldown between spawns to avoid burst spam.")]
public float spawnCooldown = 0.15f;
[Header("Motion (Upward + Lateral)")]
public float riseImpulse = 8f;
[Header("Radial Ejection")]
[Tooltip("Use radial angles so balls peel away from the center without overlapping.")]
public bool useRadialEjection = true;
[Tooltip("Horizontal speed applied radially on spawn.")]
public float radialImpulse = 1.8f;
[Tooltip("Small offset from the exact center so spawns aren't coplanar.")]
public float spawnRadialOffset = 0.12f;
[Tooltip("Distribute subsequent spawns using the golden angle (~137.5<EFBFBD>).")]
public float goldenAngleDeg = 137.50776f;
[Tooltip("Minimum angular separation from existing balls (0 = off).")]
public float minAngleSeparationDeg = 18f;
[Tooltip("Tries to find a clear angle before giving up.")]
public int maxAngleTries = 16;
[Header("Initial Seed Layout")]
[Tooltip("Evenly space the first batch on a ring so they never overlap.")]
public bool seedOnEvenRing = true;
public float seedRingRadius = 0.5f;
public float seedAngleJitterDeg = 6f;
[Header("Fallback (Non-overlap by distance)")]
public float minSpawnSpacing = 0.3f;
public int maxPlacementTries = 10;
public LayerMask ballLayerMask = ~0;
[System.Serializable]
public struct WeightedType { public ClawBubbleType type; public float weight; }
public List<WeightedType> weights = new List<WeightedType>
{
new WeightedType{ type = ClawBubbleType.Wolf, weight = 50f },
new WeightedType{ type = ClawBubbleType.GoldenWolf, weight = 2f },
new WeightedType{ type = ClawBubbleType.Star, weight = 15f },
new WeightedType{ type = ClawBubbleType.Crystal, weight = 10f },
new WeightedType{ type = ClawBubbleType.Freeze, weight = 4f },
new WeightedType{ type = ClawBubbleType.Time, weight = 6f },
new WeightedType{ type = ClawBubbleType.Turbo, weight = 5f },
new WeightedType{ type = ClawBubbleType.Bomb, weight = 5f },
new WeightedType{ type = ClawBubbleType.Shock, weight = 3f },
};
// Internal
private readonly HashSet<GameObject> live = new HashSet<GameObject>();
private float lastSpawnTime;
private float nextAngleDeg; // used for golden-angle progression
2025-09-06 00:08:22 +05:00
public
void Awake()
{
if (Instance == null) Instance = this;
else Destroy(gameObject);
nextAngleDeg = Random.Range(0f, 360f);
}
void OnEnable() => StartRound();
[ContextMenu("StartRound")]
public void StartRound()
{
live.RemoveWhere(g => g == null);
StopAllCoroutines();
StartCoroutine(SeedInitial());
}
public void EndRound()
{
StopAllCoroutines();
foreach (var g in live) if (g) Destroy(g);
live.Clear();
}
System.Collections.IEnumerator SeedInitial()
{
if (seedOnEvenRing && initialSeedCount > 1 && spawnPoint)
{
float step = 360f / initialSeedCount;
for (int i = 0; i < initialSeedCount; i++)
{
float ang = i * step + Random.Range(-seedAngleJitterDeg, seedAngleJitterDeg);
SpawnRadial(ang, useRing: true);
lastSpawnTime = Time.time;
yield return new WaitForSeconds(spawnCooldown);
}
}
else
{
for (int i = 0; i < initialSeedCount; i++)
{
TrySpawn(1);
yield return new WaitForSeconds(spawnCooldown);
}
}
}
public void NotifyBubbleGrabbed(ClawBubbleType grabbedType)
{
TrySpawn(spawnPerGrab);
if (grabbedType == ClawBubbleType.Bomb || grabbedType == ClawBubbleType.Shock)
TrySpawn(extraOnTrap);
}
public void TrySpawn(int count)
{
if (!bubblePrefab || !spawnPoint) return;
live.RemoveWhere(g => g == null);
for (int i = 0; i < count; i++)
{
if (live.Count >= maxConcurrentBubbles) break;
if (Time.time - lastSpawnTime < spawnCooldown) break;
SpawnOne();
lastSpawnTime = Time.time;
}
}
void SpawnOne()
{
if (useRadialEjection)
{
// pick an angle using golden-angle progression, but enforce min separation
float chosen = PickClearAngle(nextAngleDeg, minAngleSeparationDeg, maxAngleTries);
SpawnRadial(chosen, useRing: false);
// advance angle for next time
nextAngleDeg = Mathf.Repeat(chosen + goldenAngleDeg, 360f);
}
else
{
// fallback to jittered position around center (with non-overlap)
if (!TryFindSpawnPos(out var pos))
pos = spawnPoint.position;
SpawnOneAt(pos, lateralDir: Vector3.zero); // will randomize inside
}
}
void SpawnRadial(float angleDeg, bool useRing)
{
// Build a local XZ direction from the angle, then convert to world via spawnPoint
float rad = angleDeg * Mathf.Deg2Rad;
// Local direction (right = +X, forward = +Z)
Vector3 localDir = new Vector3(Mathf.Cos(rad), 0f, Mathf.Sin(rad)).normalized;
// Position: small offset from center (or larger ring for initial seed)
float radius = useRing ? seedRingRadius : spawnRadialOffset;
Vector3 pos = spawnPoint.TransformPoint(localDir * radius);
SpawnOneAt(pos, spawnPoint.TransformDirection(localDir));
}
void SpawnOneAt(Vector3 pos, Vector3 lateralDir)
{
var t = PickWeightedType();
2025-09-06 00:08:22 +05:00
spawnPointLid.transform.DOLocalMoveY(-0.5f, 0.3f).OnComplete(() =>
{
var go = Instantiate(bubblePrefab, pos, Quaternion.identity);
spawnPointLid.transform.DOLocalMoveY(0, 0.3f).SetEase(Ease.OutBack).SetDelay(0.5f);
live.Add(go);
2025-09-06 00:08:22 +05:00
go.AddComponent<_SpawnRegistration>().Init(this);
2025-09-06 00:08:22 +05:00
var cb = go.GetComponent<ClawBubble>();
if (cb != null) cb.type = t;
cb.bottomMeshRenderer.material = bubbleTypeMaterials[(int)t];
var rb = go.GetComponent<Rigidbody>();
if (rb != null)
{
// If a lateral direction was provided, use it; otherwise random
Vector3 lateral = lateralDir != Vector3.zero
? lateralDir.normalized * radialImpulse
: new Vector3(Random.Range(-0.5f, 0.5f), 0f, Random.Range(-0.5f, 0.5f)).normalized * radialImpulse;
Vector3 impulse = Vector3.up * riseImpulse + lateral;
rb.AddForce(impulse, ForceMode.VelocityChange);
rb.detectCollisions = true;
}
});
}
// ----- Angle picking / separation helpers -----
float PickClearAngle(float startDeg, float minSepDeg, int tries)
{
if (minSepDeg <= 0f || live.Count == 0) return startDeg;
float a = startDeg;
for (int i = 0; i < tries; i++)
{
if (IsAngleClear(a, minSepDeg)) return a;
a = Mathf.Repeat(a + goldenAngleDeg, 360f);
}
return a; // fallback
}
bool IsAngleClear(float angleDeg, float minSepDeg)
{
float half = minSepDeg * 0.5f;
Vector3 center = spawnPoint.position;
foreach (var g in live)
{
if (!g) continue;
Vector3 v = g.transform.position - center;
v.y = 0f;
if (v.sqrMagnitude < 0.0001f) continue;
float ang = Mathf.Atan2(v.z, v.x) * Mathf.Rad2Deg; // project to XZ (note: x/z order)
float delta = Mathf.DeltaAngle(angleDeg, ang);
if (Mathf.Abs(delta) < half) return false;
}
return true;
}
// ----- Non-radial fallback placement (distance-based) -----
bool TryFindSpawnPos(out Vector3 pos)
{
for (int i = 0; i < maxPlacementTries; i++)
{
var off = Random.insideUnitCircle * Mathf.Max(spawnRadialOffset, minSpawnSpacing * 0.6f);
var p = spawnPoint.position + new Vector3(off.x, 0f, off.y);
if (IsPositionClear(p, minSpawnSpacing))
{
pos = p;
return true;
}
}
// Fallback push along any direction
var dir = Random.insideUnitCircle.normalized;
pos = spawnPoint.position + new Vector3(dir.x, 0f, dir.y) * Mathf.Max(spawnRadialOffset, minSpawnSpacing);
return true;
}
bool IsPositionClear(Vector3 p, float spacing)
{
float r2 = spacing * spacing;
foreach (var g in live)
{
if (!g) continue;
if ((g.transform.position - p).sqrMagnitude < r2) return false;
}
return !Physics.CheckSphere(p, spacing * 0.5f, ballLayerMask, QueryTriggerInteraction.Ignore);
}
// ----- Odds -----
ClawBubbleType PickWeightedType()
{
if (weights == null || weights.Count == 0) return ClawBubbleType.Wolf;
float total = 0f;
foreach (var w in weights) if (w.weight > 0f) total += w.weight;
if (total <= 0f) return ClawBubbleType.Wolf;
float r = Random.value * total, acc = 0f;
foreach (var w in weights)
{
if (w.weight <= 0f) continue;
acc += w.weight;
if (r <= acc) return w.type;
}
return weights[weights.Count - 1].type;
}
private class _SpawnRegistration : MonoBehaviour
{
private ClawBubbleSpawner mgr;
public void Init(ClawBubbleSpawner m) { mgr = m; }
void OnDestroy() { if (mgr) mgr.live.Remove(gameObject); }
}
}