Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persistent sceneid #563

Merged
merged 16 commits into from Mar 8, 2019
Merged
2 changes: 1 addition & 1 deletion Assets/Mirror/Editor/NetworkInformationPreview.cs
Expand Up @@ -215,7 +215,7 @@ void GetNetworkInformation(GameObject gameObject)
m_Info = new List<NetworkIdentityInfo>
{
GetAssetId(),
GetString("Scene ID", m_Identity.sceneId.ToString())
GetString("Scene ID", m_Identity.sceneId.ToString("X"))
};

if (!Application.isPlaying)
Expand Down
190 changes: 25 additions & 165 deletions Assets/Mirror/Editor/NetworkScenePostProcess.cs
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Callbacks;
Expand All @@ -9,168 +8,20 @@ namespace Mirror
{
public class NetworkScenePostProcess : MonoBehaviour
{
// persistent sceneId assignment to fix readstring bug that occurs when restarting the editor and
// connecting to a build again. sceneids were then different because FindObjectsOfType's order
// is not guranteed to be the same.
// -> we need something unique and persistent, aka always the same when pressing play/building the first time
// -> Unity has no built in unique id for GameObjects in the scene

// helper function to figure out a unique, persistent scene id for a GameObject in the hierarchy
// -> Unity's instanceId is unique but not persistent
// -> hashing the whole GameObject is not enough either since a duplicate would have the same hash
// -> we definitely need the transform sibling index in the hierarchy
// -> so we might as well use just that
// -> transforms have children too so we need a list of sibling indices like 0->3->5
public static List<int> SiblingPathFor(Transform t)
{
List<int> result = new List<int>();
while (t != null)
{
result.Add(t.GetSiblingIndex());
t = t.parent;
}

result.Reverse(); // parent to child instead of child to parent order
return result;
}

// we need to compare by using the whole sibling list
// comparing the string won't work work because:
// "1->2"
// "20->2"
// would compare '1' vs '2', then '-' vs '0'
//
// tests:
// CompareSiblingPaths(new List<int>(){0}, new List<int>(){0}) => 0
// CompareSiblingPaths(new List<int>(){0}, new List<int>(){1}) => -1
// CompareSiblingPaths(new List<int>(){1}, new List<int>(){0}) => 1
// CompareSiblingPaths(new List<int>(){0,1}, new List<int>(){0,2}) => -1
// CompareSiblingPaths(new List<int>(){0,2}, new List<int>(){0,1}) => 1
// CompareSiblingPaths(new List<int>(){1}, new List<int>(){0,1}) => 1
// CompareSiblingPaths(new List<int>(){1}, new List<int>(){2,1}) => -1
public static int CompareSiblingPaths(List<int> left, List<int> right)
{
// compare [0], remove it, compare next, etc.
while (left.Count > 0 && right.Count > 0)
{
if (left[0] < right[0])
{
return -1;
}
else if (left[0] > right[0])
{
return 1;
}
else
{
// equal, so they are both children of the same transform
// -> which also means that they both must have one more
// entry, so we can remove both without checking size
left.RemoveAt(0);
right.RemoveAt(0);
}
}

// equal if both were empty or both had the same entry without any
// more children (should never happen in practice)
return 0;
}

public static int CompareNetworkIdentitySiblingPaths(NetworkIdentity left, NetworkIdentity right)
{
return CompareSiblingPaths(SiblingPathFor(left.transform), SiblingPathFor(right.transform));
}

// we might have inactive scenes in the Editor's build settings, which
// aren't actually included in builds.
// so we have to only count the active ones when in Editor, otherwise
// editor and build sceneIds might get out of sync.
public static int GetSceneCount()
{
#if UNITY_EDITOR
return EditorBuildSettings.scenes.Count(scene => scene.enabled);
#else
return SceneManager.sceneCountInBuildSettings;
#endif
}
// helper function to check if a NetworkIdentity is in the active scene
static bool InActiveScene(NetworkIdentity identity) =>
identity.gameObject.scene == SceneManager.GetActiveScene();

[PostProcessScene]
public static void OnPostProcessScene()
{
// vis2k: MISMATCHING SCENEID BUG FIX
// problem:
// * FindObjectsOfType order is not guaranteed. restarting the
// editor results in a different order
// * connecting to a build again would cause Mirror to deserialize
// the wrong objects, causing all kinds of weird errors like
// 'ReadString out of range'
//
// solution:
// sort by sibling-index path, e.g. [0,1,2] vs [1]
// this is the only deterministic way to sort a list of objects in
// the scene.
// -> it's the same result every single time, even after restarts
//
// note: there is a reason why we 'sort by' sibling path instead of
// using it as sceneId directly. networkmanager etc. use Dont-
// DestroyOnLoad, which changes the hierarchy:
//
// World:
// NetworkManager
// Player
//
// ..becomes..
//
// World:
// Player
// DontDestroyOnLoad:
// NetworkManager
//
// so the player's siblingindex would be decreased by one.
// -> this is a problem because when building, OnPostProcessScene
// is called before any dontdestroyonload happens, but when
// entering play mode, it's called after
// -> hence sceneids would differ by one
//
// => but if we only SORT it, then it doesn't matter if one
// inbetween disappeared. as long as no NetworkIdentity used
// DontDestroyOnLoad.
//
// note: assigning a GUID in NetworkIdentity.OnValidate would be way
// cooler, but OnValidate isn't called for other unopened scenes
// when building or pressing play, so the bug would still happen
// there.
//
// note: this can still fail if DontDestroyOnLoad is called for a
// NetworkIdentity - but no one should ever do that anyway.
List<NetworkIdentity> identities = FindObjectsOfType<NetworkIdentity>().ToList();
identities.Sort(CompareNetworkIdentitySiblingPaths);

// sceneId assignments need to work with additive scene loading, so
// it can't always start at 1,2,3,4,..., otherwise there will be
// sceneId duplicates.
// -> we need an offset to start at 1000+1,+2,+3, etc.
// -> the most robust way is to split uint value range by sceneCount
// -> only if more than one scene. otherwise use offset 0 to avoid
// DivisionByZero if no scene in build settings, and to avoid
// different offsets in editor/build if scene wasn't added to
// build settings.
uint offsetPerScene = 0;
if (SceneManager.sceneCountInBuildSettings > 1)
{
offsetPerScene = uint.MaxValue / (uint)GetSceneCount();

// make sure that there aren't more sceneIds than offsetPerScene
// -> only if we have multiple scenes. otherwise offset is 0, in
// which case it doesn't matter.
if (identities.Count >= offsetPerScene)
{
Debug.LogWarning(">=" + offsetPerScene + " NetworkIdentities in scene. Additive scene loading will cause duplicate ids.");
}
}

uint nextSceneId = 1;
foreach (NetworkIdentity identity in identities)
// find all NetworkIdentities in this scene
// => but really only from this scene. this avoids weird situations
// like in NetworkZones when we destroy the local player and
// load another scene afterwards, yet the local player is still
// in the FindObjectsOfType result with scene=DontDestroyOnLoad
// for some reason
foreach (NetworkIdentity identity in FindObjectsOfType<NetworkIdentity>().Where(InActiveScene))
{
// if we had a [ConflictComponent] attribute that would be better than this check.
// also there is no context about which scene this is in.
Expand All @@ -181,13 +32,22 @@ public static void OnPostProcessScene()
if (identity.isClient || identity.isServer)
continue;

uint offset = (uint)identity.gameObject.scene.buildIndex * offsetPerScene;
identity.ForceSceneId(offset + nextSceneId++);
if (LogFilter.Debug) Debug.Log("PostProcess sceneid assigned: name=" + identity.name + " scene=" + identity.gameObject.scene.name + " sceneid=" + identity.sceneId);
// valid scene id? then set build index byte
// otherwise it might be an unopened scene that still has null
// sceneIds. builds are interrupted if they contain 0 sceneIds,
// but it's still possible that we call LoadScene in Editor
// for a previously unopened scene.
// => throwing an exception would only show it for one object
// because this function would return afterwards.
if (identity.sceneId != 0)
{
identity.SetSceneIdSceneIndexByteInternal();
}
else Debug.LogError("Scene " + identity.gameObject.scene.path + " needs to be opened and resaved, because the scene object " + identity.name + " has no valid sceneId yet.");

// disable it AFTER assigning the sceneId.
// -> this way NetworkIdentity.OnDisable adds itself to the
// spawnableObjects dictionary (only if sceneId != 0)
// disable it
// note: NetworkIdentity.OnDisable adds itself to the
// spawnableObjects dictionary (only if sceneId != 0)
identity.gameObject.SetActive(false);

// safety check for prefabs with more than one NetworkIdentity
Expand Down