using UnityEngine; using System.Collections.Generic; using DG.Tweening; public class ClawBubbleSpawner : MonoBehaviour { public static ClawBubbleSpawner Instance; public GameObject spawnPointLid; public List 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°).")] 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 weights = new List { 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 live = new HashSet(); private float lastSpawnTime; private float nextAngleDeg; // golden-angle progression // Forced types queue (guaranteed next spawns) private readonly Queue _forcedTypes = new Queue(); // Weight bias management private List _weightsOriginal; // === Public helpers for mode managers === public void QueueType(ClawBubbleType t, int count = 1) { for (int i = 0; i < count; i++) _forcedTypes.Enqueue(t); } // Guarantees at least one of 't' exists; spawns immediately (ignores cooldown) if needed public void EnsureTypePresent(ClawBubbleType t) { foreach (var g in live) { if (!g) continue; var b = g.GetComponent(); if (b && b.type == t) return; // already present } _forcedTypes.Enqueue(t); TrySpawn(1, ignoreCooldown: true); } /// /// Temporarily biases spawn odds. Example: (3f, 0.6f) = triple target, 60% others. /// public void ApplyTargetBias(ClawBubbleType target, float targetMult = 3f, float othersMult = 0.6f) { if (_weightsOriginal == null || _weightsOriginal.Count == 0) return; if (weights == null) return; for (int i = 0; i < weights.Count; i++) { var original = _weightsOriginal.Find(w => w.type == weights[i].type); var w = weights[i]; w.weight = (w.type == target) ? Mathf.Max(0f, original.weight * targetMult) : Mathf.Max(0f, original.weight * othersMult); weights[i] = w; } } /// Restore original authoring weights. public void ClearWeightBias() { if (_weightsOriginal == null) return; weights.Clear(); foreach (var w in _weightsOriginal) weights.Add(w); } void Awake() { if (Instance == null) Instance = this; else { Destroy(gameObject); return; } nextAngleDeg = Random.Range(0f, 360f); // Deep copy original weights for later restore _weightsOriginal = new List(weights.Count); foreach (var w in weights) _weightsOriginal.Add(new WeightedType { type = w.type, weight = w.weight }); } 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, bool ignoreCooldown = false) { if (!bubblePrefab || !spawnPoint) return; live.RemoveWhere(g => g == null); for (int i = 0; i < count; i++) { if (live.Count >= maxConcurrentBubbles) break; if (!ignoreCooldown && Time.time - lastSpawnTime < spawnCooldown) break; SpawnOne(); lastSpawnTime = Time.time; } } void SpawnOne() { if (useRadialEjection) { float chosen = PickClearAngle(nextAngleDeg, minAngleSeparationDeg, maxAngleTries); SpawnRadial(chosen, useRing: false); nextAngleDeg = Mathf.Repeat(chosen + goldenAngleDeg, 360f); } else { if (!TryFindSpawnPos(out var pos)) pos = spawnPoint.position; SpawnOneAt(pos, lateralDir: Vector3.zero); } } void SpawnRadial(float angleDeg, bool useRing) { float rad = angleDeg * Mathf.Deg2Rad; Vector3 localDir = new Vector3(Mathf.Cos(rad), 0f, Mathf.Sin(rad)).normalized; 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(); if (spawnPointLid) spawnPointLid.transform.DOLocalMoveY(-0.5f, 0.3f).OnComplete(() => { DoInstantiate(pos, lateralDir, t); spawnPointLid.transform.DOLocalMoveY(0, 0.3f).SetEase(Ease.OutBack).SetDelay(0.5f); }); else { DoInstantiate(pos, lateralDir, t); } } void DoInstantiate(Vector3 pos, Vector3 lateralDir, ClawBubbleType t) { var go = Instantiate(bubblePrefab, pos, Quaternion.identity); live.Add(go); go.AddComponent<_SpawnRegistration>().Init(this); var cb = go.GetComponent(); if (cb != null) { cb.type = t; if (cb.bottomMeshRenderer && bubbleTypeMaterials != null) { int idx = (int)t; if (idx >= 0 && idx < bubbleTypeMaterials.Count && bubbleTypeMaterials[idx] != null) cb.bottomMeshRenderer.material = bubbleTypeMaterials[idx]; } } var rb = go.GetComponent(); if (rb != null) { 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 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 (_forcedTypes.Count > 0) return _forcedTypes.Dequeue(); 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); } } } //// ClawBubbleSpawner.cs //using UnityEngine; //using System.Collections.Generic; //using DG.Tweening; //public class ClawBubbleSpawner : MonoBehaviour //{ // public static ClawBubbleSpawner Instance; // public GameObject spawnPointLid; // public List 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°).")] // 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 weights = new List // { // 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 live = new HashSet(); // private float lastSpawnTime; // private float nextAngleDeg; // used for golden-angle progression // // Internal // private readonly Queue _forcedTypes = new Queue(); // <— NEW // public void QueueType(ClawBubbleType t, int count = 1) // { // for (int i = 0; i < count; i++) _forcedTypes.Enqueue(t); // } // // Guarantees at least one of 't' exists; spawns immediately (ignores cooldown) if needed // public void EnsureTypePresent(ClawBubbleType t) // { // foreach (var g in live) // { // if (!g) continue; // var b = g.GetComponent(); // if (b && b.type == t) return; // already present // } // // ensure one now // _forcedTypes.Enqueue(t); // TrySpawn(1, ignoreCooldown: true); // } // 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, bool ignoreCooldown = false) // { // if (!bubblePrefab || !spawnPoint) return; // live.RemoveWhere(g => g == null); // for (int i = 0; i < count; i++) // { // if (live.Count >= maxConcurrentBubbles) break; // if (!ignoreCooldown && Time.time - lastSpawnTime < spawnCooldown) break; // SpawnOne(); // lastSpawnTime = Time.time; // } // } // //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(); // 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); // go.AddComponent<_SpawnRegistration>().Init(this); // var cb = go.GetComponent(); // if (cb != null) cb.type = t; // cb.bottomMeshRenderer.material = bubbleTypeMaterials[(int)t]; // var rb = go.GetComponent(); // 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 (_forcedTypes.Count > 0) return _forcedTypes.Dequeue(); // <— NEW // 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; // } // //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); } // } //}