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
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|