2023-06-24 14:27:50 +02:00
using System.Collections.Generic ;
2023-06-24 16:43:10 +02:00
using System.IO ;
2023-06-26 01:03:14 +02:00
using System.Text.RegularExpressions ;
2023-06-24 14:27:50 +02:00
using UnityEditor ;
2023-06-24 16:43:10 +02:00
using UnityEditor.SceneManagement ;
2023-06-24 14:27:50 +02:00
using UnityEngine ;
2023-06-24 16:43:10 +02:00
using UnityEngine.SceneManagement ;
2023-06-24 14:27:50 +02:00
2023-06-24 16:43:10 +02:00
namespace VertexColor.ScenePartition.Editor
2023-06-24 14:27:50 +02:00
{
[CreateAssetMenu(fileName = "Scene", menuName = "Max/ScenePartitionSO")]
public class ScenePartitionSO : ScriptableObject
{
[field: SerializeField]
2023-06-26 01:03:14 +02:00
public SceneAsset SceneAsset { get ; private set ; } = null ;
public string SceneName = > SceneAsset = = null ? name : SceneAsset . name ;
2023-07-03 23:35:29 +02:00
public List < ulong > alwaysLoadIds = new List < ulong > { 0 , 1 , 2 , 3 , 4 } ;
2023-06-24 16:43:10 +02:00
2023-06-27 01:09:28 +02:00
public ScenePartitionData Data
2023-06-27 00:24:46 +02:00
{
get
{
2023-06-27 01:09:28 +02:00
// Load data from the ScriptableSingleton.
data ? ? = ScenePartitionSS . instance . GetScenePartitionData ( this ) ;
return data ;
2023-06-27 00:24:46 +02:00
}
}
2023-06-27 01:09:28 +02:00
private ScenePartitionData data = null ;
2023-06-27 00:24:46 +02:00
2023-06-24 16:43:10 +02:00
public void CreateScene ( )
{
2023-06-26 01:03:14 +02:00
if ( SceneAsset ! = null ) return ;
2023-06-24 16:43:10 +02:00
string scenePath = Path . Combine ( AssetDatabase . GetAssetPath ( this ) , $"../{this.name}.unity" ) ;
Scene scene = EditorSceneManager . NewScene ( NewSceneSetup . EmptyScene , NewSceneMode . Single ) ;
EditorSceneManager . SaveScene ( scene , scenePath ) ;
2023-06-27 00:24:46 +02:00
Save ( ) ;
2023-06-24 16:43:10 +02:00
2023-06-27 00:24:46 +02:00
AssetDatabase . SaveAssets ( ) ;
2023-06-24 16:43:10 +02:00
AssetDatabase . Refresh ( ) ;
2023-06-25 04:39:12 +02:00
2023-06-26 01:03:14 +02:00
SceneAsset = AssetDatabase . LoadAssetAtPath < SceneAsset > ( scenePath ) ;
2023-06-25 04:39:12 +02:00
AssetDatabase . SaveAssets ( ) ;
AssetDatabase . Refresh ( ) ;
2023-06-24 16:43:10 +02:00
}
2023-06-25 04:39:12 +02:00
/// <summary>
/// Load partitions from disk and construct the scene file.
/// </summary>
2023-06-24 16:43:10 +02:00
public void LoadAll ( )
2023-06-25 04:39:12 +02:00
{
CreateScenePartitions ( ) ;
2023-07-03 23:35:29 +02:00
SortedSet < ulong > ids = new SortedSet < ulong > ( Data . ScenePartitions . Keys ) ;
2023-06-25 04:39:12 +02:00
LoadScenePartitions ( ids ) ;
}
2023-06-27 01:09:28 +02:00
/// <summary>
2023-06-28 01:07:27 +02:00
/// Discard changes and reload loaded partitions.
2023-06-27 01:09:28 +02:00
/// </summary>
public void Reload ( )
{
if ( ! Data . HasLoadedPartitions ) return ;
2023-07-03 23:35:29 +02:00
LoadScenePartitions ( new SortedSet < ulong > ( Data . LoadedScenePartitions . Keys ) ) ;
2023-06-27 01:09:28 +02:00
}
2023-06-25 04:39:12 +02:00
private void CreateScenePartitions ( )
2023-06-24 16:43:10 +02:00
{
string dataPath = ScenePartitionUtils . GetDataPath ( this ) ;
2023-06-25 04:39:12 +02:00
string [ ] files = Directory . GetFiles ( dataPath ) ;
2023-06-27 01:09:28 +02:00
Data . ScenePartitions = new ScenePartitionSortedList ( ) ;
2023-06-25 04:39:12 +02:00
for ( int i = 0 ; i < files . Length ; i + + )
{
ScenePartition scenePartition = new ScenePartition ( files [ i ] ) ;
2023-06-27 01:09:28 +02:00
Data . ScenePartitions . Add ( scenePartition . id , scenePartition ) ;
2023-06-25 04:39:12 +02:00
}
}
2023-07-03 23:35:29 +02:00
private void LoadScenePartitions ( SortedSet < ulong > partitionIds )
2023-06-25 04:39:12 +02:00
{
2023-07-16 19:58:37 +02:00
using ( new ProfilerUtility . ProfilerScope ( $"{nameof(LoadScenePartitions)}" ) )
{
if ( ! Data . HasCreatedPartitions ) return ;
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
string scenePath = ScenePartitionUtils . GetScenePath ( this ) ;
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
Data . LoadedScenePartitions . Clear ( ) ;
2023-06-25 04:39:12 +02:00
2023-07-16 19:58:37 +02:00
// Add always load ids.
SortedSet < ulong > baseIds = GetAlwaysLoadIds ( ) ;
2023-08-10 21:41:57 +02:00
foreach ( ulong id in baseIds )
2023-07-16 19:58:37 +02:00
{
partitionIds . Add ( id ) ;
}
2023-06-25 04:39:12 +02:00
2023-07-16 19:58:37 +02:00
// Clear file.
File . WriteAllText ( scenePath , string . Empty ) ;
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
// Create scene data.
try
{
using ( FileStream outputStream = new FileStream ( scenePath , FileMode . Append , FileAccess . Write ) )
{
foreach ( ulong id in partitionIds )
{
ScenePartition p = Data . ScenePartitions [ id ] ;
using ( FileStream inputStream = new FileStream ( p . filePath , FileMode . Open , FileAccess . Read ) )
{
byte [ ] buffer = new byte [ 4096 ] ;
int bytesRead ;
while ( ( bytesRead = inputStream . Read ( buffer , 0 , buffer . Length ) ) > 0 )
{
outputStream . Write ( buffer , 0 , bytesRead ) ;
}
}
Data . LoadedScenePartitions . Add ( p . id , p ) ;
}
}
}
catch ( System . Exception ex )
{
Debug . LogException ( ex ) ;
}
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
AssetDatabase . Refresh ( ) ;
2023-08-10 22:27:10 +02:00
// 'Reload' the scene to prevent the user getting the popup 'Scene has been changed on disk'.
2023-08-10 22:24:10 +02:00
EditorSceneManager . OpenScene ( scenePath , OpenSceneMode . Single ) ;
2023-07-16 19:58:37 +02:00
}
2023-06-24 16:43:10 +02:00
}
2023-06-25 04:39:12 +02:00
/// <summary>
/// Convert scene to partitions and save them to disk.
/// </summary>
2023-06-25 15:51:54 +02:00
public void Save ( )
2023-06-24 16:43:10 +02:00
{
2023-07-16 19:58:37 +02:00
using ( new ProfilerUtility . ProfilerScope ( $"{nameof(Save)}" ) )
{
2023-08-10 21:41:57 +02:00
// Check if the user wants to save the scene if dirty.
if ( ! EditorSceneManager . SaveCurrentModifiedScenesIfUserWantsTo ( ) ) return ;
2023-07-16 19:58:37 +02:00
DeleteLoadedPartitions ( ) ; // Delete the loaded partitions from disk so we can write the new ones.
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
string pattern = @"&(\d+)" ;
string dataPath = ScenePartitionUtils . GetDataPath ( this ) ;
string scenePath = ScenePartitionUtils . GetScenePath ( this ) ;
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
// Read the data from the scene file.
string [ ] sceneData = File . ReadAllLines ( scenePath ) ;
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
// Split it into blocks.
int lastIndex = sceneData . Length ;
for ( int i = sceneData . Length - 1 ; i > = 0 ; i - - )
2023-06-24 16:43:10 +02:00
{
2023-07-16 19:58:37 +02:00
if ( sceneData [ i ] . StartsWith ( "---" ) ) // --- is the start of a new yaml document.
2023-06-26 01:03:14 +02:00
{
2023-07-16 19:58:37 +02:00
Match match = Regex . Match ( sceneData [ i ] , pattern ) ;
2023-06-26 01:03:14 +02:00
2023-07-16 19:58:37 +02:00
if ( match . Success )
{
2023-08-13 04:24:01 +02:00
// Modify scene data.
// Optional: ClearRootOrderProperty.
ClearRootOrderProperty ( ref sceneData , i , lastIndex ) ;
// Extract the file number.
2023-07-16 19:58:37 +02:00
string id = match . Groups [ 1 ] . Value ;
// Write data to disk.
File . WriteAllLines ( $"{dataPath}/{SceneName}-{id}.yaml" , sceneData [ i . . lastIndex ] ) ;
}
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
lastIndex = i ;
}
2023-06-24 16:43:10 +02:00
}
2023-07-16 19:58:37 +02:00
// Write header to disk.
File . WriteAllLines ( $"{dataPath}/{SceneName}.yaml" , sceneData [ 0. . lastIndex ] ) ;
}
2023-06-24 16:43:10 +02:00
}
2023-06-25 04:39:12 +02:00
/// <summary>
/// Empty the scene and save it (so it has no changes in source control).
/// </summary>
2023-06-24 16:43:10 +02:00
public void Unload ( )
{
2023-07-16 19:58:37 +02:00
using ( new ProfilerUtility . ProfilerScope ( $"{nameof(Unload)}" ) )
{
string dataPath = ScenePartitionUtils . GetDataPath ( this ) ;
string scenePath = ScenePartitionUtils . GetScenePath ( this ) ;
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
Scene scene = EditorSceneManager . OpenScene ( scenePath , OpenSceneMode . Single ) ;
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
GameObject [ ] gameObjects = scene . GetRootGameObjects ( ) ;
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
for ( int i = gameObjects . Length - 1 ; i > = 0 ; i - - )
{
DestroyImmediate ( gameObjects [ i ] ) ;
}
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
EditorSceneManager . SaveScene ( scene ) ;
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
Data . LoadedScenePartitions . Clear ( ) ;
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
AssetDatabase . Refresh ( ) ;
}
2023-06-24 16:43:10 +02:00
}
private void DeleteLoadedPartitions ( )
{
2023-07-16 19:58:37 +02:00
using ( new ProfilerUtility . ProfilerScope ( $"{nameof(DeleteLoadedPartitions)}" ) )
2023-06-24 16:43:10 +02:00
{
2023-07-16 19:58:37 +02:00
if ( ! Data . HasLoadedPartitions ) return ;
foreach ( KeyValuePair < ulong , ScenePartition > scenePartition in Data . LoadedScenePartitions )
{
if ( ! File . Exists ( scenePartition . Value . filePath ) ) continue ;
2023-06-24 16:43:10 +02:00
2023-07-16 19:58:37 +02:00
File . Delete ( scenePartition . Value . filePath ) ;
}
2023-06-24 16:43:10 +02:00
}
}
2023-06-24 14:27:50 +02:00
2023-07-03 23:35:29 +02:00
public void LoadPartitions ( ulong [ ] ids )
2023-06-24 14:27:50 +02:00
{
2023-07-16 19:58:37 +02:00
using ( new ProfilerUtility . ProfilerScope ( $"{nameof(LoadPartitions)}" ) )
2023-06-24 14:27:50 +02:00
{
2023-07-16 19:58:37 +02:00
SortedSet < ulong > partitionIds = new SortedSet < ulong > ( ) ;
for ( int i = 0 ; i < ids . Length ; i + + )
2023-06-25 04:39:12 +02:00
{
2023-07-16 19:58:37 +02:00
SortedSet < ulong > connections = ScenePartitionUtils . FindDeeplyLinkedObjects ( Data . ScenePartitions , ids [ i ] ) ;
foreach ( ulong c in connections )
{
partitionIds . Add ( c ) ;
}
2023-06-25 04:39:12 +02:00
}
2023-07-16 19:58:37 +02:00
LoadScenePartitions ( partitionIds ) ;
}
2023-06-24 14:27:50 +02:00
}
2023-06-28 01:07:27 +02:00
2023-07-29 16:06:03 +02:00
public void LoadPartitionsAdditive ( ulong [ ] ids )
{
using ( new ProfilerUtility . ProfilerScope ( $"{nameof(LoadPartitions)}" ) )
{
SortedSet < ulong > partitionIds = new SortedSet < ulong > ( ) ;
// Additive partitions to load.
for ( int i = 0 ; i < ids . Length ; i + + )
{
SortedSet < ulong > connections = ScenePartitionUtils . FindDeeplyLinkedObjects ( Data . ScenePartitions , ids [ i ] ) ;
foreach ( ulong c in connections )
{
partitionIds . Add ( c ) ;
}
}
// Partitions already loaded.
if ( Data . HasLoadedPartitions )
{
foreach ( KeyValuePair < ulong , ScenePartition > item in Data . LoadedScenePartitions )
{
partitionIds . Add ( item . Key ) ;
}
}
LoadScenePartitions ( partitionIds ) ;
}
}
2023-07-03 23:35:29 +02:00
private SortedSet < ulong > GetAlwaysLoadIds ( )
2023-06-28 01:07:27 +02:00
{
2023-07-03 23:35:29 +02:00
SortedSet < ulong > partitionIds = new SortedSet < ulong > ( ) ;
2023-06-30 03:28:44 +02:00
2023-07-03 23:35:29 +02:00
foreach ( ulong id in alwaysLoadIds )
2023-06-30 03:28:44 +02:00
{
2023-07-03 23:35:29 +02:00
SortedSet < ulong > connections = ScenePartitionUtils . FindDeeplyLinkedObjects ( Data . ScenePartitions , id ) ;
foreach ( ulong c in connections )
2023-06-30 03:28:44 +02:00
{
partitionIds . Add ( c ) ;
}
}
2023-06-28 01:07:27 +02:00
2023-06-30 03:28:44 +02:00
return partitionIds ;
}
public void GenerateSceneGridData ( )
{
2023-07-16 19:58:37 +02:00
using ( new ProfilerUtility . ProfilerScope ( $"{nameof(GenerateSceneGridData)}" ) )
2023-06-30 03:28:44 +02:00
{
2023-07-16 19:58:37 +02:00
LoadAll ( ) ;
if ( ! Data . HasCreatedPartitions ) return ;
Scene scene = EditorSceneManager . OpenScene ( ScenePartitionUtils . GetScenePath ( this ) , OpenSceneMode . Single ) ;
GameObject [ ] rootGameObjects = scene . GetRootGameObjects ( ) ;
Data . SceneGrid . Grid . Clear ( ) ;
//// Maybe later switch to getting the data from disk instead of loading the scene and then unloading it again.
//foreach (GameObject gameObject in rootGameObjects)
//{
// // https://forum.unity.com/threads/how-to-get-the-local-identifier-in-file-for-scene-objects.265686/
// PropertyInfo inspectorModeInfo = typeof(SerializedObject).GetProperty("inspectorMode", BindingFlags.NonPublic | BindingFlags.Instance);
// SerializedObject serializedObject = new SerializedObject(gameObject.transform);
// inspectorModeInfo.SetValue(serializedObject, InspectorMode.Debug, null);
// SerializedProperty localIdProp = serializedObject.FindProperty("m_LocalIdentfierInFile");
// long localId = localIdProp.longValue;
// if (localId == 0)
// {
// if (PrefabUtility.IsPartOfPrefabInstance(gameObject))
// {
// GlobalObjectId id = GlobalObjectId.GetGlobalObjectIdSlow(gameObject); // We could use this funtion for all objects. Might be a bit slower but is also simple.
// localId = long.Parse(id.targetPrefabId.ToString());
// Debug.Log($"{id.assetGUID} | {id.identifierType} | {id.targetObjectId} | {id.targetPrefabId}");
// if (id.targetObjectId == 0 && id.targetPrefabId == 0)
// {
// Debug.LogWarning($"Could not find LocalIdentfierInFile for {gameObject.transform} {gameObject.name} {gameObject.transform.GetInstanceID()}");
// continue;
// }
// }
// else
// {
// Debug.LogWarning($"Could not find LocalIdentfierInFile for {gameObject.transform} {gameObject.name} {gameObject.transform.GetInstanceID()}");
// continue;
// }
// }
// if (!Data.ScenePartitions.ContainsKey(localId))
// {
// Debug.LogWarning($"Could not find LocalIdentfierInFile for {gameObject.transform} {gameObject.name} {gameObject.transform.GetInstanceID()}");
// continue;
// }
// Data.SceneGrid.Insert(localId, gameObject.transform.position);
//}
GlobalObjectId [ ] ids = new GlobalObjectId [ rootGameObjects . Length ] ;
GlobalObjectId . GetGlobalObjectIdsSlow ( rootGameObjects , ids ) ;
for ( int i = 0 ; i < ids . Length ; i + + )
2023-06-28 01:07:27 +02:00
{
2023-07-16 19:58:37 +02:00
//Debug.Log($"{ids[i].assetGUID} | {ids[i].identifierType} | {ids[i].targetObjectId} | {ids[i].targetPrefabId}");
if ( ids [ i ] . targetPrefabId = = 0 ) // 0 = no prefab.
{
Data . SceneGrid . Insert ( ids [ i ] . targetObjectId , rootGameObjects [ i ] . transform . position , ScenePartitionSceneViewEditor . cellSize ) ;
}
else
{
Data . SceneGrid . Insert ( ids [ i ] . targetPrefabId , rootGameObjects [ i ] . transform . position , ScenePartitionSceneViewEditor . cellSize ) ;
}
2023-06-28 01:07:27 +02:00
}
2023-07-16 19:58:37 +02:00
Unload ( ) ;
}
2023-06-28 01:07:27 +02:00
}
public void LoadCell ( int gridId )
{
2023-07-03 23:35:29 +02:00
if ( Data . SceneGrid . Grid . TryGetValue ( gridId , out GridList ids ) )
2023-06-28 01:07:27 +02:00
{
2023-07-03 23:35:29 +02:00
LoadPartitions ( ids . list . ToArray ( ) ) ;
2023-06-28 01:07:27 +02:00
}
}
2023-06-30 03:28:44 +02:00
2023-07-29 16:06:03 +02:00
public void LoadCellAdditive ( int gridId )
{
if ( Data . SceneGrid . Grid . TryGetValue ( gridId , out GridList ids ) )
{
LoadPartitionsAdditive ( ids . list . ToArray ( ) ) ;
}
}
2023-06-30 03:28:44 +02:00
public void ClearCache ( )
{
data = null ;
}
2023-08-13 04:24:01 +02:00
/// <summary>
/// Sets the m_RootOrder property to '0' on all transforms and prefab transform modifications.
/// The property changes every time you add/remove something in the scene for each object underneat it in the hierarchy.
/// This results in a lot of changes in source control that might cause conficts, and we don't want that.
/// This does however change the scene hierarchy order, so if things are order dependent this will break it.
/// </summary>
private void ClearRootOrderProperty ( ref string [ ] data , int i , int lastIndex )
{
using ( new ProfilerUtility . ProfilerScope ( $"{nameof(ClearRootOrderProperty)}" ) )
{
const string prefabHeaderPattern = "--- !u!1001 &" ;
const string transformHeaderPattern = "--- !u!4 &" ;
const string prefabRootOrderPropertyPattern = " propertyPath: m_RootOrder" ;
const string transformRootOrderPropertyPattern = " m_RootOrder:" ;
const string numberPattern = @"[\d-]" ;
// If object is a Prefab.
if ( data [ i ] . StartsWith ( prefabHeaderPattern ) )
{
for ( int j = i ; j < lastIndex ; j + + )
{
if ( ! data [ j ] . StartsWith ( prefabRootOrderPropertyPattern ) ) continue ;
data [ j + 1 ] = Regex . Replace ( data [ j + 1 ] , numberPattern , "0" ) ;
return ;
}
}
// If object is a Transform.
else if ( data [ i ] . StartsWith ( transformHeaderPattern ) )
{
// Reverse loop since property is usually at the bottom of the transform component.
for ( int j = lastIndex - 1 ; j > = 0 ; j - - )
{
if ( ! data [ j ] . StartsWith ( transformRootOrderPropertyPattern ) ) continue ;
data [ j ] = Regex . Replace ( data [ j ] , numberPattern , "0" ) ;
return ;
}
}
}
}
2023-06-24 14:27:50 +02:00
}
}