Files
megakoop/Game/Scripts/Vfx/ProjectileImpactVfxService.cs
2025-10-26 14:17:31 +01:00

207 lines
6.1 KiB
C#

using MegaKoop.Game.Networking;
using Steamworks;
using UnityEngine;
namespace MegaKoop.Game.Vfx
{
[DefaultExecutionOrder(-50)]
public class ProjectileImpactVfxService : MonoBehaviour
{
private const float MinLifetime = 0.5f;
private static ProjectileImpactVfxService instance;
private SteamCoopNetworkManager networkManager;
private bool isRegistered;
private ProjectileImpactVfxSettings settings;
public static ProjectileImpactVfxService Instance
{
get
{
if (instance == null)
{
instance = FindObjectOfType<ProjectileImpactVfxService>();
if (instance == null)
{
var go = new GameObject(nameof(ProjectileImpactVfxService));
instance = go.AddComponent<ProjectileImpactVfxService>();
}
}
return instance;
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void AutoCreate()
{
_ = Instance;
}
public static void ReportImpact(ProjectileImpactKind kind, Vector3 position, Vector3 normal)
{
Instance.InternalReportImpact(kind, position, normal);
}
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
LoadSettings();
}
private void OnEnable()
{
TryBindNetworkManager();
}
private void Update()
{
TryBindNetworkManager();
}
private void OnDisable()
{
UnregisterHandler();
}
private void OnDestroy()
{
if (instance == this)
{
instance = null;
}
UnregisterHandler();
}
private void LoadSettings()
{
if (settings != null)
{
return;
}
settings = ProjectileImpactVfxSettings.LoadFromResources();
if (settings == null)
{
Debug.LogWarning($"[ProjectileImpactVfxService] Could not locate Resources/{ProjectileImpactVfxSettings.ResourcePath}. Impact particles will be skipped.");
}
}
private void TryBindNetworkManager()
{
var current = SteamCoopNetworkManager.Instance;
if (current != networkManager)
{
UnregisterHandler();
networkManager = current;
}
if (networkManager != null && !isRegistered)
{
networkManager.RegisterHandler(NetworkMessageType.ProjectileImpact, HandleImpactMessage);
isRegistered = true;
}
}
private void UnregisterHandler()
{
if (isRegistered && networkManager != null)
{
networkManager.UnregisterHandler(NetworkMessageType.ProjectileImpact, HandleImpactMessage);
}
isRegistered = false;
}
private void HandleImpactMessage(NetworkMessage message)
{
ProjectileImpactMessage impact = ProjectileImpactMessage.Deserialize(message.Payload);
SpawnImpact(impact.Kind, impact.Position, impact.Normal);
}
private void InternalReportImpact(ProjectileImpactKind kind, Vector3 position, Vector3 normal)
{
SpawnImpact(kind, position, normal);
TryBindNetworkManager();
if (networkManager == null || !networkManager.IsHost)
{
return;
}
var impactMessage = new ProjectileImpactMessage(kind, position, normal);
byte[] payload = ProjectileImpactMessage.Serialize(impactMessage);
networkManager.SendToAll(NetworkMessageType.ProjectileImpact, payload, EP2PSend.k_EP2PSendUnreliableNoDelay);
}
private void SpawnImpact(ProjectileImpactKind kind, Vector3 position, Vector3 normal)
{
LoadSettings();
if (settings == null)
{
return;
}
GameObject prefab = kind switch
{
ProjectileImpactKind.Hero => settings.HeroImpactPrefab,
ProjectileImpactKind.Enemy => settings.EnemyImpactPrefab,
_ => null
};
if (prefab == null)
{
return;
}
Quaternion rotation = normal.sqrMagnitude > 0.001f ? Quaternion.LookRotation(normal) : Quaternion.identity;
GameObject instance = Instantiate(prefab, position, rotation);
float lifetime = EstimateLifetime(instance);
if (lifetime > 0f)
{
Destroy(instance, lifetime);
}
}
private float EstimateLifetime(GameObject effectInstance)
{
float fallback = settings != null ? settings.FallbackLifetime : 4f;
ParticleSystem[] particleSystems = effectInstance.GetComponentsInChildren<ParticleSystem>();
if (particleSystems == null || particleSystems.Length == 0)
{
return Mathf.Max(MinLifetime, fallback);
}
float maxLifetime = fallback;
foreach (ParticleSystem system in particleSystems)
{
var main = system.main;
if (main.loop)
{
maxLifetime = Mathf.Max(maxLifetime, fallback);
continue;
}
float candidate = main.duration + main.startLifetime.constantMax;
if (candidate <= 0f)
{
candidate = fallback;
}
maxLifetime = Mathf.Max(maxLifetime, candidate);
}
return Mathf.Max(MinLifetime, maxLifetime);
}
}
}