109 lines
3.8 KiB
C#
109 lines
3.8 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Fusion;
|
||
using UnityEngine;
|
||
|
||
namespace TPSBR
|
||
{
|
||
public sealed class LootBoxSpawner : NetworkBehaviour
|
||
{
|
||
[Header("Prefab")]
|
||
[SerializeField] private NetworkPrefabRef _itemBoxPrefab;
|
||
|
||
[Header("Placement")]
|
||
[Min(1)] [SerializeField] private int _count = 10;
|
||
[Min(0f)] [SerializeField] private float _radius = 40f;
|
||
[Min(0f)] [SerializeField] private float _minSpacing = 3f;
|
||
[SerializeField] private LayerMask _groundMask = ~0;
|
||
[SerializeField] private float _raycastUp = 60f;
|
||
[SerializeField] private float _raycastDown = 120f;
|
||
[SerializeField] private uint _seed = 12345;
|
||
[SerializeField] private bool _randomYaw = true;
|
||
|
||
// Keep references to despawn/respawn later if you extend it
|
||
private readonly List<NetworkObject> _spawned = new();
|
||
|
||
public override void Spawned()
|
||
{
|
||
if (!HasStateAuthority) return;
|
||
SpawnAll();
|
||
}
|
||
|
||
private void SpawnAll()
|
||
{
|
||
// If this is re-entered (e.g., soft reset), clean previous
|
||
foreach (var no in _spawned)
|
||
if (no && no.IsValid) Runner.Despawn(no);
|
||
_spawned.Clear();
|
||
|
||
var rnd = new System.Random(unchecked((int)_seed));
|
||
var positions = SamplePositions(transform.position, _radius, _count, _minSpacing, rnd);
|
||
|
||
foreach (var p in positions)
|
||
{
|
||
var rot = _randomYaw ? Quaternion.Euler(0f, (float)rnd.NextDouble() * 360f, 0f) : Quaternion.identity;
|
||
// Spawn server-side; replicated to all clients
|
||
var obj = Runner.Spawn(_itemBoxPrefab, p, rot);
|
||
_spawned.Add(obj);
|
||
}
|
||
}
|
||
|
||
// Deterministic sampling inside a circle with ground projection + spacing
|
||
private List<Vector3> SamplePositions(Vector3 center, float radius, int count, float minSpacing, System.Random rnd)
|
||
{
|
||
var results = new List<Vector3>(count);
|
||
var maxTrials = 2500; // generous to pack in cluttered areas
|
||
int trials = 0;
|
||
|
||
while (results.Count < count && trials++ < maxTrials)
|
||
{
|
||
// pick point in disc (sqrt for uniform)
|
||
float r = radius * Mathf.Sqrt((float)rnd.NextDouble());
|
||
float ang = Mathf.PI * 2f * (float)rnd.NextDouble();
|
||
Vector3 local = new Vector3(Mathf.Cos(ang) * r, 0f, Mathf.Sin(ang) * r);
|
||
Vector3 rayFrom = center + local + Vector3.up * _raycastUp;
|
||
|
||
if (Physics.Raycast(rayFrom, Vector3.down, out var hit, _raycastUp + _raycastDown, _groundMask, QueryTriggerInteraction.Ignore))
|
||
{
|
||
var pos = hit.point;
|
||
|
||
// spacing test
|
||
bool ok = true;
|
||
if (minSpacing > 0f)
|
||
{
|
||
for (int i = 0; i < results.Count; i++)
|
||
{
|
||
if (Vector3.SqrMagnitude(results[i] - pos) < minSpacing * minSpacing) { ok = false; break; }
|
||
}
|
||
}
|
||
|
||
if (ok) results.Add(pos);
|
||
}
|
||
}
|
||
|
||
if (results.Count < count)
|
||
Debug.LogWarning($"[LootBoxSpawner] Only placed {results.Count}/{count}. Increase radius or lower spacing.");
|
||
|
||
return results;
|
||
}
|
||
|
||
// Scene gizmo previews the exact deterministic layout you’ll get at runtime
|
||
private void OnDrawGizmosSelected()
|
||
{
|
||
Gizmos.color = new Color(0.1f, 0.8f, 1f, 0.25f);
|
||
Gizmos.DrawWireSphere(transform.position, _radius);
|
||
|
||
// preview sample using same seed
|
||
var rnd = new System.Random(unchecked((int)_seed));
|
||
var preview = SamplePositions(transform.position, _radius, _count, _minSpacing, rnd);
|
||
|
||
Gizmos.color = new Color(0.1f, 0.8f, 1f, 0.7f);
|
||
foreach (var p in preview)
|
||
{
|
||
Gizmos.DrawSphere(p + Vector3.up * 0.15f, 0.25f);
|
||
Gizmos.DrawLine(p + Vector3.up * 3f, p); // drop line
|
||
}
|
||
}
|
||
}
|
||
}
|