diff --git a/Game/Enemy/Golem/Golem.prefab b/Game/Enemy/Golem/Golem.prefab
index 401a70a..f06c9ad 100644
--- a/Game/Enemy/Golem/Golem.prefab
+++ b/Game/Enemy/Golem/Golem.prefab
@@ -56,7 +56,7 @@ Transform:
m_GameObject: {fileID: 1070748433161756}
serializedVersion: 2
m_LocalRotation: {x: 0.05427486, y: 0.395974, z: 0.124479234, w: 0.9081651}
- m_LocalPosition: {x: -0.3286817, y: -5.684342e-16, z: 1.4654943e-16}
+ m_LocalPosition: {x: -0.3286817, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
@@ -477,6 +477,13 @@ GameObject:
- component: {fileID: 2197706699938650528}
- component: {fileID: 6042376723608263729}
- component: {fileID: -1978034200425575233}
+ - component: {fileID: 5386150419490112215}
+ - component: {fileID: -2049111374676499537}
+ - component: {fileID: 1868764211993700903}
+ - component: {fileID: -8655886036466571170}
+ - component: {fileID: 4325330991877430748}
+ - component: {fileID: 8191023303709792670}
+ - component: {fileID: -3415813308782895168}
m_Layer: 8
m_Name: Golem
m_TagString: Untagged
@@ -509,6 +516,7 @@ Transform:
- {fileID: 4435288916602084}
- {fileID: 4622407164390344}
- {fileID: 4810071759811394}
+ - {fileID: 8960331849695004692}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!95 &95902137545226380
@@ -649,6 +657,143 @@ MonoBehaviour:
SwitchTransformSpaceWhenParented: 0
Interpolate: 1
SlerpPosition: 0
+--- !u!114 &5386150419490112215
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1170732337855516}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 1e6f99ca4475ead269829ba672213d5c, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::MegaKoop.Game.Networking.NetworkIdentity
+ networkId: 0
+ assignOnAwake: 1
+--- !u!114 &-2049111374676499537
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1170732337855516}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 88efaaed4187a0ee8ad63b619f65e970, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::MegaKoop.Game.Networking.SteamNetworkTransform
+ targetTransform: {fileID: 0}
+ identity: {fileID: 0}
+ trackedRigidbody: {fileID: 0}
+ trackedNavMeshAgent: {fileID: 0}
+ authorityIsHost: 1
+ simulateWhenDisconnected: 1
+ broadcastInterval: 0.05
+ remoteLerpSpeed: 12
+ teleportThreshold: 4
+--- !u!114 &1868764211993700903
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1170732337855516}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: e6e77e83dcc282364afb3428637c723c, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::MegaKoop.Game.WeaponSystem.WeaponController
+ ownerTeam: 2
+ weaponSocket: {fileID: 0}
+ acquisitionRadius: 30
+ retargetInterval: 0.25
+ targetMask:
+ serializedVersion: 2
+ m_Bits: 499
+ lineOfSightMask:
+ serializedVersion: 2
+ m_Bits: 499
+ requireLineOfSight: 1
+ startingWeapons:
+ - {fileID: 11400000, guid: 912de7c15ecf9d38d9be364bc0ac0f70, type: 2}
+ autoFire: 1
+--- !u!114 &-8655886036466571170
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1170732337855516}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 44c91a3744d2af1e4b9ee0f82451691a, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::MegaKoop.Game.Enemy.SteamEnemyController
+ moveSpeed: 3.5
+ turnSpeed: 6
+ stoppingDistance: 4
+ detectionRadius: 30
+ leashDistance: 60
+ retargetInterval: 0.4
+ repathThreshold: 1.5
+ returnToSpawnWhenIdle: 1
+ navMeshAgent: {fileID: -3415813308782895168}
+ weaponController: {fileID: 1868764211993700903}
+ health: {fileID: 6003919833639142508}
+ identity: {fileID: 5386150419490112215}
+ networkTransform: {fileID: -2049111374676499537}
+--- !u!114 &4325330991877430748
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1170732337855516}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: dad61aa7b24bddb6b9add5a461263779, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::MegaKoop.Game.Networking.SteamWeaponNetworkBridge
+ weaponController: {fileID: 1868764211993700903}
+ identity: {fileID: 5386150419490112215}
+ disableLocalFiringWhenClient: 1
+--- !u!114 &8191023303709792670
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1170732337855516}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a55005e32d7c7fdd89d4c15872d0466d, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::MegaKoop.Game.Networking.SteamHealthNetworkBridge
+ health: {fileID: 6003919833639142508}
+ identity: {fileID: 5386150419490112215}
+--- !u!195 &-3415813308782895168
+NavMeshAgent:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1170732337855516}
+ m_Enabled: 1
+ m_AgentTypeID: 0
+ m_Radius: 0.5
+ m_Speed: 3.5
+ m_Acceleration: 8
+ avoidancePriority: 50
+ m_AngularSpeed: 120
+ m_StoppingDistance: 0
+ m_AutoTraverseOffMeshLink: 1
+ m_AutoBraking: 1
+ m_AutoRepath: 1
+ m_Height: 2
+ m_BaseOffset: 0
+ m_WalkableMask: 4294967295
+ m_ObstacleAvoidanceType: 4
--- !u!1 &1186076032050976
GameObject:
m_ObjectHideFlags: 0
@@ -3291,3 +3436,34 @@ Transform:
- {fileID: 4359018392181336}
m_Father: {fileID: 4923149105459994}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &5677012161785445265
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 8960331849695004692}
+ m_Layer: 0
+ m_Name: weaponSocket
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &8960331849695004692
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5677012161785445265}
+ serializedVersion: 2
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0.37696, y: 0, z: -17.87976}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 4526463827110206}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
diff --git a/Game/Scripts/Enemy.meta b/Game/Scripts/Enemy.meta
new file mode 100644
index 0000000..6e064ad
--- /dev/null
+++ b/Game/Scripts/Enemy.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: aef1c1cc20669f95d8a2ccf0c0b3e6f5
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Game/Scripts/Enemy/SteamEnemyController.cs b/Game/Scripts/Enemy/SteamEnemyController.cs
new file mode 100644
index 0000000..c6a7b77
--- /dev/null
+++ b/Game/Scripts/Enemy/SteamEnemyController.cs
@@ -0,0 +1,362 @@
+using System.Collections.Generic;
+using MegaKoop.Game.Combat;
+using MegaKoop.Game.Networking;
+using MegaKoop.Game.WeaponSystem;
+using UnityEngine;
+using UnityEngine.AI;
+
+namespace MegaKoop.Game.Enemy
+{
+ ///
+ /// Host-driven enemy brain that chases the closest hero, fires using the shared weapon system,
+ /// and mirrors movement state to peers through SteamCoopNetworkManager.
+ ///
+ [DisallowMultipleComponent]
+ [RequireComponent(typeof(NetworkIdentity))]
+ [RequireComponent(typeof(Health))]
+ [RequireComponent(typeof(SteamNetworkTransform))]
+ [RequireComponent(typeof(WeaponController))]
+ public class SteamEnemyController : MonoBehaviour
+ {
+ private static readonly List SharedHealthBuffer = new(32);
+
+ [Header("Movement")]
+ [SerializeField] private float moveSpeed = 3.5f;
+ [SerializeField] private float turnSpeed = 6f;
+ [SerializeField] private float stoppingDistance = 4f;
+ [SerializeField] private float detectionRadius = 30f;
+ [SerializeField] private float leashDistance = 60f;
+ [SerializeField] private float retargetInterval = 0.4f;
+ [SerializeField] private float repathThreshold = 1.5f;
+ [SerializeField] private bool returnToSpawnWhenIdle = true;
+
+ [Header("References")]
+ [SerializeField] private NavMeshAgent navMeshAgent;
+ [SerializeField] private WeaponController weaponController;
+ [SerializeField] private Health health;
+ [SerializeField] private NetworkIdentity identity;
+ [SerializeField] private SteamNetworkTransform networkTransform;
+
+ private SteamCoopNetworkManager networkManager;
+ private Transform currentTarget;
+ private Health currentTargetHealth;
+ private Vector3 lastTargetPosition;
+ private float retargetTimer;
+ private Vector3 spawnPosition;
+ private bool cachedNavAgentState;
+
+ private void Awake()
+ {
+ spawnPosition = transform.position;
+
+ if (navMeshAgent == null)
+ {
+ navMeshAgent = GetComponent();
+ }
+
+ if (weaponController == null)
+ {
+ weaponController = GetComponent();
+ }
+
+ if (health == null)
+ {
+ health = GetComponent();
+ }
+
+ if (identity == null)
+ {
+ identity = GetComponent();
+ }
+
+ if (networkTransform == null)
+ {
+ networkTransform = GetComponent();
+ }
+
+ if (navMeshAgent != null)
+ {
+ navMeshAgent.updateRotation = false;
+ navMeshAgent.stoppingDistance = stoppingDistance;
+ navMeshAgent.speed = moveSpeed;
+ cachedNavAgentState = navMeshAgent.enabled;
+ }
+
+ lastTargetPosition = Vector3.positiveInfinity;
+ retargetTimer = Random.Range(0f, Mathf.Max(0.05f, retargetInterval));
+ }
+
+ private void OnEnable()
+ {
+ networkManager = SteamCoopNetworkManager.Instance;
+ SyncNavMeshAgentState(ShouldSimulate());
+ }
+
+ private void Update()
+ {
+ networkManager ??= SteamCoopNetworkManager.Instance;
+
+ bool simulate = ShouldSimulate();
+ SyncNavMeshAgentState(simulate);
+
+ if (weaponController != null)
+ {
+ weaponController.enabled = simulate;
+ }
+
+ if (!simulate)
+ {
+ return;
+ }
+
+ if (health != null && !health.IsAlive)
+ {
+ StopAgentMovement();
+ return;
+ }
+
+ retargetTimer -= Time.deltaTime;
+ if (!IsTargetValid() || retargetTimer <= 0f)
+ {
+ AcquireTarget();
+ }
+
+ if (currentTarget == null)
+ {
+ HandleIdle();
+ return;
+ }
+
+ TickMovement(Time.deltaTime);
+ FaceTarget(Time.deltaTime);
+ }
+
+ private bool ShouldSimulate()
+ {
+ if (networkManager == null)
+ {
+ return true;
+ }
+
+ if (!networkManager.IsConnected)
+ {
+ return true;
+ }
+
+ return networkManager.IsHost;
+ }
+
+ private void SyncNavMeshAgentState(bool simulate)
+ {
+ if (navMeshAgent == null)
+ {
+ return;
+ }
+
+ if (cachedNavAgentState == simulate)
+ {
+ return;
+ }
+
+ navMeshAgent.enabled = simulate;
+ cachedNavAgentState = simulate;
+ }
+
+ private bool IsTargetValid()
+ {
+ if (currentTarget == null)
+ {
+ return false;
+ }
+
+ if (currentTargetHealth != null && !currentTargetHealth.IsAlive)
+ {
+ return false;
+ }
+
+ float sqrDistance = (currentTarget.position - transform.position).sqrMagnitude;
+ if (detectionRadius > 0f && sqrDistance > detectionRadius * detectionRadius)
+ {
+ return false;
+ }
+
+ if (leashDistance > 0f)
+ {
+ float sqrLeash = (currentTarget.position - spawnPosition).sqrMagnitude;
+ if (sqrLeash > leashDistance * leashDistance)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private void AcquireTarget()
+ {
+ retargetTimer = retargetInterval;
+ currentTarget = null;
+ currentTargetHealth = null;
+ float bestScore = float.MaxValue;
+
+ SharedHealthBuffer.Clear();
+ SharedHealthBuffer.AddRange(FindObjectsOfType(false));
+
+ foreach (Health candidate in SharedHealthBuffer)
+ {
+ if (candidate == null || candidate == health)
+ {
+ continue;
+ }
+
+ if (!candidate.IsAlive || candidate.Team != Team.Heroes)
+ {
+ continue;
+ }
+
+ Transform candidateTransform = candidate.transform;
+ float sqrDistance = (candidateTransform.position - transform.position).sqrMagnitude;
+ if (detectionRadius > 0f && sqrDistance > detectionRadius * detectionRadius)
+ {
+ continue;
+ }
+
+ if (sqrDistance < bestScore)
+ {
+ bestScore = sqrDistance;
+ currentTarget = candidateTransform;
+ currentTargetHealth = candidate;
+ }
+ }
+
+ if (currentTarget == null)
+ {
+ lastTargetPosition = Vector3.positiveInfinity;
+ }
+
+ SharedHealthBuffer.Clear();
+ }
+
+ private void TickMovement(float deltaTime)
+ {
+ Vector3 targetPosition = currentTarget.position;
+
+ if (navMeshAgent != null && navMeshAgent.enabled && navMeshAgent.isOnNavMesh)
+ {
+ navMeshAgent.speed = moveSpeed;
+ navMeshAgent.stoppingDistance = stoppingDistance;
+
+ if (!navMeshAgent.hasPath || (targetPosition - lastTargetPosition).sqrMagnitude >= repathThreshold * repathThreshold)
+ {
+ navMeshAgent.SetDestination(targetPosition);
+ lastTargetPosition = targetPosition;
+ }
+ }
+ else
+ {
+ ManualMove(targetPosition, deltaTime);
+ }
+ }
+
+ private void ManualMove(Vector3 targetPosition, float deltaTime)
+ {
+ Vector3 currentPosition = transform.position;
+ Vector3 direction = targetPosition - currentPosition;
+ direction.y = 0f;
+ float distance = direction.magnitude;
+
+ if (distance <= Mathf.Max(0.1f, stoppingDistance))
+ {
+ return;
+ }
+
+ Vector3 normalized = direction / distance;
+ float step = moveSpeed * deltaTime;
+ Vector3 newPosition = currentPosition + normalized * Mathf.Min(step, distance - stoppingDistance);
+ transform.position = newPosition;
+ }
+
+ private void FaceTarget(float deltaTime)
+ {
+ Vector3 toTarget = currentTarget.position - transform.position;
+ toTarget.y = 0f;
+ if (toTarget.sqrMagnitude < 0.0001f)
+ {
+ return;
+ }
+
+ Quaternion desiredRotation = Quaternion.LookRotation(toTarget.normalized, Vector3.up);
+ transform.rotation = Quaternion.Slerp(transform.rotation, desiredRotation, Mathf.Clamp01(turnSpeed * deltaTime));
+ }
+
+ private void HandleIdle()
+ {
+ StopAgentMovement();
+
+ if (!returnToSpawnWhenIdle)
+ {
+ return;
+ }
+
+ if ((transform.position - spawnPosition).sqrMagnitude <= 0.25f)
+ {
+ return;
+ }
+
+ if (navMeshAgent != null && navMeshAgent.enabled && navMeshAgent.isOnNavMesh)
+ {
+ navMeshAgent.SetDestination(spawnPosition);
+ }
+ else
+ {
+ ManualMove(spawnPosition, Time.deltaTime);
+ FacePoint(spawnPosition, Time.deltaTime);
+ }
+ }
+
+ private void FacePoint(Vector3 point, float deltaTime)
+ {
+ Vector3 toPoint = point - transform.position;
+ toPoint.y = 0f;
+ if (toPoint.sqrMagnitude < 0.0001f)
+ {
+ return;
+ }
+
+ Quaternion desiredRotation = Quaternion.LookRotation(toPoint.normalized, Vector3.up);
+ transform.rotation = Quaternion.Slerp(transform.rotation, desiredRotation, Mathf.Clamp01(turnSpeed * deltaTime));
+ }
+
+ private void StopAgentMovement()
+ {
+ if (navMeshAgent != null && navMeshAgent.enabled)
+ {
+ navMeshAgent.ResetPath();
+ }
+ }
+
+ private void OnDrawGizmosSelected()
+ {
+ Gizmos.color = new Color(1f, 0.25f, 0f, 0.2f);
+ Gizmos.DrawWireSphere(transform.position, detectionRadius);
+
+ if (leashDistance > 0f)
+ {
+ Gizmos.color = new Color(0.2f, 0.4f, 1f, 0.2f);
+ Vector3 leashCenter = Application.isPlaying ? spawnPosition : transform.position;
+ Gizmos.DrawWireSphere(leashCenter, leashDistance);
+ }
+ }
+
+ private void OnValidate()
+ {
+ moveSpeed = Mathf.Max(0f, moveSpeed);
+ turnSpeed = Mathf.Max(0f, turnSpeed);
+ stoppingDistance = Mathf.Max(0f, stoppingDistance);
+ detectionRadius = Mathf.Max(0f, detectionRadius);
+ leashDistance = Mathf.Max(0f, leashDistance);
+ retargetInterval = Mathf.Max(0.05f, retargetInterval);
+ repathThreshold = Mathf.Max(0.1f, repathThreshold);
+ }
+ }
+}
diff --git a/Game/Scripts/Enemy/SteamEnemyController.cs.meta b/Game/Scripts/Enemy/SteamEnemyController.cs.meta
new file mode 100644
index 0000000..aa07500
--- /dev/null
+++ b/Game/Scripts/Enemy/SteamEnemyController.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 44c91a3744d2af1e4b9ee0f82451691a
\ No newline at end of file
diff --git a/Game/Scripts/Networking/SteamNetworkTransform.cs b/Game/Scripts/Networking/SteamNetworkTransform.cs
new file mode 100644
index 0000000..4f99029
--- /dev/null
+++ b/Game/Scripts/Networking/SteamNetworkTransform.cs
@@ -0,0 +1,216 @@
+using Steamworks;
+using UnityEngine;
+
+namespace MegaKoop.Game.Networking
+{
+ ///
+ /// Host-authoritative transform replication that mirrors state to all peers via Steam P2P messages.
+ ///
+ [DisallowMultipleComponent]
+ public class SteamNetworkTransform : MonoBehaviour
+ {
+ [Header("References")]
+ [SerializeField] private Transform targetTransform;
+ [SerializeField] private NetworkIdentity identity;
+ [SerializeField] private Rigidbody trackedRigidbody;
+ [SerializeField] private UnityEngine.AI.NavMeshAgent trackedNavMeshAgent;
+
+ [Header("Authority")]
+ [SerializeField] private bool authorityIsHost = true;
+ [SerializeField] private bool simulateWhenDisconnected = true;
+
+ [Header("Synchronization")]
+ [SerializeField] private float broadcastInterval = 0.05f;
+ [SerializeField] private float remoteLerpSpeed = 12f;
+ [SerializeField] private float teleportThreshold = 4f;
+
+ private SteamCoopNetworkManager networkManager;
+ private float broadcastTimer;
+ private bool isRegistered;
+ private Vector3 remoteTargetPosition;
+ private Quaternion remoteTargetRotation;
+ private Vector3 remoteTargetVelocity;
+ private bool haveRemoteState;
+
+ private void Awake()
+ {
+ if (targetTransform == null)
+ {
+ targetTransform = transform;
+ }
+
+ if (identity == null)
+ {
+ identity = GetComponent();
+ }
+
+ if (trackedRigidbody == null)
+ {
+ trackedRigidbody = GetComponent();
+ }
+
+ if (trackedNavMeshAgent == null)
+ {
+ trackedNavMeshAgent = GetComponent();
+ }
+
+ remoteTargetPosition = targetTransform.position;
+ remoteTargetRotation = targetTransform.rotation;
+ remoteTargetVelocity = Vector3.zero;
+ broadcastTimer = Random.Range(0f, Mathf.Max(0.01f, broadcastInterval));
+ }
+
+ private void OnEnable()
+ {
+ EnsureNetworkManager();
+ RegisterHandlers();
+ }
+
+ private void OnDisable()
+ {
+ UnregisterHandlers();
+ }
+
+ private void Update()
+ {
+ RefreshNetworkManager();
+
+ if (HasAuthority())
+ {
+ broadcastTimer -= Time.deltaTime;
+ if (broadcastTimer <= 0f)
+ {
+ BroadcastState();
+ broadcastTimer = Mathf.Max(0.01f, broadcastInterval);
+ }
+ }
+ else if (haveRemoteState)
+ {
+ float lerpFactor = Mathf.Clamp01(remoteLerpSpeed * Time.deltaTime);
+ Vector3 currentPosition = targetTransform.position;
+ float sqrDistance = (remoteTargetPosition - currentPosition).sqrMagnitude;
+ if (sqrDistance >= teleportThreshold * teleportThreshold)
+ {
+ targetTransform.SetPositionAndRotation(remoteTargetPosition, remoteTargetRotation);
+ }
+ else
+ {
+ targetTransform.position = Vector3.Lerp(currentPosition, remoteTargetPosition, lerpFactor);
+ targetTransform.rotation = Quaternion.Slerp(targetTransform.rotation, remoteTargetRotation, lerpFactor);
+ }
+ }
+ }
+
+ private void RefreshNetworkManager()
+ {
+ if (networkManager == null)
+ {
+ networkManager = SteamCoopNetworkManager.Instance;
+ if (networkManager != null && isActiveAndEnabled)
+ {
+ RegisterHandlers();
+ }
+ }
+ }
+
+ private void EnsureNetworkManager()
+ {
+ RefreshNetworkManager();
+ }
+
+ private bool HasAuthority()
+ {
+ if (!authorityIsHost)
+ {
+ return true;
+ }
+
+ if (networkManager == null)
+ {
+ return simulateWhenDisconnected;
+ }
+
+ if (!networkManager.IsConnected)
+ {
+ return simulateWhenDisconnected;
+ }
+
+ return networkManager.IsHost;
+ }
+
+ private void BroadcastState()
+ {
+ if (identity == null || targetTransform == null || networkManager == null)
+ {
+ return;
+ }
+
+ Vector3 velocity = Vector3.zero;
+ if (trackedNavMeshAgent != null && trackedNavMeshAgent.enabled)
+ {
+ velocity = trackedNavMeshAgent.velocity;
+ }
+ else if (trackedRigidbody != null)
+ {
+ velocity = trackedRigidbody.linearVelocity;
+ }
+
+ var message = new CharacterTransformMessage(identity.NetworkId, targetTransform.position, targetTransform.rotation, velocity);
+ byte[] payload = CharacterTransformMessage.Serialize(message);
+ networkManager.SendToAll(NetworkMessageType.CharacterTransform, payload, EP2PSend.k_EP2PSendUnreliableNoDelay);
+ }
+
+ private void RegisterHandlers()
+ {
+ if (isRegistered || networkManager == null)
+ {
+ return;
+ }
+
+ networkManager.RegisterHandler(NetworkMessageType.CharacterTransform, HandleTransformMessage);
+ isRegistered = true;
+ }
+
+ private void UnregisterHandlers()
+ {
+ if (!isRegistered || networkManager == null)
+ {
+ return;
+ }
+
+ networkManager.UnregisterHandler(NetworkMessageType.CharacterTransform, HandleTransformMessage);
+ isRegistered = false;
+ }
+
+ private void HandleTransformMessage(NetworkMessage message)
+ {
+ if (identity == null || targetTransform == null)
+ {
+ return;
+ }
+
+ if (HasAuthority())
+ {
+ return;
+ }
+
+ CharacterTransformMessage transformMessage = CharacterTransformMessage.Deserialize(message.Payload);
+ if (transformMessage.NetworkId != identity.NetworkId)
+ {
+ return;
+ }
+
+ remoteTargetPosition = transformMessage.Position;
+ remoteTargetRotation = transformMessage.Rotation;
+ remoteTargetVelocity = transformMessage.Velocity;
+ haveRemoteState = true;
+ }
+
+ private void OnValidate()
+ {
+ broadcastInterval = Mathf.Max(0.01f, broadcastInterval);
+ remoteLerpSpeed = Mathf.Max(0f, remoteLerpSpeed);
+ teleportThreshold = Mathf.Max(0.01f, teleportThreshold);
+ }
+ }
+}
diff --git a/Game/Scripts/Networking/SteamNetworkTransform.cs.meta b/Game/Scripts/Networking/SteamNetworkTransform.cs.meta
new file mode 100644
index 0000000..b533725
--- /dev/null
+++ b/Game/Scripts/Networking/SteamNetworkTransform.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 88efaaed4187a0ee8ad63b619f65e970
\ No newline at end of file
diff --git a/Scenes/CharacterScene.unity b/Scenes/CharacterScene.unity
index a0905e2..1825afc 100644
--- a/Scenes/CharacterScene.unity
+++ b/Scenes/CharacterScene.unity
@@ -216,75 +216,6 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
---- !u!1001 &196170898
-PrefabInstance:
- m_ObjectHideFlags: 0
- serializedVersion: 2
- m_Modification:
- serializedVersion: 3
- m_TransformParent: {fileID: 0}
- m_Modifications:
- - target: {fileID: 1170732337855516, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_Name
- value: Golem (1)
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalScale.x
- value: 2.1307
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalScale.y
- value: 2.1307
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalScale.z
- value: 2.1307
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalPosition.x
- value: -12.33
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalPosition.y
- value: 0.16999996
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalPosition.z
- value: 5.49
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalRotation.w
- value: -0.06743933
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalRotation.x
- value: -0
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalRotation.y
- value: -0.9977234
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalRotation.z
- value: -0
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalEulerAnglesHint.x
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalEulerAnglesHint.y
- value: -187.734
- objectReference: {fileID: 0}
- - target: {fileID: 4526463827110206, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
- propertyPath: m_LocalEulerAnglesHint.z
- value: 0
- objectReference: {fileID: 0}
- m_RemovedComponents: []
- m_RemovedGameObjects: []
- m_AddedGameObjects: []
- m_AddedComponents: []
- m_SourcePrefab: {fileID: 100100000, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
--- !u!1 &1124421571
GameObject:
m_ObjectHideFlags: 0
@@ -494,6 +425,18 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
+ - target: {fileID: 6042376723608263729, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
+ propertyPath: GlobalObjectIdHash
+ value: 2082327815
+ objectReference: {fileID: 0}
+ - target: {fileID: 6042376723608263729, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
+ propertyPath: SceneMigrationSynchronization
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 6042376723608263729, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
+ propertyPath: InScenePlacedSourceGlobalObjectIdHash
+ value: 2899175104
+ objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
@@ -666,6 +609,18 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
+ - target: {fileID: 6042376723608263729, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
+ propertyPath: GlobalObjectIdHash
+ value: 1047745387
+ objectReference: {fileID: 0}
+ - target: {fileID: 6042376723608263729, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
+ propertyPath: SceneMigrationSynchronization
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 6042376723608263729, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
+ propertyPath: InScenePlacedSourceGlobalObjectIdHash
+ value: 2899175104
+ objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
@@ -735,6 +690,18 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
+ - target: {fileID: 6042376723608263729, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
+ propertyPath: GlobalObjectIdHash
+ value: 1912244675
+ objectReference: {fileID: 0}
+ - target: {fileID: 6042376723608263729, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
+ propertyPath: SceneMigrationSynchronization
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 6042376723608263729, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
+ propertyPath: InScenePlacedSourceGlobalObjectIdHash
+ value: 2899175104
+ objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
@@ -804,6 +771,18 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
+ - target: {fileID: 6042376723608263729, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
+ propertyPath: GlobalObjectIdHash
+ value: 1968154192
+ objectReference: {fileID: 0}
+ - target: {fileID: 6042376723608263729, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
+ propertyPath: SceneMigrationSynchronization
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 6042376723608263729, guid: b5051c49d05768c73a8c42e1967fe4b2, type: 3}
+ propertyPath: InScenePlacedSourceGlobalObjectIdHash
+ value: 2899175104
+ objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
@@ -980,7 +959,6 @@ SceneRoots:
- {fileID: 1432618524}
- {fileID: 1801103367}
- {fileID: 1857588276}
- - {fileID: 196170898}
- {fileID: 1770640151}
- {fileID: 8888013435869854006}
- {fileID: 1706180863}