diff --git a/Game/Scripts/Enemy/SteamEnemyController.cs b/Game/Scripts/Enemy/SteamEnemyController.cs index c6a7b77..d679788 100644 --- a/Game/Scripts/Enemy/SteamEnemyController.cs +++ b/Game/Scripts/Enemy/SteamEnemyController.cs @@ -38,9 +38,11 @@ namespace MegaKoop.Game.Enemy [SerializeField] private SteamNetworkTransform networkTransform; private SteamCoopNetworkManager networkManager; - private Transform currentTarget; + private Transform currentTargetTransform; + private NetworkIdentity currentTargetIdentity; private Health currentTargetHealth; private Vector3 lastTargetPosition; + private Vector3 lastKnownTargetPosition; private float retargetTimer; private Vector3 spawnPosition; private bool cachedNavAgentState; @@ -83,6 +85,7 @@ namespace MegaKoop.Game.Enemy } lastTargetPosition = Vector3.positiveInfinity; + lastKnownTargetPosition = spawnPosition; retargetTimer = Random.Range(0f, Mathf.Max(0.05f, retargetInterval)); } @@ -121,7 +124,7 @@ namespace MegaKoop.Game.Enemy AcquireTarget(); } - if (currentTarget == null) + if (!TryGetTargetPosition(out _)) { HandleIdle(); return; @@ -164,7 +167,7 @@ namespace MegaKoop.Game.Enemy private bool IsTargetValid() { - if (currentTarget == null) + if (currentTargetTransform == null && currentTargetIdentity == null && currentTargetHealth == null) { return false; } @@ -174,7 +177,12 @@ namespace MegaKoop.Game.Enemy return false; } - float sqrDistance = (currentTarget.position - transform.position).sqrMagnitude; + if (!TryGetTargetPosition(out Vector3 targetPosition)) + { + return false; + } + + float sqrDistance = (targetPosition - transform.position).sqrMagnitude; if (detectionRadius > 0f && sqrDistance > detectionRadius * detectionRadius) { return false; @@ -182,7 +190,7 @@ namespace MegaKoop.Game.Enemy if (leashDistance > 0f) { - float sqrLeash = (currentTarget.position - spawnPosition).sqrMagnitude; + float sqrLeash = (targetPosition - spawnPosition).sqrMagnitude; if (sqrLeash > leashDistance * leashDistance) { return false; @@ -195,8 +203,7 @@ namespace MegaKoop.Game.Enemy private void AcquireTarget() { retargetTimer = retargetInterval; - currentTarget = null; - currentTargetHealth = null; + ClearTarget(); float bestScore = float.MaxValue; SharedHealthBuffer.Clear(); @@ -215,7 +222,18 @@ namespace MegaKoop.Game.Enemy } Transform candidateTransform = candidate.transform; - float sqrDistance = (candidateTransform.position - transform.position).sqrMagnitude; + NetworkIdentity candidateIdentity = candidate.GetComponent(); + Vector3 candidatePosition; + if (candidateIdentity != null && SteamCharacterStateCache.TryGetState(candidateIdentity.NetworkId, out var state)) + { + candidatePosition = state.Position; + } + else + { + candidatePosition = candidateTransform.position; + } + + float sqrDistance = (candidatePosition - transform.position).sqrMagnitude; if (detectionRadius > 0f && sqrDistance > detectionRadius * detectionRadius) { continue; @@ -224,12 +242,15 @@ namespace MegaKoop.Game.Enemy if (sqrDistance < bestScore) { bestScore = sqrDistance; - currentTarget = candidateTransform; + currentTargetTransform = candidateTransform; + currentTargetIdentity = candidateIdentity; currentTargetHealth = candidate; + lastKnownTargetPosition = candidatePosition; + lastTargetPosition = Vector3.positiveInfinity; } } - if (currentTarget == null) + if (currentTargetTransform == null && currentTargetIdentity == null) { lastTargetPosition = Vector3.positiveInfinity; } @@ -239,7 +260,11 @@ namespace MegaKoop.Game.Enemy private void TickMovement(float deltaTime) { - Vector3 targetPosition = currentTarget.position; + if (!TryGetTargetPosition(out Vector3 targetPosition)) + { + ClearTarget(); + return; + } if (navMeshAgent != null && navMeshAgent.enabled && navMeshAgent.isOnNavMesh) { @@ -255,6 +280,7 @@ namespace MegaKoop.Game.Enemy else { ManualMove(targetPosition, deltaTime); + lastTargetPosition = targetPosition; } } @@ -278,7 +304,12 @@ namespace MegaKoop.Game.Enemy private void FaceTarget(float deltaTime) { - Vector3 toTarget = currentTarget.position - transform.position; + if (!TryGetTargetPosition(out Vector3 targetPosition)) + { + return; + } + + Vector3 toTarget = targetPosition - transform.position; toTarget.y = 0f; if (toTarget.sqrMagnitude < 0.0001f) { @@ -314,6 +345,47 @@ namespace MegaKoop.Game.Enemy } } + private bool TryGetTargetPosition(out Vector3 position) + { + if (currentTargetIdentity != null && SteamCharacterStateCache.TryGetState(currentTargetIdentity.NetworkId, out var state)) + { + position = state.Position; + lastKnownTargetPosition = position; + return true; + } + + if (currentTargetTransform != null) + { + position = currentTargetTransform.position; + lastKnownTargetPosition = position; + return true; + } + + if (currentTargetHealth != null) + { + position = currentTargetHealth.transform.position; + lastKnownTargetPosition = position; + return true; + } + + if (lastKnownTargetPosition != Vector3.positiveInfinity) + { + position = lastKnownTargetPosition; + return true; + } + + position = Vector3.zero; + return false; + } + + private void ClearTarget() + { + currentTargetTransform = null; + currentTargetIdentity = null; + currentTargetHealth = null; + lastKnownTargetPosition = Vector3.positiveInfinity; + } + private void FacePoint(Vector3 point, float deltaTime) { Vector3 toPoint = point - transform.position; diff --git a/Game/Scripts/Networking/NetworkIdRegistry.cs b/Game/Scripts/Networking/NetworkIdRegistry.cs index a67bdf4..48b0493 100644 --- a/Game/Scripts/Networking/NetworkIdRegistry.cs +++ b/Game/Scripts/Networking/NetworkIdRegistry.cs @@ -109,6 +109,8 @@ namespace MegaKoop.Game.Networking Debug.Log($"[NetworkIdRegistry] Unregistered Network ID {networkId}"); } + SteamCharacterStateCache.RemoveState(networkId); + // Also remove from Steam ID mapping if present ulong steamIdToRemove = 0; foreach (var kvp in instance.steamIdToNetworkId) diff --git a/Game/Scripts/Networking/SteamCharacterNetworkBridge.cs b/Game/Scripts/Networking/SteamCharacterNetworkBridge.cs index 68a1072..16d8efe 100644 --- a/Game/Scripts/Networking/SteamCharacterNetworkBridge.cs +++ b/Game/Scripts/Networking/SteamCharacterNetworkBridge.cs @@ -211,6 +211,7 @@ namespace MegaKoop.Game.Networking var unityController = GetComponent(); Vector3 velocity = unityController != null ? unityController.velocity : Vector3.zero; + SteamCharacterStateCache.ReportLocalState(identity.NetworkId, rootTransform.position, rootTransform.rotation, velocity); var message = new CharacterTransformMessage(identity.NetworkId, rootTransform.position, rootTransform.rotation, velocity); byte[] payload = CharacterTransformMessage.Serialize(message); networkManager.SendToAll(NetworkMessageType.CharacterTransform, payload, EP2PSend.k_EP2PSendUnreliableNoDelay); diff --git a/Game/Scripts/Networking/SteamCharacterStateCache.cs b/Game/Scripts/Networking/SteamCharacterStateCache.cs new file mode 100644 index 0000000..29eb66e --- /dev/null +++ b/Game/Scripts/Networking/SteamCharacterStateCache.cs @@ -0,0 +1,176 @@ +using System.Collections.Generic; +using Steamworks; +using UnityEngine; + +namespace MegaKoop.Game.Networking +{ + /// + /// Lightweight cache of the latest character transform messages so systems (AI, prediction) can query peer positions. + /// + [DefaultExecutionOrder(-900)] + public class SteamCharacterStateCache : MonoBehaviour + { + public readonly struct CharacterState + { + public readonly int NetworkId; + public readonly Vector3 Position; + public readonly Quaternion Rotation; + public readonly Vector3 Velocity; + public readonly float LastUpdateTime; + public readonly ulong SourceSteamId; + + public CharacterState(int networkId, Vector3 position, Quaternion rotation, Vector3 velocity, float timestamp, ulong steamId) + { + NetworkId = networkId; + Position = position; + Rotation = rotation; + Velocity = velocity; + LastUpdateTime = timestamp; + SourceSteamId = steamId; + } + } + + private static SteamCharacterStateCache instance; + + private readonly Dictionary states = new(); + private SteamCoopNetworkManager networkManager; + private bool isRegistered; + + public static SteamCharacterStateCache Instance + { + get + { + if (instance == null) + { + var go = new GameObject("SteamCharacterStateCache"); + instance = go.AddComponent(); + DontDestroyOnLoad(go); + } + + return instance; + } + } + + private void Awake() + { + if (instance != null && instance != this) + { + Destroy(gameObject); + return; + } + + instance = this; + DontDestroyOnLoad(gameObject); + } + + private void OnEnable() + { + EnsureSubscription(); + } + + private void Update() + { + EnsureSubscription(); + } + + private void OnDisable() + { + Unsubscribe(); + } + + private void EnsureSubscription() + { + var current = SteamCoopNetworkManager.Instance; + if (current == networkManager) + { + if (networkManager != null && !isRegistered) + { + networkManager.RegisterHandler(NetworkMessageType.CharacterTransform, HandleCharacterTransformMessage); + isRegistered = true; + } + return; + } + + Unsubscribe(); + networkManager = current; + + if (networkManager != null) + { + networkManager.RegisterHandler(NetworkMessageType.CharacterTransform, HandleCharacterTransformMessage); + isRegistered = true; + } + } + + private void Unsubscribe() + { + if (networkManager != null && isRegistered) + { + networkManager.UnregisterHandler(NetworkMessageType.CharacterTransform, HandleCharacterTransformMessage); + isRegistered = false; + } + } + + private void HandleCharacterTransformMessage(NetworkMessage message) + { + CharacterTransformMessage transformMessage = CharacterTransformMessage.Deserialize(message.Payload); + StoreState(transformMessage.NetworkId, transformMessage.Position, transformMessage.Rotation, transformMessage.Velocity, message.Sender); + } + + private void StoreState(int networkId, Vector3 position, Quaternion rotation, Vector3 velocity, ulong steamId) + { + if (networkId == 0) + { + return; + } + + states[networkId] = new CharacterState(networkId, position, rotation, velocity, Time.time, steamId); + } + + private static ulong GetLocalSteamId() + { +#if STEAMWORKSNET + if (MegaKoop.Steam.SteamManager.Initialized) + { + return SteamUser.GetSteamID().m_SteamID; + } +#else + if (SteamBootstrap.IsInitialized) + { + return SteamUser.GetSteamID().m_SteamID; + } +#endif + return 0UL; + } + + public static bool TryGetState(int networkId, out CharacterState state) + { + if (Instance.states.TryGetValue(networkId, out state)) + { + return true; + } + + state = default; + return false; + } + + public static void ReportLocalState(int networkId, Vector3 position, Quaternion rotation, Vector3 velocity) + { + if (networkId == 0) + { + return; + } + + Instance.StoreState(networkId, position, rotation, velocity, GetLocalSteamId()); + } + + public static void RemoveState(int networkId) + { + if (networkId == 0) + { + return; + } + + Instance.states.Remove(networkId); + } + } +} diff --git a/Game/Scripts/Networking/SteamNetworkTransform.cs b/Game/Scripts/Networking/SteamNetworkTransform.cs index 4f99029..8d07dd4 100644 --- a/Game/Scripts/Networking/SteamNetworkTransform.cs +++ b/Game/Scripts/Networking/SteamNetworkTransform.cs @@ -152,9 +152,10 @@ namespace MegaKoop.Game.Networking } else if (trackedRigidbody != null) { - velocity = trackedRigidbody.linearVelocity; + velocity = trackedRigidbody.velocity; } + SteamCharacterStateCache.ReportLocalState(identity.NetworkId, targetTransform.position, targetTransform.rotation, velocity); var message = new CharacterTransformMessage(identity.NetworkId, targetTransform.position, targetTransform.rotation, velocity); byte[] payload = CharacterTransformMessage.Serialize(message); networkManager.SendToAll(NetworkMessageType.CharacterTransform, payload, EP2PSend.k_EP2PSendUnreliableNoDelay);