namespace Fusion.Addons.InterestManagement { using System.Collections.Generic; using Unity.Profiling; using UnityEngine; /// /// Interest manager for players. /// Provides reference to active PlayerInterestView for owner player. /// Also provides reference to observed player which can be different from owner player. /// There is always one unique instance of PlayerInterestManager per player. /// [DefaultExecutionOrder(PlayerInterestManager.EXECUTION_ORDER)] public class PlayerInterestManager : NetworkBehaviour { // CONSTANTS public const int EXECUTION_ORDER = InterestProvider.EXECUTION_ORDER + 1000; // PUBLIC MEMBERS /// /// Owner player, equals to object input authority. /// public PlayerRef Player { get => _player; } /// /// Observed player. Can be used to spectate another player and get updates for objects in his interest. /// public PlayerRef ObservedPlayer { get => _observedPlayer.IsRealPlayer == true ? _observedPlayer : _player; set => _observedPlayer = value; } /// /// Reference to active interest view. A player can have multiple views spawned and switch between them. /// Only the active view is used to provide player interest, others are ignored. /// public PlayerInterestView InterestView { get => _interestView; set => _interestView = value; } /// /// Last tick in which player interest was updated. /// public Tick LastUpdateTick { get => _lastUpdateTick; } /// /// Last known interest cell count. /// The value comes from regular update or drawing gizmos. /// At runtime the value for server/host is always 0, the interest is not processed. /// public int InterestCellCount { get => _interestCellCount; } /// /// List of interest cells from Gizmo preview. /// This is used for a editor preview of objects in player interest on state authority. /// public HashSet GizmoInterestCells { get => _gizmoCells; } // PRIVATE MEMBERS [SerializeField] [InlineHelp][Tooltip("Reference to view that provides interest for the player.")] private PlayerInterestView _interestView; private PlayerRef _player; private PlayerRef _observedPlayer; private Tick _lastUpdateTick; private bool _isUpdateRequested; private int _interestCellCount; private PlayerInterestView _interestViewBackup; private InterestProviderSet _interestProviders = new InterestProviderSet(); private static HashSet _gizmoCells = new HashSet(); private static List _objectsInPlayerInterest = new List(); private static ProfilerMarker _updateInterestsMarker = new ProfilerMarker($"{nameof(PlayerInterestManager)}.{nameof(UpdatePlayerInterests)}"); // PUBLIC METHODS /// /// Returns PlayerInterestView for observed player. /// /// PlayerInterestView instance. public bool TryGetObservedPlayerView(out PlayerInterestView observedPlayerView) { GlobalInterestManager globalInterestManager = Runner.GetGlobalInterestManager(); if (globalInterestManager.TryGetPlayerView(ObservedPlayer, out PlayerInterestView playerView) == true && playerView != null) { observedPlayerView = playerView; return true; } observedPlayerView = default; return false; } /// /// Request update of player interest. This allows for priority update after an important action so the player receives data as soon as possible. /// public void RequestInterestUpdate() { _isUpdateRequested = true; } /// /// Checks input authority and other properties and immediately propagates changes. /// public void RefreshPlayerProperties() { UpdatePlayerProperties(); } // PlayerInterestManager INTERFACE /// /// Can be used to defer evaluation of interest. /// public virtual bool CanUpdatePlayerInterest(bool isUpdateRequested) => isUpdateRequested == true || Runner.IsForward == true; /// /// Called before the player interest is updated. /// protected virtual void BeforePlayerInterestUpdate() {} /// /// Called after the player interest is updated. /// protected virtual void AfterPlayerInterestUpdate(bool success, PlayerInterestView playerView) {} /// /// Called on the end of Awake(). /// protected virtual void OnAwake() {} /// /// Called on the end of Spawned(), after updating player properties and registering to global interest manager. /// protected virtual void OnSpawned() {} /// /// Called on the start of Despawned(), before unregistering from global interest manager and resetting state. /// protected virtual void OnDespawned(NetworkRunner runner, bool hasState) {} /// /// Called on the end of FixedUpdateNetwork(), after updating player properties and resolving player interest. /// protected virtual void OnFixedUpdateNetwork() {} /// /// Called on the end of Render(), after updating player properties. /// protected virtual void OnRender() {} // NetworkBehaviour INTERFACE public override sealed void Spawned() { _player = Object.InputAuthority; if (_player.IsRealPlayer == true) { GlobalInterestManager globalInterestManager = Runner.GetGlobalInterestManager(); globalInterestManager.RegisterPlayerManager(_player, this); } UpdatePlayerProperties(); OnSpawned(); } public override sealed void Despawned(NetworkRunner runner, bool hasState) { OnDespawned(runner, hasState); Runner.GetGlobalInterestManager().UnregisterPlayerManager(_player, this); _interestView = _interestViewBackup; _player = default; _observedPlayer = default; _lastUpdateTick = default; _isUpdateRequested = default; _interestCellCount = default; _interestProviders.Clear(); } public override sealed void FixedUpdateNetwork() { UpdatePlayerProperties(); UpdatePlayerInterests(); OnFixedUpdateNetwork(); } public override sealed void Render() { UpdatePlayerProperties(); OnRender(); } // MonoBehaviour INTERFACE private void Awake() { if (_interestView == null) { _interestView = GetComponent(); } _interestViewBackup = _interestView; OnAwake(); } // PRIVATE METHODS private void UpdatePlayerProperties() { PlayerRef oldPlayer = _player; PlayerRef newPlayer = Object.InputAuthority; if (oldPlayer != newPlayer) { GlobalInterestManager globalInterestManager = Runner.GetGlobalInterestManager(); globalInterestManager.UnregisterPlayerManager(oldPlayer, this); _player = newPlayer; if (newPlayer.IsRealPlayer == true) { globalInterestManager.RegisterPlayerManager(newPlayer, this); } } if (_interestView == null) { // Replace Unity null object by true null before updating player interests. // This allows using ReferenceEquals() instead of == operator during evaluations. _interestView = null; } } private void UpdatePlayerInterests() { NetworkRunner runner = Runner; if (IsInterestManagementActive(runner) == false) return; if (_player.IsRealPlayer == false) return; if (_player == runner.LocalPlayer) return; if (CanUpdatePlayerInterest(_isUpdateRequested) == false) return; _isUpdateRequested = false; _updateInterestsMarker.Begin(); BeforePlayerInterestUpdate(); bool success = false; PlayerInterestView playerView = null; HashSet playerCells = NetworkRunnerExtensions.GetAreaOfInterestCells(runner, _player); if (playerCells != null) { GlobalInterestManager globalInterestManager = Runner.GetGlobalInterestManager(); if (globalInterestManager.GetProvidersForPlayer(ObservedPlayer, _interestProviders, out PlayerInterestView playerInterestView) == true) { GetInterestCells(_interestProviders, playerInterestView, playerCells); success = true; playerView = playerInterestView; _lastUpdateTick = Runner.Tick; } _interestCellCount = playerCells.Count; } AfterPlayerInterestUpdate(success, playerView); _updateInterestsMarker.End(); } private static void GetInterestCells(InterestProviderSet interestProviders, PlayerInterestView playerView, HashSet cells) { cells.Clear(); interestProviders.Sort(); List interestProviderValues = interestProviders.ReadOnlyValues; for (int i = Mathf.Max(0, interestProviders.GetLastOverrideProviderIndex()), count = interestProviderValues.Count; i < count; ++i) { IInterestProvider interestProvider = interestProviderValues[i]; interestProvider.GetCellsForPlayer(playerView, cells); } } private static bool IsInterestManagementActive(NetworkRunner runner) { if (ReferenceEquals(runner, null) == true) return false; return runner.IsServer == true && runner.GameMode != GameMode.Shared && runner.Config.Simulation.ReplicationFeatures == NetworkProjectConfig.ReplicationFeatures.SchedulingAndInterestManagement; } private void DrawGizmos() { if (Application.isPlaying == true) { NetworkRunner runner = Runner; if (IsInterestManagementActive(runner) == true) { HashSet interestCells = default; List interestProviders = default; if (runner.LocalPlayer != _player) { interestCells = NetworkRunnerExtensions.GetAreaOfInterestCells(runner, _player); } GlobalInterestManager globalInterestManager = runner.GetGlobalInterestManager(); if (globalInterestManager.GetProvidersForPlayer(ObservedPlayer, _interestProviders, out PlayerInterestView playerView) == true) { interestProviders = _interestProviders.ReadOnlyValues; if (runner.LocalPlayer == _player) { GetInterestCells(_interestProviders, playerView, _gizmoCells); interestCells = _gizmoCells; } } if (interestCells != null) { _interestCellCount = interestCells.Count; if (InterestUtility.DrawInterestCells == true) { InterestUtility.DrawCells(interestCells); } if (InterestUtility.DrawPlayerInterest == true) { if (NetworkRunnerExtensions.GetObjectsInPlayerInterest(Runner, Player, _objectsInPlayerInterest, interestCells) == true) { InterestUtility.DrawObjectsInPlayerInterest(_objectsInPlayerInterest); } } } if (interestProviders != null && InterestUtility.DrawPlayerInterest == true) { for (int i = Mathf.Max(0, _interestProviders.GetLastOverrideProviderIndex()), count = interestProviders.Count; i < count; ++i) { IInterestProvider interestProvider = interestProviders[i]; interestProvider.DrawGizmosForPlayer(playerView); } } } } else { if (_interestView != null) { _interestView.SetPlayerInfo(_interestView.Transform, _interestView.Transform); _interestProviders.Clear(); AddInterestProviderRecursive(_interestProviders, _interestView); _interestProviders.Sort(); GetInterestCells(_interestProviders, _interestView, _gizmoCells); InterestUtility.DrawCells(_gizmoCells); _interestCellCount = _gizmoCells.Count; } } static void AddInterestProviderRecursive(InterestProviderSet interestProviders, InterestProvider interestProvider) { if (interestProvider == null) return; if (interestProviders.Add(interestProvider) == true) { if (interestProvider.ChildProviders != null) { for (int i = 0, count = interestProvider.ChildProviders.Count; i < count; ++i) { AddInterestProviderRecursive(interestProviders, interestProvider.ChildProviders[i]); } } } } } #if UNITY_EDITOR [UnityEditor.DrawGizmo(UnityEditor.GizmoType.Active)] private static void DrawGizmo(PlayerInterestManager playerInterestManager, UnityEditor.GizmoType gizmoType) { if ((gizmoType & UnityEditor.GizmoType.Active) != UnityEditor.GizmoType.Active) return; playerInterestManager.DrawGizmos(); } #endif } }