/// <summary> /// Attempts to create a Photon Cloud Account asynchronously. /// Once your callback is called, check ReturnCode, Message and AppId to get the result of this attempt. /// </summary> /// <param name="email">Email of the account.</param> /// <param name="origin">Marks which channel created the new account (if it's new).</param> /// <param name="serviceType">Defines which type of Photon-service is being requested.</param> /// <param name="callback">Called when the result is available.</param> public void RegisterByEmail(string email, Origin origin, string serviceType, Action <AccountService> callback = null) { this.registrationCallback = callback; this.AppId = string.Empty; this.AppId2 = string.Empty; this.Message = string.Empty; this.ReturnCode = -1; string url = this.RegistrationUri(email, (byte)origin, serviceType); PhotonEditorUtils.StartCoroutine( PhotonEditorUtils.HttpGet(url, (s) => { this.ParseResult(s); if (this.registrationCallback != null) { this.registrationCallback(this); } }, (e) => { this.Message = e; if (this.registrationCallback != null) { this.registrationCallback(this); } }) ); }
public static AssetDeleteResult OnWillDeleteAsset(string assetPath, RemoveAssetOptions rao) { if ("Assets/Photon/PhotonUnityNetworking".Equals(assetPath)) { PhotonEditorUtils.CleanUpPunDefineSymbols(); } return(AssetDeleteResult.DidNotDelete); }
public bool RegisterByEmail(AccountServiceRequest request, Action <AccountServiceResponse> callback = null, Action <string> errorCallback = null) { if (request == null) { Debug.LogError("Registration request is null"); return(false); } string fullUrl = GetUrlWithQueryStringEscaped(request); RequestHeaders["x-functions-key"] = string.IsNullOrEmpty(CustomToken) ? DefaultToken : CustomToken; //Debug.LogWarningFormat("Full URL {0}", fullUrl); PhotonEditorUtils.StartCoroutine( PhotonEditorUtils.HttpPost(fullUrl, RequestHeaders, null, s => { //Debug.LogWarningFormat("received response {0}", s); if (string.IsNullOrEmpty(s)) { if (errorCallback != null) { errorCallback( "Server's response was empty. Please register through account website during this service interruption."); } } else { AccountServiceResponse ase = this.ParseResult(s); if (ase == null) { if (errorCallback != null) { errorCallback( "Error parsing registration response. Please try registering from account website"); } } else if (callback != null) { callback(ase); } } }, e => { if (errorCallback != null) { errorCallback(e); } }) ); return(true); }
public override void OnInspectorGUI() { this.m_Target = (PhotonView)this.target; bool isProjectPrefab = PhotonEditorUtils.IsPrefab(this.m_Target.gameObject); if (this.m_Target.ObservedComponents == null) { this.m_Target.ObservedComponents = new System.Collections.Generic.List <Component>(); } if (this.m_Target.ObservedComponents.Count == 0) { this.m_Target.ObservedComponents.Add(null); } EditorGUILayout.BeginHorizontal(); // Owner if (isProjectPrefab) { EditorGUILayout.LabelField("Owner:", "Set at runtime"); } else if (!this.m_Target.IsOwnerActive) { EditorGUILayout.LabelField("Owner", "Scene"); } else { Player owner = this.m_Target.Owner; string ownerInfo = (owner != null) ? owner.NickName : "<no Player found>"; if (string.IsNullOrEmpty(ownerInfo)) { ownerInfo = "<no playername set>"; } EditorGUILayout.LabelField("Owner", "[" + this.m_Target.OwnerActorNr + "] " + ownerInfo); } // ownership requests EditorGUI.BeginDisabledGroup(Application.isPlaying); OwnershipOption own = (OwnershipOption)EditorGUILayout.EnumPopup(this.m_Target.OwnershipTransfer, GUILayout.Width(100)); if (own != this.m_Target.OwnershipTransfer) { // jf: fixed 5 and up prefab not accepting changes if you quit Unity straight after change. // not touching the define nor the rest of the code to avoid bringing more problem than solving. EditorUtility.SetDirty(this.m_Target); Undo.RecordObject(this.m_Target, "Change PhotonView Ownership Transfer"); this.m_Target.OwnershipTransfer = own; } EditorGUI.EndDisabledGroup(); EditorGUILayout.EndHorizontal(); // View ID if (isProjectPrefab) { EditorGUILayout.LabelField("View ID", "Set at runtime"); } else if (EditorApplication.isPlaying) { EditorGUILayout.LabelField("View ID", this.m_Target.ViewID.ToString()); } else { int idValue = EditorGUILayout.IntField("View ID [1.." + (PhotonNetwork.MAX_VIEW_IDS - 1) + "]", this.m_Target.ViewID); if (this.m_Target.ViewID != idValue) { Undo.RecordObject(this.m_Target, "Change PhotonView viewID"); this.m_Target.ViewID = idValue; } } // Locally Controlled if (EditorApplication.isPlaying) { string masterClientHint = PhotonNetwork.IsMasterClient ? "(master)" : ""; EditorGUILayout.Toggle("Controlled locally: " + masterClientHint, this.m_Target.IsMine); } // ViewSynchronization (reliability) if (this.m_Target.Synchronization == ViewSynchronization.Off) { GUI.color = Color.grey; } EditorGUILayout.PropertyField(this.serializedObject.FindProperty("Synchronization"), new GUIContent("Observe option:")); if (this.m_Target.Synchronization != ViewSynchronization.Off && this.m_Target.ObservedComponents.FindAll(item => item != null).Count == 0) { GUILayout.BeginVertical(GUI.skin.box); GUILayout.Label("Warning", EditorStyles.boldLabel); GUILayout.Label("Setting the synchronization option only makes sense if you observe something."); GUILayout.EndVertical(); } GUI.color = Color.white; this.DrawObservedComponentsList(); // Cleanup: save and fix look if (GUI.changed) { PhotonViewHandler.HierarchyChange(); // TODO: check if needed } GUI.color = Color.white; }
internal static void OnHierarchyChanged() { // set prefabs to viewID 0 if needed // organize resource PVs in a list per viewID // process the lists: if more than one photonView is in a list, we have to resolve the clash // check if only one view had the viewId earlier // apply a new viewID to the others // update the cached list of instances and their viewID //Debug.LogWarning("OnHierarchyChanged(). isPlaying: " + Application.isPlaying); if (Application.isPlaying) { return; } PhotonView[] photonViewResources = Resources.FindObjectsOfTypeAll <PhotonView>(); List <PhotonView> photonViewInstances = new List <PhotonView>(); Dictionary <int, List <PhotonView> > viewInstancesPerViewId = new Dictionary <int, List <PhotonView> >(); List <PhotonView> photonViewsToReassign = new List <PhotonView>(); foreach (PhotonView view in photonViewResources) { if (PhotonEditorUtils.IsPrefab(view.gameObject)) { if (view.ViewID != 0) { view.ViewID = 0; EditorUtility.SetDirty(view); } continue; // skip prefabs } photonViewInstances.Add(view); // assign a new viewID if the viewId is lower than the minimum for this scene if (!IsViewIdOkForScene(view)) { photonViewsToReassign.Add(view); continue; // this view definitely gets cleaned up, so it does not count versus duplicates, checked below } // organize the viewInstances into lists per viewID, so we know duplicate usage if (!viewInstancesPerViewId.ContainsKey(view.ViewID)) { viewInstancesPerViewId[view.ViewID] = new List <PhotonView>(); } viewInstancesPerViewId[view.ViewID].Add(view); } //Debug.Log("PreviousAssignments: "+PunSceneViews.Instance.Views.Count); foreach (List <PhotonView> list in viewInstancesPerViewId.Values) { if (list.Count <= 1) { continue; // skip lists with just one entry (the viewID is unique) } PhotonView previousAssignment = null; bool wasAssigned = PunSceneViews.Instance.Views.TryGetValue(list[0].ViewID, out previousAssignment); foreach (PhotonView view in list) { if (wasAssigned && view.Equals(previousAssignment)) { // previously, we cached the used viewID as assigned to the current view. we don't change this. continue; } //Debug.LogWarning("View to reassign due to viewID: "+view, view.gameObject); photonViewsToReassign.Add(view); } } int i; foreach (PhotonView view in photonViewsToReassign) { i = MinSceneViewId(view); while (viewInstancesPerViewId.ContainsKey(i)) { i++; } view.ViewID = i; viewInstancesPerViewId.Add(i, null); // we don't need the lists anymore but we care about getting the viewIDs listed EditorUtility.SetDirty(view); } // update the "semi persistent" list of viewIDs and their PhotonViews PunSceneViews.Instance.Views.Clear(); foreach (PhotonView view in photonViewInstances) { if (PunSceneViews.Instance.Views.ContainsKey(view.ViewID)) { Debug.LogError("ViewIDs should no longer have duplicates! " + view.ViewID, view); continue; } PunSceneViews.Instance.Views[view.ViewID] = view; } //Debug.Log("photonViewsToReassign.Count: "+photonViewsToReassign.Count + " count of viewIDs in use: "+viewInstancesPerViewId.Values.Count); //Debug.Log("PreviousAssignments now counts: "+PunSceneViews.Instance.Views.Count); }
public override void OnInspectorGUI() { this.m_Target = (PhotonView)this.target; bool isProjectPrefab = PhotonEditorUtils.IsPrefab(this.m_Target.gameObject); bool multiSelected = Selection.gameObjects.Length > 1; if (this.m_Target.ObservedComponents == null) { this.m_Target.ObservedComponents = new System.Collections.Generic.List <Component>(); } if (this.m_Target.ObservedComponents.Count == 0) { this.m_Target.ObservedComponents.Add(null); } GUILayout.Space(5); EditorGUILayout.BeginVertical((GUIStyle)"HelpBox"); // View ID - Hide if we are multi-selected if (!multiSelected) { if (isProjectPrefab) { EditorGUILayout.LabelField("View ID", "<i>Set at runtime</i>", new GUIStyle("Label") { richText = true }); } else if (EditorApplication.isPlaying) { EditorGUILayout.LabelField("View ID", this.m_Target.ViewID.ToString()); } else { // this is an object in a scene, modified at edit-time. we can store this as sceneViewId int idValue = EditorGUILayout.IntField("View ID [1.." + (PhotonNetwork.MAX_VIEW_IDS - 1) + "]", this.m_Target.sceneViewId); if (this.m_Target.sceneViewId != idValue) { Undo.RecordObject(this.m_Target, "Change PhotonView viewID"); this.m_Target.sceneViewId = idValue; } } } // Locally Controlled if (EditorApplication.isPlaying) { string masterClientHint = PhotonNetwork.IsMasterClient ? " (master)" : ""; EditorGUILayout.LabelField("IsMine:", this.m_Target.IsMine.ToString() + masterClientHint); Room room = PhotonNetwork.CurrentRoom; int cretrId = this.m_Target.CreatorActorNr; Player cretr = (room != null) ? room.GetPlayer(cretrId) : null; Player owner = this.m_Target.Owner; Player ctrlr = this.m_Target.Controller; EditorGUILayout.LabelField("Controller:", (ctrlr != null ? ("[" + ctrlr.ActorNumber + "] '" + ctrlr.NickName + "' " + (ctrlr.IsMasterClient ? " (master)" : "")) : "[0] <null>")); EditorGUILayout.LabelField("Owner:", (owner != null ? ("[" + owner.ActorNumber + "] '" + owner.NickName + "' " + (owner.IsMasterClient ? " (master)" : "")) : "[0] <null>")); EditorGUILayout.LabelField("Creator:", (cretr != null ? ("[" + cretrId + "] '" + cretr.NickName + "' " + (cretr.IsMasterClient ? " (master)" : "")) : "[0] <null>")); } EditorGUILayout.EndVertical(); EditorGUI.BeginDisabledGroup(Application.isPlaying); GUILayout.Space(5); // Ownership section EditorGUILayout.LabelField("Ownership", (GUIStyle)"BoldLabel"); OwnershipOption own = (OwnershipOption)EditorGUILayout.EnumPopup(ownerTransferGuiContent, this.m_Target.OwnershipTransfer /*, GUILayout.MaxWidth(68), GUILayout.MinWidth(68)*/); if (own != this.m_Target.OwnershipTransfer) { // jf: fixed 5 and up prefab not accepting changes if you quit Unity straight after change. // not touching the define nor the rest of the code to avoid bringing more problem than solving. EditorUtility.SetDirty(this.m_Target); Undo.RecordObject(this.m_Target, "Change PhotonView Ownership Transfer"); this.m_Target.OwnershipTransfer = own; } GUILayout.Space(5); // Observables section EditorGUILayout.LabelField("Observables", (GUIStyle)"BoldLabel"); EditorGUILayout.PropertyField(this.serializedObject.FindProperty("Synchronization"), syncronizationGuiContent); if (this.m_Target.Synchronization == ViewSynchronization.Off) { // Show warning if there are any observables. The null check is because the list allows nulls. var observed = m_Target.ObservedComponents; if (observed.Count > 0) { for (int i = 0, cnt = observed.Count; i < cnt; ++i) { if (observed[i] != null) { EditorGUILayout.HelpBox("Synchronization is set to Off. Select a Synchronization setting in order to sync the listed Observables.", MessageType.Warning); break; } } } } PhotonView.ObservableSearch autoFindObservables = (PhotonView.ObservableSearch)EditorGUILayout.EnumPopup(observableSearchGuiContent, m_Target.observableSearch); if (m_Target.observableSearch != autoFindObservables) { Undo.RecordObject(this.m_Target, "Change Auto Find Observables Toggle"); m_Target.observableSearch = autoFindObservables; } m_Target.FindObservables(); if (!multiSelected) { bool disableList = Application.isPlaying || autoFindObservables != PhotonView.ObservableSearch.Manual; if (disableList) { EditorGUI.BeginDisabledGroup(true); } this.DrawObservedComponentsList(disableList); if (disableList) { EditorGUI.EndDisabledGroup(); } } // Cleanup: save and fix look if (GUI.changed) { PhotonViewHandler.OnHierarchyChanged(); // TODO: check if needed } EditorGUI.EndDisabledGroup(); }
// this method corrects the IDs for photonviews in the scene and in prefabs // make sure prefabs always use viewID 0 // make sure instances never use a owner // this is a editor class that should only run if not playing internal static void HierarchyChange() { if (Application.isPlaying) { //Debug.Log("HierarchyChange ignored, while running."); CheckSceneForStuckHandlers = true; // done once AFTER play mode. return; } if (CheckSceneForStuckHandlers) { CheckSceneForStuckHandlers = false; PhotonNetwork.InternalCleanPhotonMonoFromSceneIfStuck(); } HashSet <PhotonView> pvInstances = new HashSet <PhotonView>(); HashSet <int> usedInstanceViewNumbers = new HashSet <int>(); bool fixedSomeId = false; //// the following code would be an option if we only checked scene objects (but we can check all PVs) //PhotonView[] pvObjects = GameObject.FindSceneObjectsOfType(typeof(PhotonView)) as PhotonView[]; //Debug.Log("HierarchyChange. PV Count: " + pvObjects.Length); string levelName = SceneManagerHelper.ActiveSceneName; #if UNITY_EDITOR levelName = SceneManagerHelper.EditorActiveSceneName; #endif int minViewIdInThisScene = PunSceneSettings.MinViewIdForScene(levelName); //Debug.Log("Level '" + Application.loadedLevelName + "' has a minimum ViewId of: " + minViewIdInThisScene); PhotonView[] pvObjects = Resources.FindObjectsOfTypeAll(typeof(PhotonView)) as PhotonView[]; foreach (PhotonView view in pvObjects) { // first pass: fix prefabs to viewID 0 if they got a view number assigned (cause they should not have one!) if (PhotonEditorUtils.IsPrefab(view.gameObject)) { if (view.ViewID != 0 || view.prefixField != -1) { #if !UNITY_2018_3_OR_NEWER Debug.LogWarning("PhotonView on persistent object being fixed (id and prefix must be 0). Was: " + view); #endif view.ViewID = 0; view.prefixField = -1; EditorUtility.SetDirty(view); // even in Unity 5.3+ it's OK to SetDirty() for non-scene objects. fixedSomeId = true; } } else { // keep all scene-instanced PVs for later re-check pvInstances.Add(view); } } Dictionary <GameObject, int> idPerObject = new Dictionary <GameObject, int>(); // second pass: check all used-in-scene viewIDs for duplicate viewIDs (only checking anything non-prefab) // scene-PVs must have user == 0 (scene/room) and a subId != 0 foreach (PhotonView view in pvInstances) { if (view.OwnerActorNr > 0) { Debug.Log("Re-Setting Owner ID of: " + view); } view.Prefix = -1; // TODO: prefix could be settable via inspector per scene?! if (view.ViewID != 0) { if (view.ViewID < minViewIdInThisScene || usedInstanceViewNumbers.Contains(view.ViewID)) { view.ViewID = 0; // avoid duplicates and negative values by assigning 0 as (temporary) number to be fixed in next pass } else { usedInstanceViewNumbers.Add(view.ViewID); // builds a list of currently used viewIDs int instId = 0; if (idPerObject.TryGetValue(view.gameObject, out instId)) { view.InstantiationId = instId; } else { view.InstantiationId = view.ViewID; idPerObject[view.gameObject] = view.InstantiationId; } } } } // third pass: anything that's now 0 must get a new (not yet used) ID (starting at 0) int lastUsedId = (minViewIdInThisScene > 0) ? minViewIdInThisScene - 1 : 0; foreach (PhotonView view in pvInstances) { if (view.ViewID == 0) { Undo.RecordObject(view, "Automatic viewID change for: " + view.gameObject.name); // Debug.LogWarning("setting scene ID: " + view.gameObject.name + " ID: " + view.subId.ID + " scene ID: " + view.GetSceneID() + " IsPersistent: " + EditorUtility.IsPersistent(view.gameObject) + " IsSceneViewIDFree: " + IsSceneViewIDFree(view.subId.ID, view)); int nextViewId = PhotonViewHandler.GetID(lastUsedId, usedInstanceViewNumbers); view.ViewID = nextViewId; int instId = 0; if (idPerObject.TryGetValue(view.gameObject, out instId)) { view.InstantiationId = instId; } else { view.InstantiationId = view.ViewID; idPerObject[view.gameObject] = nextViewId; } lastUsedId = nextViewId; fixedSomeId = true; } } if (fixedSomeId) { //Debug.LogWarning("Some subId was adjusted."); // this log is only interesting for Exit Games } }
/// <summary> /// Attempts to create a Photon Cloud Account asynchronously. Blocked while RequestPendingResult is true. /// </summary> /// <remarks> /// Once your callback is called, check ReturnCode, Message and AppId to get the result of this attempt. /// </remarks> /// <param name="email">Email of the account.</param> /// <param name="serviceTypes">Defines which type of Photon-service is being requested.</param> /// <param name="callback">Called when the result is available.</param> /// <param name="errorCallback">Called when the request failed.</param> public bool RegisterByEmail(string email, List <ServiceTypes> serviceTypes, Action <AccountServiceResponse> callback = null, Action <string> errorCallback = null) { if (this.RequestPendingResult) { Debug.LogError("Registration request pending result. Not sending another."); return(false); } if (!IsValidEmail(email)) { Debug.LogErrorFormat("Email \"{0}\" is not valid", email); return(false); } string serviceTypeString = GetServiceTypesFromList(serviceTypes); if (string.IsNullOrEmpty(serviceTypeString)) { Debug.LogError("serviceTypes string is null or empty"); return(false); } string fullUrl = GetUrlWithQueryStringEscaped(email, serviceTypeString); RequestHeaders["x-functions-key"] = string.IsNullOrEmpty(CustomToken) ? DefaultToken : CustomToken; this.RequestPendingResult = true; PhotonEditorUtils.StartCoroutine( PhotonEditorUtils.HttpPost(fullUrl, RequestHeaders, null, s => { this.RequestPendingResult = false; //Debug.LogWarningFormat("received response {0}", s); if (string.IsNullOrEmpty(s)) { if (errorCallback != null) { errorCallback("Server's response was empty. Please register through account website during this service interruption."); } } else { AccountServiceResponse ase = this.ParseResult(s); if (ase == null) { if (errorCallback != null) { errorCallback("Error parsing registration response. Please try registering from account website"); } } else if (callback != null) { callback(ase); } } }, e => { this.RequestPendingResult = false; if (errorCallback != null) { errorCallback(e); } }) ); return(true); }
public override void OnInspectorGUI() { this.m_Target = (PhotonView)this.target; bool isProjectPrefab = PhotonEditorUtils.IsPrefab(this.m_Target.gameObject); bool multiSelected = Selection.gameObjects.Length > 1; if (this.m_Target.ObservedComponents == null) { this.m_Target.ObservedComponents = new System.Collections.Generic.List <Component>(); } if (this.m_Target.ObservedComponents.Count == 0) { this.m_Target.ObservedComponents.Add(null); } EditorGUILayout.BeginHorizontal(); // Owner if (isProjectPrefab) { EditorGUILayout.LabelField("Owner", "<i>Set at runtime</i>", new GUIStyle("Label") { richText = true }, GUILayout.MinWidth(120)); } else if (!this.m_Target.IsOwnerActive) { EditorGUILayout.LabelField("Owner", "Scene", GUILayout.MinWidth(120)); } else { Player owner = this.m_Target.Owner; string ownerInfo = (owner != null) ? owner.NickName : "<no Player found>"; if (string.IsNullOrEmpty(ownerInfo)) { ownerInfo = "<no playername set>"; } EditorGUILayout.LabelField("Owner [" + this.m_Target.OwnerActorNr + "] " + ownerInfo, GUILayout.MinWidth(120)); } // ownership requests EditorGUI.BeginDisabledGroup(Application.isPlaying); OwnershipOption own = (OwnershipOption)EditorGUILayout.EnumPopup(this.m_Target.OwnershipTransfer, GUILayout.MaxWidth(68), GUILayout.MinWidth(68)); if (own != this.m_Target.OwnershipTransfer) { // jf: fixed 5 and up prefab not accepting changes if you quit Unity straight after change. // not touching the define nor the rest of the code to avoid bringing more problem than solving. EditorUtility.SetDirty(this.m_Target); Undo.RecordObject(this.m_Target, "Change PhotonView Ownership Transfer"); this.m_Target.OwnershipTransfer = own; } EditorGUI.EndDisabledGroup(); EditorGUILayout.EndHorizontal(); // View ID - Hide if we are multi-selected if (!multiSelected) { if (isProjectPrefab) { EditorGUILayout.LabelField("View ID", "<i>Set at runtime</i>", new GUIStyle("Label") { richText = true }); } else if (EditorApplication.isPlaying) { EditorGUILayout.LabelField("View ID", this.m_Target.ViewID.ToString()); } else { int idValue = EditorGUILayout.IntField("View ID [1.." + (PhotonNetwork.MAX_VIEW_IDS - 1) + "]", this.m_Target.ViewID); if (this.m_Target.ViewID != idValue) { Undo.RecordObject(this.m_Target, "Change PhotonView viewID"); this.m_Target.ViewID = idValue; } } } // Locally Controlled if (EditorApplication.isPlaying) { string masterClientHint = PhotonNetwork.IsMasterClient ? "(master)" : ""; EditorGUILayout.Toggle("Controlled locally: " + masterClientHint, this.m_Target.IsMine); } // ViewSynchronization (reliability) if (this.m_Target.Synchronization == ViewSynchronization.Off) { GUI.color = Color.grey; } EditorGUILayout.PropertyField(this.serializedObject.FindProperty("Synchronization"), syncronizationGuiContent); GUI.color = Color.white; if (this.m_Target.Synchronization != ViewSynchronization.Off) { if (this.m_Target.ObservedComponents.FindAll(item => item != null).Count == 0) { EditorGUILayout.HelpBox("Setting the synchronization option only makes sense if you observe something.", MessageType.Warning); } } else { // Show warning if there are any observables. The null check is because the list allows nulls. if (Selection.gameObjects.Length == 1) { var observed = m_Target.ObservedComponents; if (observed.Count > 0) { for (int i = 0, cnt = observed.Count; i < cnt; ++i) { if (observed[i] != null) { EditorGUILayout.HelpBox("Observe Option is set to Off. Select a Syncronization setting in order to sync the listed Observables.", MessageType.Warning); break; } } } } } //GUILayout.Space(5); PhotonView.ObservableSearch autoFindObservables = (PhotonView.ObservableSearch)EditorGUILayout.EnumPopup(findObservablesGuiContent, m_Target.observableSearch); if (m_Target.observableSearch != autoFindObservables) { Undo.RecordObject(this.m_Target, "Change Auto Find Observables Toggle"); m_Target.observableSearch = autoFindObservables; } m_Target.FindObservables(); if (!multiSelected) { EditorGUI.BeginDisabledGroup(autoFindObservables != PhotonView.ObservableSearch.Manual); this.DrawObservedComponentsList(autoFindObservables != PhotonView.ObservableSearch.Manual); EditorGUI.EndDisabledGroup(); } // Cleanup: save and fix look if (GUI.changed) { PhotonViewHandler.OnHierarchyChanged(); // TODO: check if needed } GUI.color = Color.white; }