using System; using System.Linq; using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif using Normal.Realtime.Serialization; namespace Normal.Realtime { public class Realtime : MonoBehaviour { // Class public static HashSet instances { get { return __instances; } } private static HashSet __instances; static Realtime() { #if UNITY_EDITOR EditorApplication.update += EditorUpdate; # if UNITY_2017_3_OR_NEWER EditorApplication.playModeStateChanged += PlayModeStateChanged; # endif #endif // Keep track of all instances of Realtime __instances = new HashSet(); } #if UNITY_EDITOR private static void EditorUpdate() { if (EditorApplication.isCompiling && EditorApplication.isPlaying) { EditorApplication.isPlaying = false; } } #if UNITY_2017_3_OR_NEWER private static void PlayModeStateChanged(PlayModeStateChange state) { if (state == PlayModeStateChange.EnteredPlayMode) { EditorApplication.LockReloadAssemblies(); } else if (state == PlayModeStateChange.EnteredEditMode) { EditorApplication.UnlockReloadAssemblies(); } } #endif #endif public static GameObject Instantiate(string prefabName, Realtime useInstance) { return Instantiate(prefabName, useInstance: useInstance); } public static GameObject Instantiate(string prefabName, bool ownedByClient = true, bool preventOwnershipTakeover = false, bool destroyWhenOwnerOrLastClientLeaves = true, Realtime useInstance = null) { if (useInstance == null) { if (__instances.Count == 0) { Debug.LogError("Realtime: Unable to instantiate prefab. No instances of Realtime exist in the scene. Please specify a specific instance of Realtime when calling Instantiate()"); return null; } if (__instances.Count > 1) { Debug.LogError("Realtime: Multiple instances of Realtime exist in the scene. "); return null; } foreach (Realtime instance in __instances) { useInstance = instance; break; } } return useInstance._Instantiate(prefabName, ownedByClient, preventOwnershipTakeover, destroyWhenOwnerOrLastClientLeaves); } public static GameObject Instantiate(string prefabName, Vector3 position, Quaternion rotation, bool ownedByClient = true, bool preventOwnershipTakeover = false, bool destroyWhenOwnerOrLastClientLeaves = true, Realtime useInstance = null) { GameObject gameObject = Instantiate(prefabName, ownedByClient, preventOwnershipTakeover, destroyWhenOwnerOrLastClientLeaves, useInstance); if (gameObject != null) { RealtimeTransform realtimeTransform = gameObject.GetComponent(); if (realtimeTransform != null) { realtimeTransform.RequestOwnership(); gameObject.transform.position = position; gameObject.transform.rotation = rotation; } else { Debug.LogWarning("Realtime: Instantiate() asked to set position & rotation on prefab that doesn't have a RealtimeTransform component. The position / rotation will not be synchronized between clients."); } } return gameObject; } public static void Destroy(GameObject gameObject) { if (gameObject == null) { Debug.LogError("Realtime asked to destroy game object, but the game object is null."); return; } RealtimeView realtimeView = gameObject.GetComponent(); if (realtimeView == null) { Debug.LogError("Realtime asked to destroy game object, but the game object does not contain a RealtimeView component."); return; } Destroy(realtimeView); } public static void Destroy(RealtimeView realtimeView) { Realtime realtime = realtimeView.realtime; if (realtime == null) { Debug.LogError("Realtime asked to destroy RealtimeView, but the realtime view isn't associated with an instance of Realtime. Was it instantiated using Realtime.Instantiate() ?"); return; } realtime.DestroyRealtimeView(realtimeView); } // Hide the built-in Instantiate methods [Obsolete("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName).")] [UnityEngineInternal.TypeInferenceRule(UnityEngineInternal.TypeInferenceRules.TypeOfFirstArgument)] public static new UnityEngine.Object Instantiate(UnityEngine.Object original, Vector3 position, Quaternion rotation, Transform parent) { throw new NotImplementedException("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName)."); } [Obsolete("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName).")] [UnityEngineInternal.TypeInferenceRule(UnityEngineInternal.TypeInferenceRules.TypeOfFirstArgument)] public static new UnityEngine.Object Instantiate(UnityEngine.Object original) { throw new NotImplementedException("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName)."); } [Obsolete("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName).")] [UnityEngineInternal.TypeInferenceRule(UnityEngineInternal.TypeInferenceRules.TypeOfFirstArgument)] public static new UnityEngine.Object Instantiate(UnityEngine.Object original, Vector3 position, Quaternion rotation) { throw new NotImplementedException("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName)."); } [Obsolete("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName).")] public static new T Instantiate(T original, Transform parent, bool worldPositionStays) where T : UnityEngine.Object { throw new NotImplementedException("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName)."); } [Obsolete("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName).")] public static new T Instantiate(T original, Transform parent) where T : UnityEngine.Object { throw new NotImplementedException("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName)."); } [Obsolete("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName).")] public static new T Instantiate(T original, Vector3 position, Quaternion rotation, Transform parent) where T : UnityEngine.Object { throw new NotImplementedException("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName)."); } [Obsolete("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName).")] public static new T Instantiate(T original, Vector3 position, Quaternion rotation) where T : UnityEngine.Object { throw new NotImplementedException("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName)."); } [Obsolete("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName).")] public static GameObject Instantiate(GameObject original) { throw new NotImplementedException("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName)."); } [Obsolete("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName).")] [UnityEngineInternal.TypeInferenceRule(UnityEngineInternal.TypeInferenceRules.TypeOfFirstArgument)] public static new UnityEngine.Object Instantiate(UnityEngine.Object original, Transform parent, bool instantiateInWorldSpace) { throw new NotImplementedException("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName)."); } [Obsolete("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName).")] [UnityEngineInternal.TypeInferenceRule(UnityEngineInternal.TypeInferenceRules.TypeOfFirstArgument)] public static new UnityEngine.Object Instantiate(UnityEngine.Object original, Transform parent) { throw new NotImplementedException("This version of Realtime.Instantiate() is not supported. Please use Realtime.Instantiate(string prefabName)."); } // Instance public delegate void RealtimeEvent(Realtime realtime); public event RealtimeEvent didConnectToRoom; public event RealtimeEvent didDisconnectFromRoom; [SerializeField] private string _appKey = ""; [SerializeField] private string _roomToJoinOnStart = "Test Room"; [SerializeField] private bool _joinRoomOnStart = true; [SerializeField] private bool _debugLogging = false; private Room _room; public Room room { get { return _room; } set { SetRoom(value); } } public bool connecting { get { if (_room == null) return false; return _room.connecting; } } public bool connected { get { if (_room == null) return false; return _room.connected; } } public bool disconnected { get { if (_room == null) return false; return _room.disconnected; } } public int clientID { get { return _room != null ? _room.clientID : -1; } } // Scene Views private HashSet _sceneViews; // Prefab Views private HashSet _prefabViews; private GameObject _lastPrefabInstantiated; //// Instance // Unity Events private void Awake() { // Create hash set to hold all scene realtime views if (_sceneViews == null) _sceneViews = new HashSet(); // Create hash set to hold all prefab realtime views _prefabViews = new HashSet(); // Register this instance __instances.Add(this); } private void Start() { if (_joinRoomOnStart) Connect(_roomToJoinOnStart, null); } private void OnDestroy() { // Unregister this instance __instances.Remove(this); // Disconnect Disconnect(); // Destroy room SetRoom(null); } private void OnApplicationQuit() { // Unregister this instance __instances.Remove(this); // Disconnect Disconnect(); // Destroy room SetRoom(null); } private void Update() { // TODO: Make an editor script that complains if Run In Background isn't set. If we don't run in the background, we don't tick and we get disconnected. // TODO: Experiment with only calling tick once every 1/30th of a second. Make sure audio still works nicely! if (_room != null) { _room.debugLogging = _debugLogging; _room.Tick(Time.deltaTime); } } // Events private void FireDidConnectToRoom() { try { if (didConnectToRoom != null) didConnectToRoom(this); } catch (Exception exception) { Debug.LogException(exception); } } private void FireDidDisconnectFromRoom() { try { if (didDisconnectFromRoom != null) didDisconnectFromRoom(this); } catch (Exception exception) { Debug.LogException(exception); } } // Room public void Connect(string roomName, IModel roomModel = null) { if (_room == null) SetRoom(new Room()); // Connect to the room _room.Connect(roomName, _appKey, roomModel); } public void Disconnect() { if (_room == null) return; _room.Disconnect(); } void RoomConnectionStateChanged(Room room, Room.ConnectionState previousConnectionState, Room.ConnectionState connectionState) { switch (connectionState) { case Room.ConnectionState.Ready: // Connect scene views ConnectSceneViewsToDatastore(); // Fire connect event FireDidConnectToRoom(); break; case Room.ConnectionState.Disconnected: case Room.ConnectionState.Error: // Fire disconnect event FireDidDisconnectFromRoom(); // Disconnect scene views DisconnectSceneViewsFromDatastore(); // Destroy prefab views DestroyAllPrefabRealtimeViews(); break; } } void SetRoom(Room room) { if (_room != null) { if (_room.connectionState == Room.ConnectionState.Ready) { // Fire disconnect event FireDidDisconnectFromRoom(); } // Unregister for connection and datastore events _room.connectionStateChanged -= RoomConnectionStateChanged; _room.datastore.prefabRealtimeViewModelAdded -= PrefabRealtimeViewModelAdded; _room.datastore.prefabRealtimeViewModelRemoved -= PrefabRealtimeViewModelRemoved; // Destroy prefab views DestroyAllPrefabRealtimeViews(); // Disconnect scene views DisconnectSceneViewsFromDatastore(); // Clear realtime reference _room._SetRealtime(null); } _room = room; if (_room != null) { // Remove room from existing Realtime instance if it's bound to one. if (_room.realtime != null && _room.realtime != this) _room.realtime.SetRoom(null); // Set reference to realtime (only used to prevent multiple Realtime instances from using the same Room object) _room._SetRealtime(this); _room.debugLogging = _debugLogging; // Register for connection and datastore events _room.connectionStateChanged += RoomConnectionStateChanged; _room.datastore.prefabRealtimeViewModelAdded += PrefabRealtimeViewModelAdded; _room.datastore.prefabRealtimeViewModelRemoved += PrefabRealtimeViewModelRemoved; // Connect scene views ConnectSceneViewsToDatastore(); // Create prefab views CreatePrefabRealtimeViewsForDatastore(); if (_room.connectionState == Room.ConnectionState.Ready) { // Fire connect event FireDidConnectToRoom(); } } } // Scene Realtime Views public void _RegisterSceneRealtimeView(RealtimeView view) { if (view.sceneViewUUID == null || view.sceneViewUUID.Length == 0) { Debug.LogError("Realtime: Attempting to register RealtimeView as a scene view, but it doesn't have a proper UUID. Ignoring. This is a bug!"); return; } // Create hash set to hold all scene realtime views in if needed if (_sceneViews == null) _sceneViews = new HashSet(); // Check for duplicate UUIDs foreach (RealtimeView sceneView in _sceneViews) { if (sceneView.sceneViewUUID.SequenceEqual(view.sceneViewUUID)) { Debug.LogError("Realtime: RealtimeView attempting to register with a UUID that has already been registered! This means there are multiple RealtimeViews in the scene with the same UUID. Did you additively load a copy of the same scene? Make sure to reset the UUID for each RealtimeView under Advanced Settings. This RealtimeView will be ignored."); return; } } // Add to scene view collection _sceneViews.Add(view); // If we're already connected, then link up this scene view to its model in the room datastore if (_room != null && _room.connectionState == Room.ConnectionState.Ready) { ConnectSceneViewToDatastore(view); } else { // If we're not connected, we should at least give this view a fresh model to read off of in the meantime ReplaceSceneViewModelWithFreshModel(view); } } public void _UnregisterSceneRealtimeView(RealtimeView view) { if (!_sceneViews.Remove(view)) { Debug.LogError("Realtime: RealtimeView attempting to unregister, but is not found in this instance of Realtime's scene view list."); return; } // Repplace model on view so it doesn't mess with the datastore one ReplaceSceneViewModelWithFreshModel(view); } private void ConnectSceneViewsToDatastore() { // The room is already connected, connect every scene RealtimeView to the datastore. if (_room.connectionState == Room.ConnectionState.Ready) { foreach (RealtimeView view in _sceneViews) { ConnectSceneViewToDatastore(view); } } } private void ConnectSceneViewToDatastore(RealtimeView view) { if (_room.connectionState != Room.ConnectionState.Ready) { Debug.LogError("Failed to connect scene RealtimeView to model. Not connected to room... This is a bug!"); return; } if (view.sceneViewUUID == null || view.sceneViewUUID.Length == 0) { Debug.LogError("Realtime: Attempting to connect scene RealtimeView to the datastore, but it doesn't have a proper UUID. Ignoring. This is a bug!"); return; } RealtimeViewModel viewModel = _room.datastore.GetSceneRealtimeViewModelForUUID(view.sceneViewUUID); // Create a view model for this scene object in the datastore if (viewModel == null) { viewModel = view._CreateRootSceneViewModel(); // Add to datastore if (!_room.datastore.AddSceneRealtimeViewModel(viewModel)) { Debug.LogError("Unable to add scene RealtimeView's model to the room datastore to synchronize. This is a bug!"); return; } } // At this point we have a valid viewModel that exists in the datastore. Set it on the view. view.model = viewModel; } private void DisconnectSceneViewsFromDatastore() { foreach (RealtimeView view in _sceneViews) { ReplaceSceneViewModelWithFreshModel(view); } } private void ReplaceSceneViewModelWithFreshModel(RealtimeView view) { // Replace view model with clean / fresh view model. view.model = view._CreateRootSceneViewModel(); } // Prefab Realtime Views private GameObject _Instantiate(string prefabName, bool ownedByClient = true, bool preventOwnershipTakeover = false, bool destroyWhenOwnerOrLastClientLeaves = true) { if (_room == null) { Debug.LogError("Realtime asked to instantiate game object, but is not associated with a room! Ignoring."); return null; } if (_room.connectionState != Room.ConnectionState.Ready) { Debug.LogError("Realtime asked to instantiate game object, but we're not connected to a room! Ignoring. (Room: " + _room.connectionState + ")"); return null; } // Load the prefab GameObject prefab = Resources.Load(prefabName); if (prefab == null) { Debug.LogError("Failed to find prefab \"" + prefabName + "\". Make sure it's in a Resources folder. Bailing."); return null; } // Get the RealtimeView script at the root RealtimeView prefabRealtimeView = prefab.GetComponent(); if (prefabRealtimeView == null) { Debug.LogError("Failed to find RealtimeView script on prefab \"" + prefabName + "\". Make sure the prefab has a RealtimeView script at the root level. Bailing."); return null; } // Clear this just to be safe _lastPrefabInstantiated = null; // Ownership / lifetime flags int ownerID = ownedByClient ? clientID : -1; uint lifetimeFlags = 0; if (preventOwnershipTakeover) lifetimeFlags |= (uint)MetaModel.LifetimeFlags.PreventOwnershipTakeover; if (destroyWhenOwnerOrLastClientLeaves) lifetimeFlags |= (uint)MetaModel.LifetimeFlags.DestroyWhenOwnerOrLastClientLeaves; // Add model to datastore for this prefab _room.datastore.AddPrefabRealtimeViewModel(prefabRealtimeView._CreateRootPrefabViewModel(prefabName, ownerID, lifetimeFlags)); // At this point, PrefabRealtimeViewModelAdded has fired for the model above, created the prefab, and stored a reference to it in _lastPrefabInstantiated return _lastPrefabInstantiated; } private void DestroyRealtimeView(RealtimeView realtimeView) { if (_room == null) { // Note: I commented this out, because if scene objects attempt to destroy realtime views inside of // OnDestroy, they'll fire after Realtime has already disconnected and destroyed the room. // Afaik, there's no way for them to detect that, so we fail silently here. //Debug.LogError("Realtime: Unable to destroy RealtimeView because this Realtime instance is not associated with a room! Ignoring."); return; } if (_room.connectionState != Room.ConnectionState.Ready) { Debug.LogError("Realtime: Unable to destroy RealtimeView because we're not connected to a room! Ignoring. (Room: " + _room.connectionState + ")"); return; } RealtimeViewModel model = realtimeView.model; if (model == null) { Debug.LogError("Realtime: Unable to destroy RealtimeView because it doesn't have a model property associated with it."); return; } if (!_room.datastore.RemovePrefabRealtimeViewModel(model)) { Debug.LogError("Realtime: Could not find RealtimeViewModel for RealtimeView in the datastore. Unable to destroy RealtimeView."); return; } } void PrefabRealtimeViewModelAdded(Datastore datastore, RealtimeViewModel model, bool remote) { // Create a realtime view for this realtime view model. GameObject gameObject = CreatePrefabForRealtimeViewModel(model); // If this is a local call, store the game object so Instantiate() can return a reference to it if (!remote) _lastPrefabInstantiated = gameObject; } void PrefabRealtimeViewModelRemoved(Datastore datastore, RealtimeViewModel model, bool remote) { if (model.realtimeView != null) { _prefabViews.Remove(model.realtimeView); UnityEngine.Object.Destroy(model.realtimeView.gameObject); } else { Debug.LogError("Realtime: RealtimeViewModel was deleted from datastore, but has no corresponding prefab. This is a bug."); } } private void CreatePrefabRealtimeViewsForDatastore() { DestroyAllPrefabRealtimeViews(); // The room is already connected, create a RealtimeView for every model in the datastore. if (_room.connectionState == Room.ConnectionState.Ready) { foreach (RealtimeViewModel prefabViewModel in _room.datastore.prefabViewModels) { CreatePrefabForRealtimeViewModel(prefabViewModel); } } } private void DestroyAllPrefabRealtimeViews() { foreach (RealtimeView prefabView in _prefabViews) { UnityEngine.Object.Destroy(prefabView.gameObject); } _prefabViews.Clear(); } private GameObject CreatePrefabForRealtimeViewModel(RealtimeViewModel model) { string prefabName = model.prefabName; // Load the prefab GameObject prefab = Resources.Load(prefabName); if (prefab == null) { Debug.LogError("Failed to find prefab \"" + prefabName + "\". Make sure it's in a Resources folder. Bailing."); return null; } // Get the RealtimeView script at the root RealtimeView prefabRealtimeView = prefab.GetComponent(); if (prefabRealtimeView == null) { Debug.LogError("Attempting to instantiate prefab from datastore. Failed to find RealtimeView script on prefab \"" + prefabName + "\". Make sure the prefab has a RealtimeView script at the root level. Bailing."); return null; } // Instantiate and assign the model GameObject gameObject = GameObject.Instantiate(prefab); RealtimeView realtimeView = gameObject.GetComponent(); realtimeView._SetRealtime(this); realtimeView.model = model; // Add to hash set of prefab realtime views. _prefabViews.Add(realtimeView); return gameObject; } } }