using System.Collections.Generic; using Unity.Netcode; using UnityEngine; using UnityEngine.Events; [DisallowMultipleComponent] public sealed class NetworkShockwaveEmitter : NetworkBehaviour { private const int MaximumOverlaps = 256; [SerializeField] private ShockwaveProfile profile; [SerializeField] private Transform origin; [SerializeField] private LayerMask physicsLayers = ~0; [SerializeField] private UnityEvent shockwaveEmitted; private readonly NetworkVariable sequence = new( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly Collider[] overlapBuffer = new Collider[MaximumOverlaps]; private readonly HashSet affectedBodies = new(); public ShockwaveProfile Profile => profile; public Vector3 Origin => origin != null ? origin.position : transform.position; public uint Sequence => sequence.Value; public event System.Action Emitted; public override void OnNetworkSpawn() { sequence.OnValueChanged += HandleSequenceChanged; } public override void OnNetworkDespawn() { sequence.OnValueChanged -= HandleSequenceChanged; } public bool EmitShockwave() => EmitShockwave(Origin); public bool EmitShockwave(Vector3 worldOrigin) { if (!IsServer || !IsSpawned || profile == null) return false; ApplyToPlayers(worldOrigin); ApplyToPhysics(worldOrigin); sequence.Value++; return true; } [ContextMenu("Debug/Emit Shockwave (Server Play Mode)")] private void EmitFromContextMenu() { if (Application.isPlaying) EmitShockwave(); } private void ApplyToPlayers(Vector3 worldOrigin) { foreach (PlayerImpactController player in FindObjectsByType(FindObjectsSortMode.None)) { if (!player.IsSpawned) continue; Vector3 offset = player.transform.position - worldOrigin; float distance = offset.magnitude; if (!profile.TryEvaluate(distance, out ShockwaveZone zone)) continue; Vector3 direction = CreateDirection(offset, zone.UpwardBias); player.ApplyImpactOnServer(direction * zone.PlayerImpulse, zone); } } private void ApplyToPhysics(Vector3 worldOrigin) { int count = Physics.OverlapSphereNonAlloc( worldOrigin, profile.MaximumRadius, overlapBuffer, physicsLayers, QueryTriggerInteraction.Ignore); affectedBodies.Clear(); for (int index = 0; index < count; index++) { Collider hit = overlapBuffer[index]; Rigidbody body = hit != null ? hit.attachedRigidbody : null; if (body == null || body.isKinematic || !affectedBodies.Add(body) || body.GetComponentInParent() != null) { continue; } Vector3 offset = body.worldCenterOfMass - worldOrigin; if (!profile.TryEvaluate(offset.magnitude, out ShockwaveZone zone) || zone.RigidbodyImpulse <= 0f) { continue; } body.AddForce( CreateDirection(offset, zone.UpwardBias) * zone.RigidbodyImpulse, ForceMode.Impulse); } } private static Vector3 CreateDirection(Vector3 offset, float upwardBias) { if (offset.sqrMagnitude < 0.0001f) offset = Vector3.up; offset.Normalize(); return (offset + Vector3.up * Mathf.Clamp01(upwardBias)).normalized; } private void HandleSequenceChanged(uint previous, uint current) { if (current == previous) return; Emitted?.Invoke(); shockwaveEmitted?.Invoke(); } }